diff --git a/change/@microsoft-fast-element-19034156-dca2-4efc-8995-4003df6c2107.json b/change/@microsoft-fast-element-19034156-dca2-4efc-8995-4003df6c2107.json new file mode 100644 index 00000000000..71293dae9c1 --- /dev/null +++ b/change/@microsoft-fast-element-19034156-dca2-4efc-8995-4003df6c2107.json @@ -0,0 +1,7 @@ +{ + "type": "major", + "comment": "feat: add splice strategies for array observation", + "packageName": "@microsoft/fast-element", + "email": "roeisenb@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@microsoft-fast-foundation-0f273d05-e607-443b-ae31-aa5ab842432a.json b/change/@microsoft-fast-foundation-0f273d05-e607-443b-ae31-aa5ab842432a.json new file mode 100644 index 00000000000..ad20afd8611 --- /dev/null +++ b/change/@microsoft-fast-foundation-0f273d05-e607-443b-ae31-aa5ab842432a.json @@ -0,0 +1,7 @@ +{ + "type": "major", + "comment": "chore: update imports to match latest fast-element exports", + "packageName": "@microsoft/fast-foundation", + "email": "roeisenb@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/web-components/fast-element/docs/api-report.md b/packages/web-components/fast-element/docs/api-report.md index 60e857fe552..51af4a73528 100644 --- a/packages/web-components/fast-element/docs/api-report.md +++ b/packages/web-components/fast-element/docs/api-report.md @@ -30,31 +30,43 @@ export class AdoptedStyleSheetsStrategy implements StyleStrategy { readonly sheets: CSSStyleSheet[]; } +// @public +export interface ArrayObserver extends SubscriberSet { + addSplice(splice: Splice): void; + flush(): void; + readonly lengthObserver: LengthObserver; + reset(oldCollection: any[] | undefined): void; + strategy: SpliceStrategy | null; +} + +// @public +export const ArrayObserver: Readonly<{ + readonly enable: () => void; +}>; + // @public export const Aspect: Readonly<{ - none: 0; - attribute: 1; - booleanAttribute: 2; - property: 3; - content: 4; - tokenList: 5; - event: 6; - assign(directive: Aspected, value: string): void; + readonly none: 0; + readonly attribute: 1; + readonly booleanAttribute: 2; + readonly property: 3; + readonly content: 4; + readonly tokenList: 5; + readonly event: 6; + readonly assign: (directive: Aspected, value: string) => void; }>; +// @public +export type Aspect = typeof Aspect[Exclude]; + // @public export interface Aspected { - aspectType: AspectType; + aspectType: Aspect; binding?: Binding; sourceAspect: string; targetAspect: string; } -// @public -export type AspectType = Exclude<{ - [K in keyof typeof Aspect]: typeof Aspect[K] extends number ? typeof Aspect[K] : never; -}[keyof typeof Aspect], typeof Aspect.none>; - // @public export function attr(config?: DecoratorAttributeConfiguration): (target: {}, property: string) => void; @@ -118,7 +130,7 @@ export const BindingConfig: Readonly<{ export type BindingConfigResolver = (options: T) => BindingConfig; // @public -export type BindingMode = Record Pick>; +export type BindingMode = Record Pick>; // @public export const BindingMode: Readonly<{ @@ -345,9 +357,6 @@ export interface ElementViewTemplate { // @internal export const emptyArray: readonly never[]; -// @public -export function enableArrayObservation(): void; - // @public export class EventBinding { constructor(directive: HTMLBindingDirective); @@ -430,7 +439,7 @@ export function html { function length_2(array: readonly T[]): number; export { length_2 as length } +// @public +export interface LengthObserver extends Subscriber { + length: number; +} + // @public export const Markup: Readonly<{ interpolation: (id: string) => string; @@ -733,17 +747,46 @@ export interface SlottedDirectiveOptions extends NodeBehaviorOptions // @public export class Splice { - constructor( - index: number, - removed: any[], - addedCount: number); + constructor(index: number, removed: any[], addedCount: number); + // (undocumented) addedCount: number; + adjustTo(array: any[]): this; + // (undocumented) index: number; // (undocumented) - static normalize(previous: unknown[] | undefined, current: unknown[], changes: Splice[] | undefined): Splice[] | undefined; removed: any[]; + reset?: boolean; } +// @public +export interface SpliceStrategy { + normalize(previous: unknown[] | undefined, current: unknown[], changes: Splice[] | undefined): readonly Splice[]; + pop(array: any[], observer: ArrayObserver, pop: typeof Array.prototype.pop, args: any[]): any; + push(array: any[], observer: ArrayObserver, push: typeof Array.prototype.push, args: any[]): any; + reverse(array: any[], observer: ArrayObserver, reverse: typeof Array.prototype.reverse, args: any[]): any; + shift(array: any[], observer: ArrayObserver, shift: typeof Array.prototype.shift, args: any[]): any; + sort(array: any[], observer: ArrayObserver, sort: typeof Array.prototype.sort, args: any[]): any[]; + splice(array: any[], observer: ArrayObserver, splice: typeof Array.prototype.splice, args: any[]): any; + readonly support: SpliceStrategySupport; + unshift(array: any[], observer: ArrayObserver, unshift: typeof Array.prototype.unshift, args: any[]): any[]; +} + +// @public +export const SpliceStrategy: Readonly<{ + readonly reset: Splice[]; + readonly setDefaultStrategy: (strategy: SpliceStrategy) => void; +}>; + +// @public +export const SpliceStrategySupport: Readonly<{ + readonly reset: 1; + readonly splice: 2; + readonly optimized: 3; +}>; + +// @public +export type SpliceStrategySupport = typeof SpliceStrategySupport[keyof typeof SpliceStrategySupport]; + // @public export abstract class StatelessAttachedAttributeDirective implements HTMLDirective, ViewBehaviorFactory, ViewBehavior { constructor(options: T); diff --git a/packages/web-components/fast-element/package.json b/packages/web-components/fast-element/package.json index 1e5f0a6c8e0..12af2927811 100644 --- a/packages/web-components/fast-element/package.json +++ b/packages/web-components/fast-element/package.json @@ -33,6 +33,10 @@ "./debug.js": { "types": "./dist/dts/debug.d.ts", "default": "./dist/esm/debug.js" + }, + "./splice-strategies.js": { + "types": "./dist/dts/observation/splice-strategies.d.ts", + "default": "./dist/esm/observation/splice-strategies.js" } }, "unpkg": "dist/fast-element.min.js", diff --git a/packages/web-components/fast-element/src/index.ts b/packages/web-components/fast-element/src/index.ts index 03e44761575..78ccf6540e5 100644 --- a/packages/web-components/fast-element/src/index.ts +++ b/packages/web-components/fast-element/src/index.ts @@ -14,8 +14,7 @@ export * from "./platform.js"; // Observation export * from "./observation/observable.js"; export * from "./observation/notifier.js"; -export * from "./observation/array-change-records.js"; -export * from "./observation/array-observer.js"; +export * from "./observation/arrays.js"; export * from "./observation/update-queue.js"; export type { Behavior } from "./observation/behavior.js"; diff --git a/packages/web-components/fast-element/src/observation/array-observer.spec.ts b/packages/web-components/fast-element/src/observation/array-observer.spec.ts deleted file mode 100644 index b8b0b6bcfea..00000000000 --- a/packages/web-components/fast-element/src/observation/array-observer.spec.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { expect } from "chai"; -import { Observable } from "./observable"; -import { enableArrayObservation, length } from "./array-observer"; -import { SubscriberSet } from "./notifier"; -import { Updates } from "./update-queue"; - -describe("The ArrayObserver", () => { - it("can be retrieved through Observable.getNotifier()", () => { - enableArrayObservation(); - const array = []; - const notifier = Observable.getNotifier(array); - expect(notifier).to.be.instanceOf(SubscriberSet); - }); - - it("is the same instance for multiple calls to Observable.getNotifier() on the same array", () => { - enableArrayObservation(); - const array = []; - const notifier = Observable.getNotifier(array); - const notifier2 = Observable.getNotifier(array); - expect(notifier).to.equal(notifier2); - }); - - it("is different for different arrays", () => { - enableArrayObservation(); - const notifier = Observable.getNotifier([]); - const notifier2 = Observable.getNotifier([]); - expect(notifier).to.not.equal(notifier2); - }); - - it("doesn't affect for/in loops on arrays when enabled", () => { - enableArrayObservation(); - - const array = [1, 2, 3]; - const keys: string[] = []; - - for (const key in array) { - keys.push(key); - } - - expect(keys).eql(["0", "1", "2"]); - }); - - it("doesn't affect for/in loops on arrays when the array is observed", () => { - enableArrayObservation(); - - const array = [1, 2, 3]; - const keys: string[] = []; - const notifier = Observable.getNotifier(array); - - for (const key in array) { - keys.push(key); - } - - expect(notifier).to.be.instanceOf(SubscriberSet); - expect(keys).eql(["0", "1", "2"]) - }); -}); - -describe("The array length observer", () => { - class Model { - items: any[]; - } - - it("returns zero length if the array is undefined", async () => { - const instance = new Model(); - const observer = Observable.binding(x => length(x.items)); - - const value = observer.observe(instance) - - expect(value).to.equal(0); - - observer.disconnect(); - }); - - it("returns zero length if the array is null", async () => { - const instance = new Model(); - instance.items = null as any; - const observer = Observable.binding(x => length(x.items)); - - const value = observer.observe(instance) - - expect(value).to.equal(0); - - observer.disconnect(); - }); - - it("returns length of an array", async () => { - const instance = new Model(); - instance.items = [1,2,3,4,5]; - const observer = Observable.binding(x => length(x.items)); - - const value = observer.observe(instance) - - expect(value).to.equal(5); - - observer.disconnect(); - }); - - it("notifies when the array length changes", async () => { - const instance = new Model(); - instance.items = [1,2,3,4,5]; - - let changed = false; - const observer = Observable.binding(x => length(x.items), { - handleChange() { - changed = true; - } - }); - - const value = observer.observe(instance) - - expect(value).to.equal(5); - - instance.items.push(6); - - await Updates.next(); - - expect(changed).to.be.true; - expect(observer.observe(instance)).to.equal(6); - - observer.disconnect(); - }); - - it("does not notify on changes that don't change the length", async () => { - const instance = new Model(); - instance.items = [1,2,3,4,5]; - - let changed = false; - const observer = Observable.binding(x => length(x.items), { - handleChange() { - changed = true; - } - }); - - const value = observer.observe(instance); - - expect(value).to.equal(5); - - instance.items.splice(2, 1, 6); - - await Updates.next(); - - expect(changed).to.be.false; - expect(observer.observe(instance)).to.equal(5); - - observer.disconnect(); - }); -}); diff --git a/packages/web-components/fast-element/src/observation/array-observer.ts b/packages/web-components/fast-element/src/observation/array-observer.ts deleted file mode 100644 index a7fe7979982..00000000000 --- a/packages/web-components/fast-element/src/observation/array-observer.ts +++ /dev/null @@ -1,269 +0,0 @@ -import { Updates } from "./update-queue.js"; -import { Splice } from "./array-change-records.js"; -import { Subscriber, SubscriberSet } from "./notifier.js"; -import type { Notifier } from "./notifier.js"; -import { Observable } from "./observable.js"; - -function setNonEnumerable(target: any, property: string, value: any): void { - Reflect.defineProperty(target, property, { - value, - enumerable: false, - }); -} - -interface LengthSubscriber extends Subscriber { - length: number; -} - -class ArrayObserver extends SubscriberSet { - private oldCollection: any[] | undefined = void 0; - private splices: Splice[] | undefined = void 0; - private needsQueue: boolean = true; - - /** @internal */ - public lengthSubscriber: LengthSubscriber | undefined = void 0; - - call: () => void = this.flush; - - constructor(subject: any[]) { - super(subject); - setNonEnumerable(subject, "$fastController", this); - } - - public addSplice(splice: Splice): void { - if (this.splices === void 0) { - this.splices = [splice]; - } else { - this.splices.push(splice); - } - - this.enqueue(); - } - - public reset(oldCollection: any[] | undefined): void { - this.oldCollection = oldCollection; - this.enqueue(); - } - - public flush(): void { - const splices = this.splices; - const oldCollection = this.oldCollection; - - if (splices === void 0 && oldCollection === void 0) { - return; - } - - this.needsQueue = true; - this.splices = void 0; - this.oldCollection = void 0; - - this.notify(Splice.normalize(oldCollection, this.subject, splices)); - } - - private enqueue(): void { - if (this.needsQueue) { - this.needsQueue = false; - Updates.enqueue(this); - } - } -} - -let enabled = false; - -/* eslint-disable prefer-rest-params */ -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -/** - * Enables the array observation mechanism. - * @remarks - * Array observation is enabled automatically when using the - * {@link RepeatDirective}, so calling this API manually is - * not typically necessary. - * @public - */ -export function enableArrayObservation(): void { - if (enabled) { - return; - } - - enabled = true; - - Observable.setArrayObserverFactory( - (collection: any[]): Notifier => new ArrayObserver(collection) - ); - - const proto = Array.prototype; - - if (!(proto as any).$fastPatch) { - setNonEnumerable(proto, "$fastPatch", 1); - - const pop = proto.pop; - const push = proto.push; - const reverse = proto.reverse; - const shift = proto.shift; - const sort = proto.sort; - const splice = proto.splice; - const unshift = proto.unshift; - const adjustIndex = (changeRecord: Splice, array: any[]): Splice => { - let index = changeRecord.index; - const arrayLength = array.length; - - if (index > arrayLength) { - index = arrayLength - changeRecord.addedCount; - } else if (index < 0) { - index = - arrayLength + - changeRecord.removed.length + - index - - changeRecord.addedCount; - } - - changeRecord.index = index < 0 ? 0 : index; - return changeRecord; - }; - - Object.assign(proto, { - pop(...args) { - const notEmpty = this.length > 0; - const result = pop.apply(this, args); - const o = this.$fastController as ArrayObserver; - - if (o !== void 0 && notEmpty) { - o.addSplice(new Splice(this.length, [result], 0)); - } - - return result; - }, - - push(...args) { - const result = push.apply(this, args); - const o = this.$fastController as ArrayObserver; - - if (o !== void 0) { - o.addSplice( - adjustIndex( - new Splice(this.length - args.length, [], args.length), - this - ) - ); - } - - return result; - }, - - reverse(...args) { - let oldArray; - const o = this.$fastController as ArrayObserver; - - if (o !== void 0) { - o.flush(); - oldArray = this.slice(); - } - - const result = reverse.apply(this, args); - - if (o !== void 0) { - o.reset(oldArray); - } - - return result; - }, - - shift(...args) { - const notEmpty = this.length > 0; - const result = shift.apply(this, args); - const o = this.$fastController as ArrayObserver; - - if (o !== void 0 && notEmpty) { - o.addSplice(new Splice(0, [result], 0)); - } - - return result; - }, - - sort(...args) { - let oldArray; - const o = this.$fastController as ArrayObserver; - - if (o !== void 0) { - o.flush(); - oldArray = this.slice(); - } - - const result = sort.apply(this, args); - - if (o !== void 0) { - o.reset(oldArray); - } - - return result; - }, - - splice(...args) { - const result = splice.apply(this, args); - const o = this.$fastController as ArrayObserver; - - if (o !== void 0) { - o.addSplice( - adjustIndex( - new Splice( - +args[0], - result, - args.length > 2 ? args.length - 2 : 0 - ), - this - ) - ); - } - - return result; - }, - - unshift(...args) { - const result = unshift.apply(this, args); - const o = this.$fastController as ArrayObserver; - - if (o !== void 0) { - o.addSplice(adjustIndex(new Splice(0, [], args.length), this)); - } - - return result; - }, - }); - } -} - -/** - * Enables observing the length of an array. - * @param array - The array to observe the length of. - * @returns The length of the array. - * @public - */ -export function length(array: readonly T[]): number { - if (!array) { - return 0; - } - - let arrayObserver = (array as any).$fastController as ArrayObserver; - if (arrayObserver === void 0) { - enableArrayObservation(); - arrayObserver = Observable.getNotifier(array); - } - - let lengthSubscriber = arrayObserver.lengthSubscriber; - if (lengthSubscriber === void 0) { - arrayObserver.lengthSubscriber = lengthSubscriber = { - length: array.length, - handleChange() { - if (this.length !== array.length) { - this.length = array.length; - Observable.notify(lengthSubscriber, "length"); - } - }, - }; - - arrayObserver.subscribe(lengthSubscriber); - } - - Observable.track(lengthSubscriber, "length"); - return array.length; -} diff --git a/packages/web-components/fast-element/src/observation/arrays.spec.ts b/packages/web-components/fast-element/src/observation/arrays.spec.ts new file mode 100644 index 00000000000..0c1cd50ebff --- /dev/null +++ b/packages/web-components/fast-element/src/observation/arrays.spec.ts @@ -0,0 +1,430 @@ +import { expect } from "chai"; +import { Observable } from "./observable"; +import { ArrayObserver, length, Splice } from "./arrays"; +import { SubscriberSet } from "./notifier"; +import { Updates } from "./update-queue"; + +describe("The ArrayObserver", () => { + it("can be retrieved through Observable.getNotifier()", () => { + ArrayObserver.enable(); + const array = []; + const notifier = Observable.getNotifier(array); + expect(notifier).to.be.instanceOf(SubscriberSet); + }); + + it("is the same instance for multiple calls to Observable.getNotifier() on the same array", () => { + ArrayObserver.enable(); + const array = []; + const notifier = Observable.getNotifier(array); + const notifier2 = Observable.getNotifier(array); + expect(notifier).to.equal(notifier2); + }); + + it("is different for different arrays", () => { + ArrayObserver.enable(); + const notifier = Observable.getNotifier([]); + const notifier2 = Observable.getNotifier([]); + expect(notifier).to.not.equal(notifier2); + }); + + it("doesn't affect for/in loops on arrays when enabled", () => { + ArrayObserver.enable(); + + const array = [1, 2, 3]; + const keys: string[] = []; + + for (const key in array) { + keys.push(key); + } + + expect(keys).eql(["0", "1", "2"]); + }); + + it("doesn't affect for/in loops on arrays when the array is observed", () => { + ArrayObserver.enable(); + + const array = [1, 2, 3]; + const keys: string[] = []; + const notifier = Observable.getNotifier(array); + + for (const key in array) { + keys.push(key); + } + + expect(notifier).to.be.instanceOf(SubscriberSet); + expect(keys).eql(["0", "1", "2"]) + }); + + it("observes pops", async () => { + ArrayObserver.enable(); + const array = ["foo", "bar", "hello", "world"]; + + array.pop(); + expect(array).members(["foo", "bar", "hello"]); + + Array.prototype.pop.call(array); + expect(array).members(["foo", "bar"]); + + const observer = Observable.getNotifier(array); + let changeArgs: Splice[] | null = null; + + observer.subscribe({ + handleChange(array, args) { + changeArgs = args; + } + }); + + array.pop(); + expect(array).members(["foo"]); + + await Updates.next(); + + expect(changeArgs).length(1); + expect(changeArgs![0].addedCount).equal(0); + expect(changeArgs![0].removed).members(["bar"]); + expect(changeArgs![0].index).equal(1); + + Array.prototype.pop.call(array); + expect(array).members([]); + + await Updates.next(); + + expect(changeArgs).length(1); + expect(changeArgs![0].addedCount).equal(0); + expect(changeArgs![0].removed).members(["foo"]); + expect(changeArgs![0].index).equal(0); + }); + + it("observes pushes", async () => { + ArrayObserver.enable(); + const array: string[] = []; + + array.push("foo"); + expect(array).members(["foo"]); + + Array.prototype.push.call(array, "bar"); + expect(array).members(["foo", "bar"]); + + const observer = Observable.getNotifier(array); + let changeArgs: Splice[] | null = null; + + observer.subscribe({ + handleChange(array, args) { + changeArgs = args; + } + }); + + array.push("hello"); + expect(array).members(["foo", "bar", "hello"]); + + await Updates.next(); + + expect(changeArgs).length(1); + expect(changeArgs![0].addedCount).equal(1); + expect(changeArgs![0].removed).members([]); + expect(changeArgs![0].index).equal(2); + + Array.prototype.push.call(array, "world"); + expect(array).members(["foo", "bar", "hello", "world"]); + + await Updates.next(); + + expect(changeArgs).length(1); + expect(changeArgs![0].addedCount).equal(1); + expect(changeArgs![0].removed).members([]); + expect(changeArgs![0].index).equal(3); + }); + + it("observes reverses", async () => { + ArrayObserver.enable(); + const array = [1, 2, 3, 4]; + array.reverse(); + + expect(array).members([4, 3, 2, 1]); + + Array.prototype.reverse.call(array); + expect(array).members([1, 2, 3, 4]); + + const observer = Observable.getNotifier(array); + let changeArgs: Splice[] | null = null; + + observer.subscribe({ + handleChange(array, args) { + changeArgs = args; + } + }); + + array.reverse(); + expect(array).members([4, 3, 2, 1]); + + await Updates.next(); + + expect(changeArgs).length(1); + expect(changeArgs![0].addedCount).equal(0); + expect(changeArgs![0].removed).members([]); + expect(changeArgs![0].index).equal(0); + expect(changeArgs![0].reset).equal(true); + + Array.prototype.reverse.call(array); + expect(array).members([1, 2, 3, 4]); + + expect(changeArgs).length(1); + expect(changeArgs![0].addedCount).equal(0); + expect(changeArgs![0].removed).members([]); + expect(changeArgs![0].index).equal(0); + expect(changeArgs![0].reset).equal(true); + }); + + it("observes shifts", async () => { + ArrayObserver.enable(); + const array = ["foo", "bar", "hello", "world"]; + + array.shift(); + expect(array).members(["bar", "hello", "world"]); + + Array.prototype.shift.call(array); + expect(array).members(["hello", "world"]); + + const observer = Observable.getNotifier(array); + let changeArgs: Splice[] | null = null; + + observer.subscribe({ + handleChange(array, args) { + changeArgs = args; + } + }); + + array.shift(); + expect(array).members(["world"]); + + await Updates.next(); + + expect(changeArgs).length(1); + expect(changeArgs![0].addedCount).equal(0); + expect(changeArgs![0].removed).members(["hello"]); + expect(changeArgs![0].index).equal(0); + + Array.prototype.shift.call(array); + expect(array).members([]); + + await Updates.next(); + + expect(changeArgs).length(1); + expect(changeArgs![0].addedCount).equal(0); + expect(changeArgs![0].removed).members(["world"]); + expect(changeArgs![0].index).equal(0); + }); + + it("observes sorts", async () => { + ArrayObserver.enable(); + let array = [1, 2, 3, 4]; + + array.sort((a, b) => b - a); + expect(array).members([4, 3, 2, 1]); + + Array.prototype.sort.call(array, (a, b) => a - b); + expect(array).members([1, 2, 3, 4]); + + const observer = Observable.getNotifier(array); + let changeArgs: Splice[] | null = null; + + observer.subscribe({ + handleChange(array, args) { + changeArgs = args; + } + }); + + array.sort((a, b) => b - a); + expect(array).members([4, 3, 2, 1]); + + await Updates.next(); + + expect(changeArgs).length(1); + expect(changeArgs![0].addedCount).equal(0); + expect(changeArgs![0].removed).members([]); + expect(changeArgs![0].index).equal(0); + expect(changeArgs![0].reset).equal(true); + + Array.prototype.sort.call(array, (a, b) => a - b); + expect(array).members([1, 2, 3, 4]); + + await Updates.next(); + + expect(changeArgs).length(1); + expect(changeArgs![0].addedCount).equal(0); + expect(changeArgs![0].removed).members([]); + expect(changeArgs![0].index).equal(0); + expect(changeArgs![0].reset).equal(true); + }); + + it("observes splices", async () => { + ArrayObserver.enable(); + let array: any[] = [1, 2, 3, 4]; + + array.splice(1, 1, 'hello'); + expect(array).members([1, "hello", 3, 4]) + + Array.prototype.splice.call(array, 2, 1, "world"); + expect(array).members([1, "hello", "world", 4]); + + const observer = Observable.getNotifier(array); + let changeArgs: Splice[] | null = null; + + observer.subscribe({ + handleChange(array, args) { + changeArgs = args; + } + }); + + array.splice(1, 1, "foo"); + expect(array).members([1, "foo", "world", 4]); + + await Updates.next(); + + expect(changeArgs).length(1); + expect(changeArgs![0].addedCount).equal(1); + expect(changeArgs![0].removed).members(["hello"]); + expect(changeArgs![0].index).equal(1); + + Array.prototype.splice.call(array, 2, 1, 'bar'); + expect(array).members([1, "foo", "bar", 4]); + + await Updates.next(); + + expect(changeArgs).length(1); + expect(changeArgs![0].addedCount).equal(1); + expect(changeArgs![0].removed).members(["world"]); + expect(changeArgs![0].index).equal(2); + }); + + it("observes unshifts", async () => { + ArrayObserver.enable(); + let array: string[] = []; + + array.unshift("foo"); + expect(array).members(["foo"]) + + Array.prototype.unshift.call(array, "bar"); + expect(array).members(["bar", "foo"]); + + const observer = Observable.getNotifier(array); + let changeArgs: Splice[] | null = null; + + observer.subscribe({ + handleChange(array, args) { + changeArgs = args; + } + }); + + array.unshift("hello"); + expect(array).members(["hello", "bar", "foo"]); + + await Updates.next(); + + expect(changeArgs).length(1); + expect(changeArgs![0].addedCount).equal(1); + expect(changeArgs![0].removed).members([]); + expect(changeArgs![0].index).equal(0); + + Array.prototype.unshift.call(array, 'world'); + expect(array).members(["world", "hello", "bar", "foo"]); + + await Updates.next(); + + expect(changeArgs).length(1); + expect(changeArgs![0].addedCount).equal(1); + expect(changeArgs![0].removed).members([]); + expect(changeArgs![0].index).equal(0); + }); +}); + +describe("The array length observer", () => { + class Model { + items: any[]; + } + + it("returns zero length if the array is undefined", async () => { + const instance = new Model(); + const observer = Observable.binding(x => length(x.items)); + + const value = observer.observe(instance) + + expect(value).to.equal(0); + + observer.disconnect(); + }); + + it("returns zero length if the array is null", async () => { + const instance = new Model(); + instance.items = null as any; + const observer = Observable.binding(x => length(x.items)); + + const value = observer.observe(instance) + + expect(value).to.equal(0); + + observer.disconnect(); + }); + + it("returns length of an array", async () => { + const instance = new Model(); + instance.items = [1,2,3,4,5]; + const observer = Observable.binding(x => length(x.items)); + + const value = observer.observe(instance) + + expect(value).to.equal(5); + + observer.disconnect(); + }); + + it("notifies when the array length changes", async () => { + const instance = new Model(); + instance.items = [1,2,3,4,5]; + + let changed = false; + const observer = Observable.binding(x => length(x.items), { + handleChange() { + changed = true; + } + }); + + const value = observer.observe(instance) + + expect(value).to.equal(5); + + instance.items.push(6); + + await Updates.next(); + + expect(changed).to.be.true; + expect(observer.observe(instance)).to.equal(6); + + observer.disconnect(); + }); + + it("does not notify on changes that don't change the length", async () => { + const instance = new Model(); + instance.items = [1,2,3,4,5]; + + let changed = false; + const observer = Observable.binding(x => length(x.items), { + handleChange() { + changed = true; + } + }); + + const value = observer.observe(instance); + + expect(value).to.equal(5); + + instance.items.splice(2, 1, 6); + + await Updates.next(); + + expect(changed).to.be.false; + expect(observer.observe(instance)).to.equal(5); + + observer.disconnect(); + }); +}); diff --git a/packages/web-components/fast-element/src/observation/arrays.ts b/packages/web-components/fast-element/src/observation/arrays.ts new file mode 100644 index 00000000000..ea240ab242e --- /dev/null +++ b/packages/web-components/fast-element/src/observation/arrays.ts @@ -0,0 +1,544 @@ +import { emptyArray } from "../platform.js"; +import { Notifier, Subscriber, SubscriberSet } from "./notifier.js"; +import { Observable } from "./observable.js"; +import { Updates } from "./update-queue.js"; + +/** + * A splice map is a representation of how a previous array of items + * was transformed into a new array of items. Conceptually it is a list of + * tuples of + * + * (index, removed, addedCount) + * + * which are kept in ascending index order of. The tuple represents that at + * the |index|, |removed| sequence of items were removed, and counting forward + * from |index|, |addedCount| items were added. + * @public + */ +export class Splice { + /** + * Indicates that this splice represents a complete array reset. + */ + public reset?: boolean; + + /** + * Creates a splice. + * @param index - The index that the splice occurs at. + * @param removed - The items that were removed. + * @param addedCount - The number of items that were added. + */ + public constructor( + public index: number, + public removed: any[], + public addedCount: number + ) {} + + /** + * Adjusts the splice index based on the provided array. + * @param array - The array to adjust to. + * @returns The same splice, mutated based on the reference array. + */ + public adjustTo(array: any[]): this { + let index = this.index; + const arrayLength = array.length; + + if (index > arrayLength) { + index = arrayLength - this.addedCount; + } else if (index < 0) { + index = arrayLength + this.removed.length + index - this.addedCount; + } + + this.index = index < 0 ? 0 : index; + return this; + } +} + +/** + * Indicates what level of feature support the splice + * strategy provides. + * @public + */ +export const SpliceStrategySupport = Object.freeze({ + /** + * Only supports resets. + */ + reset: 1, + /** + * Supports tracking splices and resets. + */ + splice: 2, + /** + * Supports tracking splices and resets, while applying some form + * of optimization, such as merging, to the splices. + */ + optimized: 3, +} as const); + +/** + * The available values for SpliceStrategySupport. + * @public + */ +export type SpliceStrategySupport = typeof SpliceStrategySupport[keyof typeof SpliceStrategySupport]; + +/** + * An approach to tracking changes in an array. + * @public + */ +export interface SpliceStrategy { + /** + * The level of feature support the splice strategy provides. + */ + readonly support: SpliceStrategySupport; + + /** + * Normalizes the splices before delivery to array change subscribers. + * @param previous - The previous version of the array if a reset has taken place. + * @param current - The current version of the array. + * @param changes - The set of changes tracked against the array. + */ + normalize( + previous: unknown[] | undefined, + current: unknown[], + changes: Splice[] | undefined + ): readonly Splice[]; + + /** + * Performs and tracks a pop operation on an array. + * @param array - The array to track the change for. + * @param observer - The observer to register the change with. + * @param pop - The operation to perform. + * @param args - The arguments for the operation. + */ + pop( + array: any[], + observer: ArrayObserver, + pop: typeof Array.prototype.pop, + args: any[] + ): any; + + /** + * Performs and tracks a push operation on an array. + * @param array - The array to track the change for. + * @param observer - The observer to register the change with. + * @param push - The operation to perform. + * @param args - The arguments for the operation. + */ + push( + array: any[], + observer: ArrayObserver, + push: typeof Array.prototype.push, + args: any[] + ): any; + + /** + * Performs and tracks a reverse operation on an array. + * @param array - The array to track the change for. + * @param observer - The observer to register the change with. + * @param reverse - The operation to perform. + * @param args - The arguments for the operation. + */ + reverse( + array: any[], + observer: ArrayObserver, + reverse: typeof Array.prototype.reverse, + args: any[] + ): any; + + /** + * Performs and tracks a shift operation on an array. + * @param array - The array to track the change for. + * @param observer - The observer to register the change with. + * @param shift - The operation to perform. + * @param args - The arguments for the operation. + */ + shift( + array: any[], + observer: ArrayObserver, + shift: typeof Array.prototype.shift, + args: any[] + ): any; + + /** + * Performs and tracks a sort operation on an array. + * @param array - The array to track the change for. + * @param observer - The observer to register the change with. + * @param sort - The operation to perform. + * @param args - The arguments for the operation. + */ + sort( + array: any[], + observer: ArrayObserver, + sort: typeof Array.prototype.sort, + args: any[] + ): any[]; + + /** + * Performs and tracks a splice operation on an array. + * @param array - The array to track the change for. + * @param observer - The observer to register the change with. + * @param splice - The operation to perform. + * @param args - The arguments for the operation. + */ + splice( + array: any[], + observer: ArrayObserver, + splice: typeof Array.prototype.splice, + args: any[] + ): any; + + /** + * Performs and tracks an unshift operation on an array. + * @param array - The array to track the change for. + * @param observer - The observer to register the change with. + * @param unshift - The operation to perform. + * @param args - The arguments for the operation. + */ + unshift( + array: any[], + observer: ArrayObserver, + unshift: typeof Array.prototype.unshift, + args: any[] + ): any[]; +} + +const reset = new Splice(0, emptyArray as any, 0); +reset.reset = true; +const resetSplices = [reset]; + +let defaultSpliceStrategy: SpliceStrategy = Object.freeze({ + support: SpliceStrategySupport.splice, + + normalize( + previous: unknown[] | undefined, + current: unknown[], + changes: Splice[] | undefined + ): readonly Splice[] { + return previous === void 0 ? changes ?? emptyArray : resetSplices; + }, + + pop( + array: any[], + observer: ArrayObserver, + pop: typeof Array.prototype.pop, + args: any[] + ) { + const notEmpty = array.length > 0; + const result = pop.apply(array, args); + + if (notEmpty) { + observer.addSplice(new Splice(array.length, [result], 0)); + } + + return result; + }, + + push( + array: any[], + observer: ArrayObserver, + push: typeof Array.prototype.push, + args: any[] + ): any { + const result = push.apply(array, args); + observer.addSplice( + new Splice(array.length - args.length, [], args.length).adjustTo(array) + ); + return result; + }, + + reverse( + array: any[], + observer: ArrayObserver, + reverse: typeof Array.prototype.reverse, + args: any[] + ): any { + const result = reverse.apply(array, args); + observer.reset(array); + return result; + }, + + shift( + array: any[], + observer: ArrayObserver, + shift: typeof Array.prototype.shift, + args: any[] + ): any { + const notEmpty = array.length > 0; + const result = shift.apply(array, args); + + if (notEmpty) { + observer.addSplice(new Splice(0, [result], 0)); + } + + return result; + }, + + sort( + array: any[], + observer: ArrayObserver, + sort: typeof Array.prototype.sort, + args: any[] + ): any[] { + const result = sort.apply(array, args); + observer.reset(array); + return result; + }, + + splice( + array: any[], + observer: ArrayObserver, + splice: typeof Array.prototype.splice, + args: any[] + ): any { + const result = splice.apply(array, args); + observer.addSplice( + new Splice(+args[0], result, args.length > 2 ? args.length - 2 : 0).adjustTo( + array + ) + ); + return result; + }, + + unshift( + array: any[], + observer: ArrayObserver, + unshift: typeof Array.prototype.unshift, + args: any[] + ): any[] { + const result = unshift.apply(array, args); + observer.addSplice(new Splice(0, [], args.length).adjustTo(array)); + return result; + }, +}); + +/** + * Functionality related to tracking changes in arrays. + * @public + */ +export const SpliceStrategy = Object.freeze({ + /** + * A set of changes that represent a full array reset. + */ + reset: resetSplices, + + /** + * Sets the default strategy to use for array observers. + * @param strategy - The splice strategy to use. + */ + setDefaultStrategy(strategy: SpliceStrategy) { + defaultSpliceStrategy = strategy; + }, +} as const); + +function setNonEnumerable(target: any, property: string, value: any): void { + Reflect.defineProperty(target, property, { + value, + enumerable: false, + }); +} + +/** + * Observes array lengths. + * @public + */ +export interface LengthObserver extends Subscriber { + /** + * The length of the observed array. + */ + length: number; +} + +/** + * An observer for arrays. + * @public + */ +export interface ArrayObserver extends SubscriberSet { + /** + * The strategy to use for tracking changes. + */ + strategy: SpliceStrategy | null; + + /** + * The length observer for the array. + */ + readonly lengthObserver: LengthObserver; + + /** + * Adds a splice to the list of changes. + * @param splice - The splice to add. + */ + addSplice(splice: Splice): void; + + /** + * Indicates that a reset change has occurred. + * @param oldCollection - The collection as it was before the reset. + */ + reset(oldCollection: any[] | undefined): void; + + /** + * Flushes the changes to subscribers. + */ + flush(): void; +} + +class DefaultArrayObserver extends SubscriberSet implements ArrayObserver { + private oldCollection: any[] | undefined = void 0; + private splices: Splice[] | undefined = void 0; + private needsQueue: boolean = true; + private _strategy: SpliceStrategy | null = null; + private _lengthObserver: LengthObserver | undefined = void 0; + + public get strategy(): SpliceStrategy | null { + return this._strategy; + } + + public set strategy(value: SpliceStrategy | null) { + this._strategy = value; + } + + public get lengthObserver(): LengthObserver { + let observer = this._lengthObserver; + + if (observer === void 0) { + const array = this.subject; + this._lengthObserver = observer = { + length: array.length, + handleChange() { + if (this.length !== array.length) { + this.length = array.length; + Observable.notify(observer, "length"); + } + }, + }; + + this.subscribe(observer); + } + + return observer; + } + + call: () => void = this.flush; + + constructor(subject: any[]) { + super(subject); + setNonEnumerable(subject, "$fastController", this); + } + + public addSplice(splice: Splice) { + if (this.splices === void 0) { + this.splices = [splice]; + } else { + this.splices.push(splice); + } + + this.enqueue(); + } + + public reset(oldCollection: any[] | undefined): void { + this.oldCollection = oldCollection; + this.enqueue(); + } + + public flush(): void { + const splices = this.splices; + const oldCollection = this.oldCollection; + + if (splices === void 0 && oldCollection === void 0) { + return; + } + + this.needsQueue = true; + this.splices = void 0; + this.oldCollection = void 0; + + this.notify( + (this._strategy ?? defaultSpliceStrategy).normalize( + oldCollection, + this.subject, + splices + ) + ); + } + + private enqueue(): void { + if (this.needsQueue) { + this.needsQueue = false; + Updates.enqueue(this); + } + } +} + +let enabled = false; + +/** + * An observer for arrays. + * @public + */ +export const ArrayObserver = Object.freeze({ + /** + * Enables the array observation mechanism. + * @remarks + * Array observation is enabled automatically when using the + * {@link RepeatDirective}, so calling this API manually is + * not typically necessary. + */ + enable(): void { + if (enabled) { + return; + } + + enabled = true; + + Observable.setArrayObserverFactory( + (collection: any[]): Notifier => new DefaultArrayObserver(collection) + ); + + const proto = Array.prototype; + + if (!(proto as any).$fastPatch) { + setNonEnumerable(proto, "$fastPatch", 1); + + [ + proto.pop, + proto.push, + proto.reverse, + proto.shift, + proto.sort, + proto.splice, + proto.unshift, + ].forEach(method => { + proto[method.name] = function (...args) { + const o = this.$fastController as ArrayObserver; + return o === void 0 + ? method.apply(this, args) + : (o.strategy ?? defaultSpliceStrategy)[method.name]( + this, + o, + method, + args + ); + }; + }); + } + }, +} as const); + +/** + * Enables observing the length of an array. + * @param array - The array to observe the length of. + * @returns The length of the array. + * @public + */ +export function length(array: readonly T[]): number { + if (!array) { + return 0; + } + + let arrayObserver = (array as any).$fastController as ArrayObserver; + if (arrayObserver === void 0) { + ArrayObserver.enable(); + arrayObserver = Observable.getNotifier(array); + } + + Observable.track(arrayObserver.lengthObserver, "length"); + return array.length; +} diff --git a/packages/web-components/fast-element/src/observation/observable.ts b/packages/web-components/fast-element/src/observation/observable.ts index 4c147b681d7..e40954388d3 100644 --- a/packages/web-components/fast-element/src/observation/observable.ts +++ b/packages/web-components/fast-element/src/observation/observable.ts @@ -164,7 +164,6 @@ export const Observable = FAST.getById(KernelServiceId.observable, () => { callback.call(source, oldValue, newValue); } - /* eslint-disable-next-line @typescript-eslint/no-use-before-define */ getNotifier(source).notify(this.name); } } @@ -219,7 +218,6 @@ export const Observable = FAST.getById(KernelServiceId.observable, () => { } } - /** @internal */ public watch(propertySource: unknown, propertyName: string): void { const prev = this.last; const notifier = getNotifier(propertySource); @@ -254,7 +252,6 @@ export const Observable = FAST.getById(KernelServiceId.observable, () => { this.last = current!; } - /** @internal */ handleChange(): void { if (this.needsQueue) { this.needsQueue = false; @@ -262,7 +259,6 @@ export const Observable = FAST.getById(KernelServiceId.observable, () => { } } - /** @internal */ call(): void { if (this.last !== null) { this.needsQueue = true; @@ -365,7 +361,6 @@ export const Observable = FAST.getById(KernelServiceId.observable, () => { initialSubscriber?: Subscriber, isVolatileBinding: boolean = this.isVolatileBinding(binding) ): BindingObserver { - /* eslint-disable-next-line @typescript-eslint/no-use-before-define */ return new BindingObserverImplementation( binding, initialSubscriber, diff --git a/packages/web-components/fast-element/src/observation/array-change-records.ts b/packages/web-components/fast-element/src/observation/splice-strategies.ts similarity index 67% rename from packages/web-components/fast-element/src/observation/array-change-records.ts rename to packages/web-components/fast-element/src/observation/splice-strategies.ts index 33bc6ad55ce..1385f7f31ea 100644 --- a/packages/web-components/fast-element/src/observation/array-change-records.ts +++ b/packages/web-components/fast-element/src/observation/splice-strategies.ts @@ -1,4 +1,10 @@ import { emptyArray } from "../platform.js"; +import { + ArrayObserver, + Splice, + SpliceStrategy, + SpliceStrategySupport, +} from "./arrays.js"; const enum Edit { leave = 0, @@ -164,51 +170,6 @@ function intersect(start1: number, end1: number, start2: number, end2: number): return end1 - start1; // Contained } -/** - * A splice map is a representation of how a previous array of items - * was transformed into a new array of items. Conceptually it is a list of - * tuples of - * - * (index, removed, addedCount) - * - * which are kept in ascending index order of. The tuple represents that at - * the |index|, |removed| sequence of items were removed, and counting forward - * from |index|, |addedCount| items were added. - * @public - */ -export class Splice { - constructor( - /** - * The index that the splice occurs at. - */ - public index: number, - - /** - * The items that were removed. - */ - public removed: any[], - - /** - * The number of items that were added. - */ - public addedCount: number - ) {} - - static normalize( - previous: unknown[] | undefined, - current: unknown[], - changes: Splice[] | undefined - ): Splice[] | undefined { - return previous === void 0 - ? changes!.length > 1 - ? /* eslint-disable-next-line @typescript-eslint/no-use-before-define */ - project(current, changes!) - : changes - : /* eslint-disable-next-line @typescript-eslint/no-use-before-define */ - calc(current, 0, current.length, previous, 0, previous.length); - } -} - /** * @remarks * Lacking individual splice mutation information, the minimal set of @@ -431,3 +392,219 @@ function project(array: unknown[], changes: Splice[]): Splice[] { return splices; } + +/** + * A SpliceStrategy that attempts to merge all splices into the minimal set of + * splices needed to represent the change from the old array to the new array. + * @public + */ +export const mergeSpliceStrategy: SpliceStrategy = Object.freeze({ + support: SpliceStrategySupport.optimized, + + normalize( + previous: unknown[] | undefined, + current: unknown[], + changes: Splice[] | undefined + ): readonly Splice[] { + if (previous === void 0) { + if (changes === void 0) { + return emptyArray; + } + + return changes.length > 1 ? project(current, changes) : changes; + } + + return calc(current, 0, current.length, previous, 0, previous.length); + }, + + pop( + array: any[], + observer: ArrayObserver, + pop: typeof Array.prototype.pop, + args: any[] + ) { + const notEmpty = array.length > 0; + const result = pop.apply(array, args); + + if (notEmpty) { + observer.addSplice(new Splice(array.length, [result], 0)); + } + + return result; + }, + + push( + array: any[], + observer: ArrayObserver, + push: typeof Array.prototype.push, + args: any[] + ): any { + const result = push.apply(array, args); + observer.addSplice( + new Splice(array.length - args.length, [], args.length).adjustTo(array) + ); + return result; + }, + + reverse( + array: any[], + observer: ArrayObserver, + reverse: typeof Array.prototype.reverse, + args: any[] + ): any { + observer.flush(); + const oldArray = array.slice(); + const result = reverse.apply(array, args); + observer.reset(oldArray); + return result; + }, + + shift( + array: any[], + observer: ArrayObserver, + shift: typeof Array.prototype.shift, + args: any[] + ): any { + const notEmpty = array.length > 0; + const result = shift.apply(array, args); + + if (notEmpty) { + observer.addSplice(new Splice(0, [result], 0)); + } + + return result; + }, + + sort( + array: any[], + observer: ArrayObserver, + sort: typeof Array.prototype.sort, + args: any[] + ): any[] { + observer.flush(); + const oldArray = array.slice(); + const result = sort.apply(array, args); + observer.reset(oldArray); + return result; + }, + + splice( + array: any[], + observer: ArrayObserver, + splice: typeof Array.prototype.splice, + args: any[] + ): any { + const result = splice.apply(array, args); + observer.addSplice( + new Splice(+args[0], result, args.length > 2 ? args.length - 2 : 0).adjustTo( + array + ) + ); + return result; + }, + + unshift( + array: any[], + observer: ArrayObserver, + unshift: typeof Array.prototype.unshift, + args: any[] + ): any[] { + const result = unshift.apply(array, args); + observer.addSplice(new Splice(0, [], args.length).adjustTo(array)); + return result; + }, +}); + +/** + * A splice strategy that doesn't create splices, but instead + * tracks every change as a full array reset. + * @public + */ +export const resetSpliceStrategy: SpliceStrategy = Object.freeze({ + support: SpliceStrategySupport.reset, + + normalize( + previous: unknown[] | undefined, + current: unknown[], + changes: Splice[] | undefined + ): readonly Splice[] { + return SpliceStrategy.reset; + }, + + pop( + array: any[], + observer: ArrayObserver, + pop: typeof Array.prototype.pop, + args: any[] + ) { + const result = pop.apply(array, args); + observer.reset(array); + return result; + }, + + push( + array: any[], + observer: ArrayObserver, + push: typeof Array.prototype.push, + args: any[] + ): any { + const result = push.apply(array, args); + observer.reset(array); + return result; + }, + + reverse( + array: any[], + observer: ArrayObserver, + reverse: typeof Array.prototype.reverse, + args: any[] + ): any { + const result = reverse.apply(array, args); + observer.reset(array); + return result; + }, + + shift( + array: any[], + observer: ArrayObserver, + shift: typeof Array.prototype.shift, + args: any[] + ): any { + const result = shift.apply(array, args); + observer.reset(array); + return result; + }, + + sort( + array: any[], + observer: ArrayObserver, + sort: typeof Array.prototype.sort, + args: any[] + ): any[] { + const result = sort.apply(array, args); + observer.reset(array); + return result; + }, + + splice( + array: any[], + observer: ArrayObserver, + splice: typeof Array.prototype.splice, + args: any[] + ): any { + const result = splice.apply(array, args); + observer.reset(array); + return result; + }, + + unshift( + array: any[], + observer: ArrayObserver, + unshift: typeof Array.prototype.unshift, + args: any[] + ): any[] { + const result = unshift.apply(array, args); + observer.reset(array); + return result; + }, +}); diff --git a/packages/web-components/fast-element/src/templating/binding.ts b/packages/web-components/fast-element/src/templating/binding.ts index 824d1cdca43..94c2406ea56 100644 --- a/packages/web-components/fast-element/src/templating/binding.ts +++ b/packages/web-components/fast-element/src/templating/binding.ts @@ -12,7 +12,6 @@ import { AddViewBehaviorFactory, Aspect, Aspected, - AspectType, HTMLDirective, ViewBehavior, ViewBehaviorFactory, @@ -40,7 +39,7 @@ const createInnerHTMLBinding = globalThis.TrustedHTML * @public */ export type BindingMode = Record< - AspectType, + Aspect, (directive: HTMLBindingDirective) => Pick >; @@ -792,7 +791,7 @@ export class HTMLBindingDirective /** * The type of aspect to target. */ - aspectType: AspectType = Aspect.content; + aspectType: Aspect = Aspect.content; /** * Creates an instance of HTMLBindingDirective. diff --git a/packages/web-components/fast-element/src/templating/html-directive.ts b/packages/web-components/fast-element/src/templating/html-directive.ts index 2e1711f897b..0fd034a3b3a 100644 --- a/packages/web-components/fast-element/src/templating/html-directive.ts +++ b/packages/web-components/fast-element/src/templating/html-directive.ts @@ -241,20 +241,13 @@ export const Aspect = Object.freeze({ break; } }, -}); +} as const); /** - * Valid aspect type values. + * The type of HTML aspect to target. * @public */ -export type AspectType = Exclude< - { - [K in keyof typeof Aspect]: typeof Aspect[K] extends number - ? typeof Aspect[K] - : never; - }[keyof typeof Aspect], - typeof Aspect.none ->; +export type Aspect = typeof Aspect[Exclude]; /** * Represents something that applies to a specific aspect of the DOM. @@ -274,7 +267,7 @@ export interface Aspected { /** * The type of aspect to target. */ - aspectType: AspectType; + aspectType: Aspect; /** * A binding if one is associated with the aspect. diff --git a/packages/web-components/fast-element/src/templating/repeat.ts b/packages/web-components/fast-element/src/templating/repeat.ts index 63b654daf61..77b10c0d225 100644 --- a/packages/web-components/fast-element/src/templating/repeat.ts +++ b/packages/web-components/fast-element/src/templating/repeat.ts @@ -1,6 +1,4 @@ import { isFunction } from "../interfaces.js"; -import type { Splice } from "../observation/array-change-records.js"; -import { enableArrayObservation } from "../observation/array-observer.js"; import type { Behavior } from "../observation/behavior.js"; import type { Notifier, Subscriber } from "../observation/notifier.js"; import { @@ -13,6 +11,7 @@ import { RootContext, } from "../observation/observable.js"; import { emptyArray } from "../platform.js"; +import { ArrayObserver, Splice } from "../observation/arrays.js"; import { Markup } from "./markup.js"; import { AddViewBehaviorFactory, @@ -162,6 +161,8 @@ export class RepeatBehavior implements Behavior, Subscriber { this.context! ); this.refreshAllViews(true); + } else if (args[0].reset) { + this.refreshAllViews(); } else { this.updateViews(args); } @@ -334,7 +335,7 @@ export class RepeatDirective public readonly templateBinding: Binding, public readonly options: RepeatOptions ) { - enableArrayObservation(); + ArrayObserver.enable(); this.isItemsBindingVolatile = Observable.isVolatileBinding(itemsBinding); this.isTemplateBindingVolatile = Observable.isVolatileBinding(templateBinding); }