From 4b5aae2a74047a51aac3e1fbb8fb845f5fd78957 Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Tue, 12 Oct 2021 15:33:02 -0400 Subject: [PATCH 001/135] feat(compiler): remove tree walker & switch to structural target lookup --- .../fast-element/docs/api-report.md | 56 ++-- .../web-components/fast-element/src/dom.ts | 13 - .../src/templating/binding.spec.ts | 5 +- .../fast-element/src/templating/binding.ts | 20 +- .../src/templating/children.spec.ts | 32 +- .../fast-element/src/templating/children.ts | 14 +- .../src/templating/compiler.spec.ts | 94 +++--- .../fast-element/src/templating/compiler.ts | 305 +++++++++++++----- .../src/templating/html-directive.ts | 44 ++- .../fast-element/src/templating/ref.ts | 17 +- .../src/templating/repeat.spec.ts | 83 +++-- .../fast-element/src/templating/repeat.ts | 25 +- .../src/templating/slotted.spec.ts | 29 +- .../fast-element/src/templating/slotted.ts | 15 +- .../src/templating/template.spec.ts | 3 +- .../fast-element/src/templating/template.ts | 66 ++-- 16 files changed, 493 insertions(+), 328 deletions(-) diff --git a/packages/web-components/fast-element/docs/api-report.md b/packages/web-components/fast-element/docs/api-report.md index 8417389c9ce..3a878652f8e 100644 --- a/packages/web-components/fast-element/docs/api-report.md +++ b/packages/web-components/fast-element/docs/api-report.md @@ -17,12 +17,12 @@ export interface Accessor { // @public export class AttachedBehaviorHTMLDirective extends HTMLDirective { constructor(name: string, behavior: AttachedBehaviorType, options: T); - createBehavior(target: Node): Behavior; + createBehavior(targets: BehaviorTargets): Behavior; createPlaceholder(index: number): string; } // @public -export type AttachedBehaviorType = new (target: any, options: T) => Behavior; +export type AttachedBehaviorType = new (targets: BehaviorTargets, targetId: string, options: T) => Behavior; // @public export function attr(config?: DecoratorAttributeConfiguration): (target: {}, property: string) => void; @@ -63,6 +63,11 @@ export interface Behavior { unbind(source: unknown): void; } +// @public +export type BehaviorTargets = { + [id: string]: Node; +}; + // @public export type Binding = (source: TSource, context: ExecutionContext) => TReturn; @@ -131,7 +136,7 @@ export function children(propertyOrOptions: (keyof T & string) | Childr // // @public export class ChildrenBehavior extends NodeObservationBehavior { - constructor(target: HTMLElement, options: ChildrenBehaviorOptions); + constructor(targets: BehaviorTargets, targetId: string, options: ChildrenBehaviorOptions); disconnect(): void; protected getNodes(): ChildNode[]; observe(): void; @@ -140,18 +145,8 @@ export class ChildrenBehavior extends NodeObservationBehavior = ChildListBehaviorOptions | SubtreeBehaviorOptions; -// @beta -export interface CompilationResult { - fragment: DocumentFragment; - hostBehaviorFactories: NodeBehaviorFactory[]; - targetOffset: number; - viewBehaviorFactories: NodeBehaviorFactory[]; -} - -// Warning: (ae-incompatible-release-tags) The symbol "compileTemplate" is marked as @public, but its signature references "CompilationResult" which is marked as @beta -// // @public -export function compileTemplate(template: HTMLTemplateElement, directives: ReadonlyArray): CompilationResult; +export function compileTemplate(template: HTMLTemplateElement, directives: ReadonlyArray): HTMLTemplateCompilationResult; // @public export type ComposableStyles = string | ElementStyles | CSSStyleSheet; @@ -221,7 +216,6 @@ export const DOM: Readonly<{ setAttribute(element: HTMLElement, attributeName: string, value: any): void; setBooleanAttribute(element: HTMLElement, attributeName: string, value: boolean): void; removeChildNodes(parent: Node): void; - createTemplateWalker(fragment: DocumentFragment): TreeWalker; }>; // @public @@ -348,7 +342,7 @@ export class HTMLBindingDirective extends TargetedHTMLDirective { constructor(binding: Binding); // (undocumented) binding: Binding; - createBehavior(target: Node): BindingBehavior; + createBehavior(targets: BehaviorTargets): BindingBehavior; targetAtContent(): void; get targetName(): string | undefined; set targetName(value: string | undefined); @@ -356,9 +350,25 @@ export class HTMLBindingDirective extends TargetedHTMLDirective { // @public export abstract class HTMLDirective implements NodeBehaviorFactory { - abstract createBehavior(target: Node): Behavior; + abstract createBehavior(targets: BehaviorTargets): Behavior; abstract createPlaceholder(index: number): string; - targetIndex: number; + targetId: string; +} + +// @public +export class HTMLTemplateCompilationResult { + constructor(fragment: DocumentFragment, viewBehaviorFactories: NodeBehaviorFactory[], hostBehaviorFactories: NodeBehaviorFactory[], targetIds: string[]); + // (undocumented) + get behaviorCount(): number; + createTargets(root: Node, host?: Node): BehaviorTargets; + // (undocumented) + readonly fragment: DocumentFragment; + // (undocumented) + get hasHostBehaviors(): boolean; + // (undocumented) + readonly hostBehaviorFactories: NodeBehaviorFactory[]; + // (undocumented) + readonly viewBehaviorFactories: NodeBehaviorFactory[]; } // @public @@ -400,8 +410,8 @@ export type Mutable = { // @public export interface NodeBehaviorFactory { - createBehavior(target: Node): Behavior; - targetIndex: number; + createBehavior(targets: BehaviorTargets): Behavior; + targetId: string; } // @public @@ -467,7 +477,7 @@ export function ref(propertyName: keyof T & string): CaptureType; // @public export class RefBehavior implements Behavior { - constructor(target: HTMLElement, propertyName: string); + constructor(targets: BehaviorTargets, targetId: string, propertyName: string); bind(source: any): void; unbind(): void; } @@ -487,7 +497,7 @@ export class RepeatBehavior implements Behavior, Subscriber { // @public export class RepeatDirective extends HTMLDirective { constructor(itemsBinding: Binding, templateBinding: Binding, options: RepeatOptions); - createBehavior(target: Node): RepeatBehavior; + createBehavior(targets: BehaviorTargets): RepeatBehavior; createPlaceholder: (index: number) => string; } @@ -502,7 +512,7 @@ export function slotted(propertyOrOptions: (keyof T & string) | Slotted // @public export class SlottedBehavior extends NodeObservationBehavior { - constructor(target: HTMLSlotElement, options: SlottedBehaviorOptions); + constructor(targets: BehaviorTargets, targetId: string, options: SlottedBehaviorOptions); disconnect(): void; protected getNodes(): Node[]; observe(): void; diff --git a/packages/web-components/fast-element/src/dom.ts b/packages/web-components/fast-element/src/dom.ts index a004bafdd68..b458cdbf5b4 100644 --- a/packages/web-components/fast-element/src/dom.ts +++ b/packages/web-components/fast-element/src/dom.ts @@ -234,17 +234,4 @@ export const DOM = Object.freeze({ parent.removeChild(child); } }, - - /** - * Creates a TreeWalker configured to walk a template fragment. - * @param fragment - The fragment to walk. - */ - createTemplateWalker(fragment: DocumentFragment): TreeWalker { - return document.createTreeWalker( - fragment, - 133, // element, text, comment - null, - false - ); - }, }); diff --git a/packages/web-components/fast-element/src/templating/binding.spec.ts b/packages/web-components/fast-element/src/templating/binding.spec.ts index 6e7e3c56a91..5d381d67777 100644 --- a/packages/web-components/fast-element/src/templating/binding.spec.ts +++ b/packages/web-components/fast-element/src/templating/binding.spec.ts @@ -29,9 +29,12 @@ describe("The HTML binding directive", () => { function contentBinding(propertyName: keyof Model = "value") { const directive = new HTMLBindingDirective(x => x[propertyName]); directive.targetAtContent(); + directive.targetId = 'r'; const node = document.createTextNode(" "); - const behavior = directive.createBehavior(node); + const targets = { r: node }; + + const behavior = directive.createBehavior(targets); const parentNode = document.createElement("div"); parentNode.appendChild(node); diff --git a/packages/web-components/fast-element/src/templating/binding.ts b/packages/web-components/fast-element/src/templating/binding.ts index 90726419e8f..a2176d51720 100644 --- a/packages/web-components/fast-element/src/templating/binding.ts +++ b/packages/web-components/fast-element/src/templating/binding.ts @@ -1,13 +1,15 @@ -import { DOM } from "../dom.js"; -import type { Behavior } from "../observation/behavior.js"; +import type { BehaviorTargets } from ".."; +import { DOM } from "../dom"; +import type { Behavior } from "../observation/behavior"; import { Binding, BindingObserver, ExecutionContext, Observable, -} from "../observation/observable.js"; -import { TargetedHTMLDirective } from "./html-directive.js"; -import type { SyntheticView } from "./view.js"; + setCurrentEvent, +} from "../observation/observable"; +import { TargetedHTMLDirective } from "./html-directive"; +import type { SyntheticView } from "./view"; function normalBind( this: BindingBehavior, @@ -261,10 +263,10 @@ export class HTMLBindingDirective extends TargetedHTMLDirective { * information stored in the BindingDirective. * @param target - The target node that the binding behavior should attach to. */ - createBehavior(target: Node): BindingBehavior { + createBehavior(targets: BehaviorTargets): BindingBehavior { /* eslint-disable-next-line @typescript-eslint/no-use-before-define */ return new BindingBehavior( - target, + targets[this.targetId], this.binding, this.isBindingVolatile, this.bind, @@ -359,9 +361,9 @@ export class BindingBehavior implements Behavior { /** @internal */ public handleEvent(event: Event): void { - ExecutionContext.setEvent(event); + setCurrentEvent(event); const result = this.binding(this.source, this.context!); - ExecutionContext.setEvent(null); + setCurrentEvent(null); if (result !== true) { event.preventDefault(); diff --git a/packages/web-components/fast-element/src/templating/children.spec.ts b/packages/web-components/fast-element/src/templating/children.spec.ts index d513640ccd4..0689e7f7f5e 100644 --- a/packages/web-components/fast-element/src/templating/children.spec.ts +++ b/packages/web-components/fast-element/src/templating/children.spec.ts @@ -15,9 +15,11 @@ describe("The children", () => { context("directive", () => { it("creates a ChildrenBehavior", () => { + const targetId = 'r'; const directive = children("test") as AttachedBehaviorHTMLDirective; const target = document.createElement("div"); - const behavior = directive.createBehavior(target); + const targets = { [targetId]: target }; + const behavior = directive.createBehavior(targets); expect(behavior).to.be.instanceOf(ChildrenBehavior); }); @@ -43,13 +45,15 @@ describe("The children", () => { function createDOM(elementName: string = "div") { const host = document.createElement("div"); const children = createAndAppendChildren(host, elementName); + const targetId = 'r'; + const targets = { [targetId]: host }; - return { host, children }; + return { host, children, targets, targetId }; } it("gathers child nodes", () => { - const { host, children } = createDOM(); - const behavior = new ChildrenBehavior(host, { + const { host, children, targets, targetId } = createDOM(); + const behavior = new ChildrenBehavior(targets, targetId, { property: "nodes", }); const model = new Model(); @@ -60,8 +64,8 @@ describe("The children", () => { }); it("gathers child nodes with a filter", () => { - const { host, children } = createDOM("foo-bar"); - const behavior = new ChildrenBehavior(host, { + const { host, children, targets, targetId } = createDOM("foo-bar"); + const behavior = new ChildrenBehavior(targets, targetId, { property: "nodes", filter: elements("foo-bar"), }); @@ -73,8 +77,8 @@ describe("The children", () => { }); it("updates child nodes when they change", async () => { - const { host, children } = createDOM("foo-bar"); - const behavior = new ChildrenBehavior(host, { + const { host, children, targets, targetId } = createDOM("foo-bar"); + const behavior = new ChildrenBehavior(targets, targetId, { property: "nodes", }); const model = new Model(); @@ -91,8 +95,8 @@ describe("The children", () => { }); it("updates child nodes when they change with a filter", async () => { - const { host, children } = createDOM("foo-bar"); - const behavior = new ChildrenBehavior(host, { + const { host, children, targets, targetId } = createDOM("foo-bar"); + const behavior = new ChildrenBehavior(targets, targetId, { property: "nodes", filter: elements("foo-bar"), }); @@ -110,7 +114,7 @@ describe("The children", () => { }); it("updates subtree nodes when they change with a selector", async () => { - const { host, children } = createDOM("foo-bar"); + const { host, children, targets, targetId } = createDOM("foo-bar"); const subtreeElement = "foo-bar-baz"; const subtreeChildren: HTMLElement[] = []; @@ -122,7 +126,7 @@ describe("The children", () => { } } - const behavior = new ChildrenBehavior(host, { + const behavior = new ChildrenBehavior(targets, targetId, { property: "nodes", subtree: true, selector: subtreeElement, @@ -150,8 +154,8 @@ describe("The children", () => { }); it("clears and unwatches when unbound", async () => { - const { host, children } = createDOM("foo-bar"); - const behavior = new ChildrenBehavior(host, { + const { host, children, targets, targetId } = createDOM("foo-bar"); + const behavior = new ChildrenBehavior(targets, targetId, { property: "nodes", }); const model = new Model(); diff --git a/packages/web-components/fast-element/src/templating/children.ts b/packages/web-components/fast-element/src/templating/children.ts index 649841ae90e..00f872378f5 100644 --- a/packages/web-components/fast-element/src/templating/children.ts +++ b/packages/web-components/fast-element/src/templating/children.ts @@ -1,6 +1,6 @@ -import { AttachedBehaviorHTMLDirective } from "./html-directive.js"; -import { NodeBehaviorOptions, NodeObservationBehavior } from "./node-observation.js"; -import type { CaptureType } from "./template.js"; +import { AttachedBehaviorHTMLDirective, BehaviorTargets } from "./html-directive"; +import { NodeBehaviorOptions, NodeObservationBehavior } from "./node-observation"; +import type { CaptureType } from "./template"; /** * The options used to configure child list observation. @@ -49,8 +49,12 @@ export class ChildrenBehavior extends NodeObservationBehavior { html: `${inline(0)}`, directives: [binding()], fragment: ` `, - targetIndexes: [0], + targetIds: ['r.1'], childCount: 2, }, { @@ -49,88 +49,88 @@ describe("The template compiler", () => { html: `${inline(0)} end`, directives: [binding()], fragment: ` end`, - targetIndexes: [0], - childCount: 2, + targetIds: ['r.1'], + childCount: 3, }, { type: "a single middle", html: `beginning ${inline(0)} end`, directives: [binding()], fragment: `beginning end`, - targetIndexes: [1], - childCount: 3, + targetIds: ['r.2'], + childCount: 4, }, { type: "a single ending", html: `${inline(0)} end`, directives: [binding()], fragment: ` end`, - targetIndexes: [0], - childCount: 2, + targetIds: ['r.1'], + childCount: 3, }, { type: "back-to-back", html: `${inline(0)}${inline(1)}`, directives: [binding(), binding()], fragment: ` `, - targetIndexes: [0, 1], - childCount: 2, + targetIds: ['r.1', 'r.2'], + childCount: 3, }, { type: "back-to-back starting", html: `${inline(0)}${inline(1)} end`, directives: [binding(), binding()], fragment: ` end`, - targetIndexes: [0, 1], - childCount: 3, + targetIds: ['r.1', 'r.2'], + childCount: 4, }, { type: "back-to-back middle", html: `beginning ${inline(0)}${inline(1)} end`, directives: [binding(), binding()], fragment: `beginning end`, - targetIndexes: [1, 2], - childCount: 4, + targetIds: ['r.2', 'r.3'], + childCount: 5, }, { type: "back-to-back ending", html: `start ${inline(0)}${inline(1)}`, directives: [binding(), binding()], fragment: `start `, - targetIndexes: [1, 2], - childCount: 3, + targetIds: ['r.2', 'r.3'], + childCount: 4, }, { type: "separated", html: `${inline(0)}separator${inline(1)}`, directives: [binding(), binding()], fragment: ` separator `, - targetIndexes: [0, 2], - childCount: 3, + targetIds: ['r.1', 'r.3'], + childCount: 4, }, { type: "separated starting", html: `${inline(0)}separator${inline(1)} end`, directives: [binding(), binding()], fragment: ` separator end`, - targetIndexes: [0, 2], - childCount: 4, + targetIds: ['r.1', 'r.3'], + childCount: 5, }, { type: "separated middle", html: `beginning ${inline(0)}separator${inline(1)} end`, directives: [binding(), binding()], fragment: `beginning separator end`, - targetIndexes: [1, 3], - childCount: 5, + targetIds: ['r.2', 'r.4'], + childCount: 6, }, { type: "separated ending", html: `beginning ${inline(0)}separator${inline(1)}`, directives: [binding(), binding()], fragment: `beginning separator `, - targetIndexes: [1, 3], - childCount: 4, + targetIds: ['r.2', 'r.4'], + childCount: 5, }, { type: "mixed content", @@ -139,7 +139,7 @@ describe("The template compiler", () => { )} ${inline(3)} end`, directives: [binding(), binding(), binding(), binding()], fragment: "
start end
end", - targetIndexes: [2, 4, 5, 7], + targetIds: ['r.0.1', 'r.1', 'r.1.0', 'r.3'], childCount: 5, }, ]; @@ -164,12 +164,12 @@ describe("The template compiler", () => { expect(length).to.equal(x.directives.length); - if (x.targetIndexes) { - expect(length).to.equal(x.targetIndexes.length); + if (x.targetIds) { + expect(length).to.equal(x.targetIds.length); for (let i = 0; i < length; ++i) { - expect(viewBehaviorFactories[i].targetIndex).to.equal( - x.targetIndexes[i] + expect(viewBehaviorFactories[i].targetId).to.equal( + x.targetIds[i] ); } } @@ -191,7 +191,7 @@ describe("The template compiler", () => { directives: [binding()], fragment: `Link`, result: "result", - targetIndexes: [0], + targetIds: ['r.1'], }, { type: "a single starting", @@ -199,7 +199,7 @@ describe("The template compiler", () => { directives: [binding()], fragment: `Link`, result: "result end", - targetIndexes: [0], + targetIds: ['r.1'], }, { type: "a single middle", @@ -207,7 +207,7 @@ describe("The template compiler", () => { directives: [binding()], fragment: `Link`, result: "beginning result end", - targetIndexes: [0], + targetIds: ['r.1'], }, { type: "a single ending", @@ -215,7 +215,7 @@ describe("The template compiler", () => { directives: [binding()], fragment: `Link`, result: "result end", - targetIndexes: [0], + targetIds: ['r.1'], }, { type: "back-to-back", @@ -223,7 +223,7 @@ describe("The template compiler", () => { directives: [binding(), binding()], fragment: `Link`, result: "resultresult", - targetIndexes: [0], + targetIds: ['r.1'], }, { type: "back-to-back starting", @@ -231,7 +231,7 @@ describe("The template compiler", () => { directives: [binding(), binding()], fragment: `Link`, result: "resultresult end", - targetIndexes: [0], + targetIds: ['r.1'], }, { type: "back-to-back middle", @@ -239,7 +239,7 @@ describe("The template compiler", () => { directives: [binding(), binding()], fragment: `Link`, result: "beginning resultresult end", - targetIndexes: [0], + targetIds: ['r.1'], }, { type: "back-to-back ending", @@ -247,7 +247,7 @@ describe("The template compiler", () => { directives: [binding(), binding()], fragment: `Link`, result: "start resultresult", - targetIndexes: [0], + targetIds: ['r.1'], }, { type: "separated", @@ -255,7 +255,7 @@ describe("The template compiler", () => { directives: [binding(), binding()], fragment: `Link`, result: "resultseparatorresult", - targetIndexes: [0], + targetIds: ['r.1'], }, { type: "separated starting", @@ -263,7 +263,7 @@ describe("The template compiler", () => { directives: [binding(), binding()], fragment: `Link`, result: "resultseparatorresult end", - targetIndexes: [0], + targetIds: ['r.1'], }, { type: "separated middle", @@ -273,7 +273,7 @@ describe("The template compiler", () => { directives: [binding(), binding()], fragment: `Link`, result: "beginning resultseparatorresult end", - targetIndexes: [0], + targetIds: ['r.1'], }, { type: "separated ending", @@ -281,21 +281,21 @@ describe("The template compiler", () => { directives: [binding(), binding()], fragment: `Link`, result: "beginning resultseparatorresult", - targetIndexes: [0], + targetIds: ['r.1'], }, { type: "multiple attributes on the same element with", html: `Link`, directives: [binding(), binding()], fragment: `Link`, - targetIndexes: [0, 0], + targetIds: ['r.1', 'r.1'], }, { type: "attributes on different elements with", html: `LinkLink`, directives: [binding(), binding()], fragment: `LinkLink`, - targetIndexes: [0, 2], + targetIds: ['r.0', 'r.1'], }, { type: "multiple attributes on different elements with", @@ -308,7 +308,7 @@ describe("The template compiler", () => { Link Link `, - targetIndexes: [1, 1, 4, 4], + targetIds: ['r.1', 'r.1', 'r.3', 'r.3'], }, ]; @@ -330,14 +330,14 @@ describe("The template compiler", () => { ).to.equal(x.result); } - if (x.targetIndexes) { + if (x.targetIds) { const length = viewBehaviorFactories.length; - expect(length).to.equal(x.targetIndexes.length); + expect(length).to.equal(x.targetIds.length); for (let i = 0; i < length; ++i) { - expect(viewBehaviorFactories[i].targetIndex).to.equal( - x.targetIndexes[i] + expect(viewBehaviorFactories[i].targetId).to.equal( + x.targetIds[i] ); } } diff --git a/packages/web-components/fast-element/src/templating/compiler.ts b/packages/web-components/fast-element/src/templating/compiler.ts index 090b2e8e8b5..2f3fd01db41 100644 --- a/packages/web-components/fast-element/src/templating/compiler.ts +++ b/packages/web-components/fast-element/src/templating/compiler.ts @@ -1,7 +1,8 @@ -import { _interpolationEnd, _interpolationStart, DOM } from "../dom.js"; -import type { Binding, ExecutionContext } from "../observation/observable.js"; -import { HTMLBindingDirective } from "./binding.js"; -import type { HTMLDirective, NodeBehaviorFactory } from "./html-directive.js"; +import type { BehaviorTargets } from ".."; +import { _interpolationEnd, _interpolationStart, DOM } from "../dom"; +import type { Binding, ExecutionContext } from "../observation/observable"; +import { HTMLBindingDirective } from "./binding"; +import type { HTMLDirective, NodeBehaviorFactory } from "./html-directive"; type InlineDirective = HTMLDirective & { targetName?: string; @@ -9,26 +10,86 @@ type InlineDirective = HTMLDirective & { targetAtContent(): void; }; +const descriptors: PropertyDescriptorMap = {}; + +function addTargetDescriptor(parentId: string, targetId: string, targetIndex: number) { + if ( + targetId === "r" || // root + targetId === "h" || // host + descriptors[targetId] + ) { + return; + } + + if (!descriptors[parentId]) { + const index = parentId.lastIndexOf("."); + + if (index !== -1) { + const grandparentId = parentId.substr(0, index); + const childIndex = parseInt(parentId.substr(index)); + addTargetDescriptor(grandparentId, parentId, childIndex); + } + } + + descriptors[targetId] = createTargetDescriptor(parentId, targetId, targetIndex); +} + +function createTargetDescriptor( + parentId: string, + targetId: string, + targetIndex: number +): PropertyDescriptor { + const field = `_${targetId}`; + + return { + configurable: false, + get: function () { + return this[field] || (this[field] = this[parentId].childNodes[targetIndex]); + }, + }; +} + let sharedContext: CompilationContext | null = null; +// used to prevent creating lots of objects just to track node and index while compiling +const next = { + index: 0, + node: null as ChildNode | null, +}; + class CompilationContext { - public targetIndex!: number; public behaviorFactories!: NodeBehaviorFactory[]; public directives: ReadonlyArray; + public targetIds!: string[]; + + public addFactory( + factory: NodeBehaviorFactory, + parentId: string, + targetId: string, + targetIndex: number + ): void { + if (this.targetIds.indexOf(targetId) === -1) { + this.targetIds.push(targetId); + } - public addFactory(factory: NodeBehaviorFactory): void { - factory.targetIndex = this.targetIndex; + addTargetDescriptor(parentId, targetId, targetIndex); + factory.targetId = targetId; this.behaviorFactories.push(factory); } - public captureContentBinding(directive: HTMLBindingDirective): void { + public captureContentBinding( + directive: HTMLBindingDirective, + parentId: string, + targetId: string, + targetIndex: number + ): void { directive.targetAtContent(); - this.addFactory(directive); + this.addFactory(directive, parentId, targetId, targetIndex); } public reset(): void { this.behaviorFactories = []; - this.targetIndex = -1; + this.targetIds = []; } public release(): void { @@ -49,7 +110,7 @@ function createAggregateBinding( parts: (string | InlineDirective)[] ): HTMLBindingDirective { if (parts.length === 1) { - return parts[0] as HTMLBindingDirective; + return (parts[0] as any) as HTMLBindingDirective; } let targetName: string | undefined; @@ -115,7 +176,10 @@ function parseContent( function compileAttributes( context: CompilationContext, + parentId: string, node: HTMLElement, + nodeId: string, + nodeIndex: number, includeBasicValues: boolean = false ): void { const attributes = node.attributes; @@ -139,7 +203,7 @@ function compileAttributes( node.removeAttributeNode(attr); i--; ii--; - context.addFactory(result); + context.addFactory(result, parentId, nodeId, nodeIndex); } } } @@ -147,64 +211,146 @@ function compileAttributes( function compileContent( context: CompilationContext, node: Text, - walker: TreeWalker -): void { + parentId, + nodeId, + nodeIndex +) { const parseResult = parseContent(context, node.textContent!); + if (parseResult === null) { + next.node = node.nextSibling; + next.index = nodeIndex + 1; + return next; + } - if (parseResult !== null) { - let lastNode = node; - for (let i = 0, ii = parseResult.length; i < ii; ++i) { - const currentPart = parseResult[i]; - const currentNode = - i === 0 - ? node - : lastNode.parentNode!.insertBefore( - document.createTextNode(""), - lastNode.nextSibling - ); - - if (typeof currentPart === "string") { - currentNode.textContent = currentPart; - } else { - currentNode.textContent = " "; - context.captureContentBinding(currentPart as HTMLBindingDirective); - } + let lastNode = node; + for (let i = 0, ii = parseResult.length; i < ii; ++i) { + const currentPart = parseResult[i]; + let currentNode: Text; - lastNode = currentNode; - context.targetIndex++; + if (i === 0) { + currentNode = node; + } else { + nodeIndex++; + nodeId = `${parentId}.${nodeIndex}`; + currentNode = lastNode.parentNode!.insertBefore( + document.createTextNode(""), + lastNode.nextSibling + ); + } - if (currentNode !== node) { - walker.nextNode(); - } + if (typeof currentPart === "string") { + currentNode.textContent = currentPart; + } else { + currentNode.textContent = " "; + context.captureContentBinding( + currentPart as HTMLBindingDirective, + parentId, + nodeId, + nodeIndex + ); } - context.targetIndex--; + lastNode = currentNode; } + + next.index = nodeIndex + 1; + next.node = lastNode.nextSibling; + return next; +} + +function compileNode( + context: CompilationContext, + parentId: string, + node: Node, + nodeIndex: number +) { + const nodeId = `${parentId}.${nodeIndex}`; + + switch (node.nodeType) { + case 1: // element node + compileAttributes(context, parentId, node as HTMLElement, nodeId, nodeIndex); + + let child = node.firstChild; + let childIndex = 0; + + while (child) { + const result = compileNode(context, nodeId, child, childIndex); + child = result.node; + childIndex = result.index; + } + + break; + case 3: // text node + return compileContent(context, node as Text, parentId, nodeId, nodeIndex); + case 8: // comment + if (DOM.isMarker(node)) { + context.addFactory( + context.directives[DOM.extractDirectiveIndexFromMarker(node)], + parentId, + nodeId, + nodeIndex + ); + } + break; + } + + next.index = nodeIndex + 1; + next.node = node.nextSibling; + return next; } /** * The result of compiling a template and its directives. - * @beta + * @public */ -export interface CompilationResult { - /** - * A cloneable DocumentFragment representing the compiled HTML. - */ - fragment: DocumentFragment; - /** - * The behaviors that should be applied to the template's HTML. - */ - viewBehaviorFactories: NodeBehaviorFactory[]; +export class HTMLTemplateCompilationResult { + private proto: any; + /** - * The behaviors that should be applied to the host element that + * + * @param fragment - A cloneable DocumentFragment representing the compiled HTML. + * @param viewBehaviorFactories - The behaviors that should be applied to the template's HTML. + * @param hostBehaviorFactories - The behaviors that should be applied to the host element that * the template is rendered into. + * @param targetIds - The structural ids used by the behavior factories. */ - hostBehaviorFactories: NodeBehaviorFactory[]; + public constructor( + public readonly fragment: DocumentFragment, + public readonly viewBehaviorFactories: NodeBehaviorFactory[], + public readonly hostBehaviorFactories: NodeBehaviorFactory[], + private targetIds: string[] + ) { + this.proto = Object.create(null, descriptors); + } + + get hasHostBehaviors() { + return this.hostBehaviorFactories.length > 0; + } + + get behaviorCount() { + return this.viewBehaviorFactories.length + this.hostBehaviorFactories.length; + } + /** - * An index offset to apply to BehaviorFactory target indexes when - * matching factories to targets. + * Creates a behavior target lookup object. + * @param host - The host element. + * @param root - The root element. + * @returns A lookup object for behavior targets. */ - targetOffset: number; + public createTargets(root: Node, host?: Node): BehaviorTargets { + const targets = Object.create(this.proto, { + r: { value: root }, + h: { value: host || root }, + }); + + const ids = this.targetIds; + + for (let i = 0, ii = ids.length; i < ii; ++i) { + targets[ids[i]]; // trigger locators + } + + return targets; + } } /** @@ -222,63 +368,50 @@ export interface CompilationResult { export function compileTemplate( template: HTMLTemplateElement, directives: ReadonlyArray -): CompilationResult { +): HTMLTemplateCompilationResult { const fragment = template.content; // https://bugs.chromium.org/p/chromium/issues/detail?id=1111864 document.adoptNode(fragment); const context = CompilationContext.borrow(directives); - compileAttributes(context, template, true); + compileAttributes(context, "", template, /* host */ "h", 0, true); const hostBehaviorFactories = context.behaviorFactories; context.reset(); - const walker = DOM.createTemplateWalker(fragment); - - let node: Node | null; - - while ((node = walker.nextNode())) { - context.targetIndex++; - - switch (node.nodeType) { - case 1: // element node - compileAttributes(context, node as HTMLElement); - break; - case 3: // text node - compileContent(context, node as Text, walker); - break; - case 8: // comment - if (DOM.isMarker(node)) { - context.addFactory( - directives[DOM.extractDirectiveIndexFromMarker(node)] - ); - } - } - } - - let targetOffset = 0; - if ( // If the first node in a fragment is a marker, that means it's an unstable first node, // because something like a when, repeat, etc. could add nodes before the marker. // To mitigate this, we insert a stable first node. However, if we insert a node, // that will alter the result of the TreeWalker. So, we also need to offset the target index. DOM.isMarker(fragment.firstChild!) || - // Or if there is only one node and a directive, it means the template's content + // Or if there is only one node, it means the template's content // is *only* the directive. In that case, HTMLView.dispose() misses any nodes inserted by // the directive. Inserting a new node ensures proper disposal of nodes added by the directive. - (fragment.childNodes.length === 1 && directives.length) + fragment.childNodes.length === 1 ) { fragment.insertBefore(document.createComment(""), fragment.firstChild); - targetOffset = -1; } + let node = fragment.firstChild; + let nodeIndex = 0; + const parentId = "r"; //root + + while (node) { + const result = compileNode(context, parentId, node, nodeIndex); + node = result.node; + nodeIndex = result.index; + } + + next.node = null; // prevent leaks + const viewBehaviorFactories = context.behaviorFactories; + const targetIds = context.targetIds; context.release(); - return { + return new HTMLTemplateCompilationResult( fragment, viewBehaviorFactories, hostBehaviorFactories, - targetOffset, - }; + targetIds + ); } 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 30f69b5a50b..9ac9aae3314 100644 --- a/packages/web-components/fast-element/src/templating/html-directive.ts +++ b/packages/web-components/fast-element/src/templating/html-directive.ts @@ -1,6 +1,14 @@ import { DOM } from "../dom.js"; import type { Behavior } from "../observation/behavior.js"; +/** + * The target nodes available to a behavior. + * @public + */ +export type BehaviorTargets = { + [id: string]: Node; +}; + /** * A factory that can create a {@link Behavior} associated with a particular * location within a DOM fragment. @@ -8,15 +16,15 @@ import type { Behavior } from "../observation/behavior.js"; */ export interface NodeBehaviorFactory { /** - * The index of the DOM node to which the created behavior will apply. + * The structural id of the DOM node to which the created behavior will apply. */ - targetIndex: number; + targetId: string; /** - * Creates a behavior for the provided target node. - * @param target - The node instance to create the behavior for. + * Creates a behavior. + * @param target - The targets available for behaviors to be attached to. */ - createBehavior(target: Node): Behavior; + createBehavior(targets: BehaviorTargets): Behavior; } /** @@ -25,9 +33,9 @@ export interface NodeBehaviorFactory { */ export abstract class HTMLDirective implements NodeBehaviorFactory { /** - * The index of the DOM node to which the created behavior will apply. + * The structural id of the DOM node to which the created behavior will apply. */ - public targetIndex: number = 0; + public targetId: string = "h"; /** * Creates a placeholder string based on the directive's index within the template. @@ -36,10 +44,10 @@ export abstract class HTMLDirective implements NodeBehaviorFactory { public abstract createPlaceholder(index: number): string; /** - * Creates a behavior for the provided target node. - * @param target - The node instance to create the behavior for. + * Creates a behavior. + * @param targets - The targets available for behaviors to be attached to. */ - public abstract createBehavior(target: Node): Behavior; + public abstract createBehavior(targets: BehaviorTargets): Behavior; } /** @@ -66,7 +74,11 @@ export abstract class TargetedHTMLDirective extends HTMLDirective { * an {@link AttachedBehaviorHTMLDirective}. * @public */ -export type AttachedBehaviorType = new (target: any, options: T) => Behavior; +export type AttachedBehaviorType = new ( + targets: BehaviorTargets, + targetId: string, + options: T +) => Behavior; /** * A directive that attaches special behavior to an element via a custom attribute. @@ -98,13 +110,13 @@ export class AttachedBehaviorHTMLDirective extends HTMLDirective { } /** - * Creates a behavior for the provided target node. - * @param target - The node instance to create the behavior for. + * Creates a behavior. + * @param targets - The targets available for behaviors to be attached to. * @remarks * Creates an instance of the `behavior` type this directive was constructed with - * and passes the target and options to that `behavior`'s constructor. + * and passes the targets, targetId, and options to that `behavior`'s constructor. */ - public createBehavior(target: Node): Behavior { - return new this.behavior(target, this.options); + public createBehavior(targets: BehaviorTargets): Behavior { + return new this.behavior(targets, this.targetId, this.options); } } diff --git a/packages/web-components/fast-element/src/templating/ref.ts b/packages/web-components/fast-element/src/templating/ref.ts index 96a65cb026d..a3d90e9db41 100644 --- a/packages/web-components/fast-element/src/templating/ref.ts +++ b/packages/web-components/fast-element/src/templating/ref.ts @@ -1,18 +1,27 @@ -import type { Behavior } from "../observation/behavior.js"; -import type { CaptureType } from "./template.js"; -import { AttachedBehaviorHTMLDirective } from "./html-directive.js"; +import type { Behavior } from "../observation/behavior"; +import type { CaptureType } from "./template"; +import { AttachedBehaviorHTMLDirective } from "./html-directive"; +import type { BehaviorTargets } from ".."; /** * The runtime behavior for template references. * @public */ export class RefBehavior implements Behavior { + private target: Node; + /** * Creates an instance of RefBehavior. * @param target - The element to reference. * @param propertyName - The name of the property to assign the reference to. */ - public constructor(private target: HTMLElement, private propertyName: string) {} + public constructor( + targets: BehaviorTargets, + targetId: string, + private propertyName: string + ) { + this.target = targets[targetId]; + } /** * Bind this behavior to the source. diff --git a/packages/web-components/fast-element/src/templating/repeat.spec.ts b/packages/web-components/fast-element/src/templating/repeat.spec.ts index e5b03b2f6b9..f4094de7514 100644 --- a/packages/web-components/fast-element/src/templating/repeat.spec.ts +++ b/packages/web-components/fast-element/src/templating/repeat.spec.ts @@ -6,6 +6,17 @@ import { DOM } from "../dom"; import { toHTML } from "../__test__/helpers"; describe("The repeat", () => { + function createLocation() { + const parent = document.createElement("div"); + const location = document.createComment(""); + const targetId = 'r'; + const targets = { [targetId]: location }; + + parent.appendChild(location); + + return { parent, targets, targetId }; + } + context("template function", () => { it("returns a RepeatDirective", () => { const directive = repeat( @@ -18,12 +29,14 @@ describe("The repeat", () => { context("directive", () => { it("creates a RepeatBehavior", () => { + const { parent, targets, targetId } = createLocation(); const directive = repeat( () => [], html`test` ) as RepeatDirective; - const target = document.createComment(""); - const behavior = directive.createBehavior(target); + directive.targetId = targetId; + + const behavior = directive.createBehavior(targets); expect(behavior).to.be.instanceOf(RepeatBehavior); }); @@ -65,15 +78,6 @@ describe("The repeat", () => { } } - function createLocation() { - const parent = document.createElement("div"); - const location = document.createComment(""); - - parent.appendChild(location); - - return { parent, location }; - } - function createOutput( size: number, filter: (index: number) => boolean = () => true, @@ -93,12 +97,14 @@ describe("The repeat", () => { zeroThroughTen.forEach(size => { it(`renders a template for each item in array of size ${size}`, () => { - const { parent, location } = createLocation(); + const { parent, targets, targetId } = createLocation(); const directive = repeat( x => x.items, itemTemplate ) as RepeatDirective; - const behavior = directive.createBehavior(location); + directive.targetId = targetId; + + const behavior = directive.createBehavior(targets); const vm = new ViewModel(size); behavior.bind(vm, defaultExecutionContext); @@ -109,12 +115,13 @@ describe("The repeat", () => { zeroThroughTen.forEach(size => { it(`renders empty when an array of size ${size} is replaced with an empty array`, async () => { - const { parent, location } = createLocation(); + const { parent, targets, targetId } = createLocation(); const directive = repeat( x => x.items, wrappedItemTemplate ) as RepeatDirective; - const behavior = directive.createBehavior(location); + directive.targetId = targetId; + const behavior = directive.createBehavior(targets); const data = new ViewModel(size); behavior.bind(data, defaultExecutionContext); @@ -141,12 +148,13 @@ describe("The repeat", () => { zeroThroughTen.forEach(size => { it(`updates rendered HTML when a new item is pushed into an array of size ${size}`, async () => { - const { parent, location } = createLocation(); + const { parent, targets, targetId } = createLocation(); const directive = repeat( x => x.items, itemTemplate ) as RepeatDirective; - const behavior = directive.createBehavior(location); + directive.targetId = targetId; + const behavior = directive.createBehavior(targets); const vm = new ViewModel(size); behavior.bind(vm, defaultExecutionContext); @@ -160,12 +168,13 @@ describe("The repeat", () => { oneThroughTen.forEach(size => { it(`updates rendered HTML when a single item is spliced from the end of an array of size ${size}`, async () => { - const { parent, location } = createLocation(); + const { parent, targets, targetId } = createLocation(); const directive = repeat( x => x.items, itemTemplate ) as RepeatDirective; - const behavior = directive.createBehavior(location); + directive.targetId = targetId; + const behavior = directive.createBehavior(targets); const vm = new ViewModel(size); behavior.bind(vm, defaultExecutionContext); @@ -183,12 +192,13 @@ describe("The repeat", () => { oneThroughTen.forEach(size => { it(`updates rendered HTML when a single item is spliced from the beginning of an array of size ${size}`, async () => { - const { parent, location } = createLocation(); + const { parent, targets, targetId } = createLocation(); const directive = repeat( x => x.items, itemTemplate ) as RepeatDirective; - const behavior = directive.createBehavior(location); + directive.targetId = targetId; + const behavior = directive.createBehavior(targets); const vm = new ViewModel(size); behavior.bind(vm, defaultExecutionContext); @@ -203,12 +213,13 @@ describe("The repeat", () => { oneThroughTen.forEach(size => { it(`updates rendered HTML when a single item is replaced from the end of an array of size ${size}`, async () => { - const { parent, location } = createLocation(); + const { parent, targets, targetId } = createLocation(); const directive = repeat( x => x.items, itemTemplate ) as RepeatDirective; - const behavior = directive.createBehavior(location); + directive.targetId = targetId; + const behavior = directive.createBehavior(targets); const vm = new ViewModel(size); behavior.bind(vm, defaultExecutionContext); @@ -226,12 +237,13 @@ describe("The repeat", () => { oneThroughTen.forEach(size => { it(`updates rendered HTML when a single item is replaced from the beginning of an array of size ${size}`, async () => { - const { parent, location } = createLocation(); + const { parent, targets, targetId } = createLocation(); const directive = repeat( x => x.items, itemTemplate ) as RepeatDirective; - const behavior = directive.createBehavior(location); + directive.targetId = targetId; + const behavior = directive.createBehavior(targets); const vm = new ViewModel(size); behavior.bind(vm, defaultExecutionContext); @@ -248,12 +260,13 @@ describe("The repeat", () => { oneThroughTen.forEach(size => { it(`updates all when the template changes for an array of size ${size}`, async () => { - const { parent, location } = createLocation(); + const { parent, targets, targetId } = createLocation(); const directive = repeat( x => x.items, x => vm.template ) as RepeatDirective; - const behavior = directive.createBehavior(location); + directive.targetId = targetId; + const behavior = directive.createBehavior(targets); const vm = new ViewModel(size); behavior.bind(vm, defaultExecutionContext); @@ -277,13 +290,13 @@ describe("The repeat", () => { )} `; - const { parent, location } = createLocation(); + const { parent, targets, targetId } = createLocation(); const directive = repeat( x => x.items, deepItemTemplate ) as RepeatDirective; - - const behavior = directive.createBehavior(location); + directive.targetId = targetId; + const behavior = directive.createBehavior(targets); const vm = new ViewModel(size, true); behavior.bind(vm, defaultExecutionContext); @@ -299,12 +312,13 @@ describe("The repeat", () => { oneThroughTen.forEach(size => { it(`handles back to back shift operations for arrays of size ${size}`, async () => { - const { parent, location } = createLocation(); + const { parent, targets, targetId } = createLocation(); const directive = repeat( x => x.items, itemTemplate ) as RepeatDirective; - const behavior = directive.createBehavior(location); + directive.targetId = targetId; + const behavior = directive.createBehavior(targets); const vm = new ViewModel(size); behavior.bind(vm, defaultExecutionContext); @@ -322,12 +336,13 @@ describe("The repeat", () => { zeroThroughTen.forEach(size => { it(`updates rendered HTML when a new item is pushed into an array of size ${size} after it has been unbound and rebound`, async () => { - const { parent, location } = createLocation(); + const { parent, targets, targetId } = createLocation(); const directive = repeat( x => x.items, itemTemplate ) as RepeatDirective; - const behavior = directive.createBehavior(location); + directive.targetId = targetId; + const behavior = directive.createBehavior(targets); const vm = new ViewModel(size); behavior.bind(vm, defaultExecutionContext); diff --git a/packages/web-components/fast-element/src/templating/repeat.ts b/packages/web-components/fast-element/src/templating/repeat.ts index 6e4fa2fa3c8..19a9c5d07d3 100644 --- a/packages/web-components/fast-element/src/templating/repeat.ts +++ b/packages/web-components/fast-element/src/templating/repeat.ts @@ -1,18 +1,19 @@ -import { DOM } from "../dom.js"; +import { DOM } from "../dom"; import { Binding, BindingObserver, ExecutionContext, Observable, -} from "../observation/observable.js"; -import type { Notifier, Subscriber } from "../observation/notifier.js"; -import { enableArrayObservation } from "../observation/array-observer.js"; -import type { Splice } from "../observation/array-change-records.js"; -import type { Behavior } from "../observation/behavior.js"; -import { emptyArray } from "../platform.js"; -import { HTMLDirective } from "./html-directive.js"; -import { HTMLView, SyntheticView } from "./view.js"; -import type { CaptureType, SyntheticViewTemplate } from "./template.js"; +} from "../observation/observable"; +import type { Notifier, Subscriber } from "../observation/notifier"; +import { enableArrayObservation } from "../observation/array-observer"; +import type { Splice } from "../observation/array-change-records"; +import type { Behavior } from "../observation/behavior"; +import { emptyArray } from "../platform"; +import { HTMLDirective } from "./html-directive"; +import { HTMLView, SyntheticView } from "./view"; +import type { CaptureType, SyntheticViewTemplate } from "./template"; +import type { BehaviorTargets } from ".."; /** * Options for configuring repeat behavior. @@ -329,9 +330,9 @@ export class RepeatDirective extends HTMLDirective { * Creates a behavior for the provided target node. * @param target - The node instance to create the behavior for. */ - public createBehavior(target: Node): RepeatBehavior { + public createBehavior(targets: BehaviorTargets): RepeatBehavior { return new RepeatBehavior( - target, + targets[this.targetId], this.itemsBinding, this.isItemsBindingVolatile, this.templateBinding, diff --git a/packages/web-components/fast-element/src/templating/slotted.spec.ts b/packages/web-components/fast-element/src/templating/slotted.spec.ts index e5593fc3004..bfcbd4c448f 100644 --- a/packages/web-components/fast-element/src/templating/slotted.spec.ts +++ b/packages/web-components/fast-element/src/templating/slotted.spec.ts @@ -15,9 +15,12 @@ describe("The slotted", () => { context("directive", () => { it("creates a SlottedBehavior", () => { + const targetId = 'r'; const directive = slotted("test") as AttachedBehaviorHTMLDirective; + directive.targetId = targetId; const target = document.createElement("slot"); - const behavior = directive.createBehavior(target); + const targets = { [targetId]: target } + const behavior = directive.createBehavior(targets); expect(behavior).to.be.instanceOf(SlottedBehavior); }); @@ -45,15 +48,17 @@ describe("The slotted", () => { const slot = document.createElement("slot"); const shadowRoot = host.attachShadow({ mode: "open" }); const children = createAndAppendChildren(host, elementName); + const targetId = 'r'; + const targets = { [targetId]: slot }; shadowRoot.appendChild(slot); - return { host, slot, children }; + return { host, slot, children, targets, targetId }; } it("gathers nodes from a slot", () => { - const { host, slot, children } = createDOM(); - const behavior = new SlottedBehavior(slot, { property: "nodes" }); + const { children, targets, targetId } = createDOM(); + const behavior = new SlottedBehavior(targets, targetId, { property: "nodes" }); const model = new Model(); behavior.bind(model); @@ -62,8 +67,8 @@ describe("The slotted", () => { }); it("gathers nodes from a slot with a filter", () => { - const { host, slot, children } = createDOM("foo-bar"); - const behavior = new SlottedBehavior(slot, { + const { targets, targetId, children } = createDOM("foo-bar"); + const behavior = new SlottedBehavior(targets, targetId, { property: "nodes", filter: elements("foo-bar"), }); @@ -75,8 +80,8 @@ describe("The slotted", () => { }); it("updates when slotted nodes change", async () => { - const { host, slot, children } = createDOM("foo-bar"); - const behavior = new SlottedBehavior(slot, { property: "nodes" }); + const { host, slot, children, targets, targetId } = createDOM("foo-bar"); + const behavior = new SlottedBehavior(targets, targetId, { property: "nodes" }); const model = new Model(); behavior.bind(model); @@ -91,8 +96,8 @@ describe("The slotted", () => { }); it("updates when slotted nodes change with a filter", async () => { - const { host, slot, children } = createDOM("foo-bar"); - const behavior = new SlottedBehavior(slot, { + const { host, slot, children, targets, targetId } = createDOM("foo-bar"); + const behavior = new SlottedBehavior(targets, targetId, { property: "nodes", filter: elements("foo-bar"), }); @@ -110,8 +115,8 @@ describe("The slotted", () => { }); it("clears and unwatches when unbound", async () => { - const { host, slot, children } = createDOM("foo-bar"); - const behavior = new SlottedBehavior(slot, { property: "nodes" }); + const { host, slot, children, targets, targetId } = createDOM("foo-bar"); + const behavior = new SlottedBehavior(targets, targetId, { property: "nodes" }); const model = new Model(); behavior.bind(model); diff --git a/packages/web-components/fast-element/src/templating/slotted.ts b/packages/web-components/fast-element/src/templating/slotted.ts index 16af54c6a1b..880e2f0cf67 100644 --- a/packages/web-components/fast-element/src/templating/slotted.ts +++ b/packages/web-components/fast-element/src/templating/slotted.ts @@ -1,6 +1,7 @@ -import { AttachedBehaviorHTMLDirective } from "./html-directive.js"; -import { NodeBehaviorOptions, NodeObservationBehavior } from "./node-observation.js"; -import type { CaptureType } from "./template.js"; +import type { BehaviorTargets } from ".."; +import { AttachedBehaviorHTMLDirective } from "./html-directive"; +import { NodeBehaviorOptions, NodeObservationBehavior } from "./node-observation"; +import type { CaptureType } from "./template"; /** * The options used to configure slotted node observation. @@ -20,8 +21,12 @@ export class SlottedBehavior extends NodeObservationBehavior { it(`transforms a string into a ViewTemplate.`, () => { @@ -230,7 +231,7 @@ describe(`The html tag template helper`, () => { it(`captures a case-sensitive property name when used with a named target directive`, () => { class TestDirective extends TargetedHTMLDirective { targetName: string | undefined; - createBehavior(target: Node) { + createBehavior(targets: BehaviorTargets) { return { bind() {}, unbind() {} }; } } diff --git a/packages/web-components/fast-element/src/templating/template.ts b/packages/web-components/fast-element/src/templating/template.ts index f05b4079f49..626414713ce 100644 --- a/packages/web-components/fast-element/src/templating/template.ts +++ b/packages/web-components/fast-element/src/templating/template.ts @@ -1,14 +1,15 @@ -import { DOM } from "../dom.js"; -import type { Behavior } from "../observation/behavior.js"; -import { Binding, defaultExecutionContext } from "../observation/observable.js"; -import { compileTemplate } from "./compiler.js"; -import { ElementView, HTMLView, SyntheticView } from "./view.js"; +import { DOM } from "../dom"; +import type { Behavior } from "../observation/behavior"; +import { Binding, defaultExecutionContext } from "../observation/observable"; +import { compileTemplate } from "./compiler"; +import { ElementView, HTMLView, SyntheticView } from "./view"; import { HTMLDirective, NodeBehaviorFactory, TargetedHTMLDirective, -} from "./html-directive.js"; -import { HTMLBindingDirective } from "./binding.js"; +} from "./html-directive"; +import { HTMLBindingDirective } from "./binding"; +import type { HTMLTemplateCompilationResult } from ".."; /** * A template capable of creating views specifically for rendering custom elements. @@ -50,12 +51,7 @@ export interface SyntheticViewTemplate { /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ export class ViewTemplate implements ElementViewTemplate, SyntheticViewTemplate { - private behaviorCount: number = 0; - private hasHostBehaviors: boolean = false; - private fragment: DocumentFragment | null = null; - private targetOffset: number = 0; - private viewBehaviorFactories: NodeBehaviorFactory[] | null = null; - private hostBehaviorFactories: NodeBehaviorFactory[] | null = null; + private result: HTMLTemplateCompilationResult | null = null; /** * The html representing what this template will @@ -86,7 +82,7 @@ export class ViewTemplate * @param hostBindingTarget - The element that host behaviors will be bound to. */ public create(hostBindingTarget?: Element): HTMLView { - if (this.fragment === null) { + if (this.result === null) { let template: HTMLTemplateElement; const html = this.html; @@ -103,48 +99,26 @@ export class ViewTemplate template = html; } - const result = compileTemplate(template, this.directives); - - this.fragment = result.fragment; - this.viewBehaviorFactories = result.viewBehaviorFactories; - this.hostBehaviorFactories = result.hostBehaviorFactories; - this.targetOffset = result.targetOffset; - this.behaviorCount = - this.viewBehaviorFactories.length + this.hostBehaviorFactories.length; - this.hasHostBehaviors = this.hostBehaviorFactories.length > 0; + this.result = compileTemplate(template, this.directives); } - const fragment = this.fragment.cloneNode(true) as DocumentFragment; - const viewFactories = this.viewBehaviorFactories!; - const behaviors = new Array(this.behaviorCount); - const walker = DOM.createTemplateWalker(fragment); - + const result = this.result; + const fragment = result.fragment.cloneNode(true) as DocumentFragment; + const viewFactories = result.viewBehaviorFactories; + const behaviors = new Array(result.behaviorCount); + const targets = result.createTargets(fragment, hostBindingTarget); let behaviorIndex = 0; - let targetIndex = this.targetOffset; - let node = walker.nextNode(); for (let ii = viewFactories.length; behaviorIndex < ii; ++behaviorIndex) { const factory = viewFactories[behaviorIndex]; - const factoryIndex = factory.targetIndex; - - while (node !== null) { - if (targetIndex === factoryIndex) { - behaviors[behaviorIndex] = factory.createBehavior(node); - break; - } else { - node = walker.nextNode(); - targetIndex++; - } - } + behaviors[behaviorIndex] = factory.createBehavior(targets); } - if (this.hasHostBehaviors) { - const hostFactories = this.hostBehaviorFactories!; + if (result.hasHostBehaviors) { + const hostFactories = result.hostBehaviorFactories; for (let i = 0, ii = hostFactories.length; i < ii; ++i, ++behaviorIndex) { - behaviors[behaviorIndex] = hostFactories[i].createBehavior( - hostBindingTarget! - ); + behaviors[behaviorIndex] = hostFactories[i].createBehavior(targets); } } From c1b2c76e5ebdbbec90be6b91e6140341896dd290 Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Tue, 12 Oct 2021 15:43:55 -0400 Subject: [PATCH 002/135] fix(compiler): don't add unnecessary descriptors to compile result --- .../fast-element/docs/api-report.md | 2 +- .../fast-element/src/templating/compiler.ts | 44 +++++++++++++------ 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/packages/web-components/fast-element/docs/api-report.md b/packages/web-components/fast-element/docs/api-report.md index 3a878652f8e..e0f872c35aa 100644 --- a/packages/web-components/fast-element/docs/api-report.md +++ b/packages/web-components/fast-element/docs/api-report.md @@ -357,7 +357,7 @@ export abstract class HTMLDirective implements NodeBehaviorFactory { // @public export class HTMLTemplateCompilationResult { - constructor(fragment: DocumentFragment, viewBehaviorFactories: NodeBehaviorFactory[], hostBehaviorFactories: NodeBehaviorFactory[], targetIds: string[]); + constructor(fragment: DocumentFragment, viewBehaviorFactories: NodeBehaviorFactory[], hostBehaviorFactories: NodeBehaviorFactory[], targetIds: string[], descriptors: PropertyDescriptorMap); // (undocumented) get behaviorCount(): number; createTargets(root: Node, host?: Node): BehaviorTargets; diff --git a/packages/web-components/fast-element/src/templating/compiler.ts b/packages/web-components/fast-element/src/templating/compiler.ts index 2f3fd01db41..1a61fff87c2 100644 --- a/packages/web-components/fast-element/src/templating/compiler.ts +++ b/packages/web-components/fast-element/src/templating/compiler.ts @@ -10,9 +10,14 @@ type InlineDirective = HTMLDirective & { targetAtContent(): void; }; -const descriptors: PropertyDescriptorMap = {}; +const descriptorCache: PropertyDescriptorMap = {}; -function addTargetDescriptor(parentId: string, targetId: string, targetIndex: number) { +function addTargetDescriptor( + descriptors: PropertyDescriptorMap, + parentId: string, + targetId: string, + targetIndex: number +) { if ( targetId === "r" || // root targetId === "h" || // host @@ -27,7 +32,7 @@ function addTargetDescriptor(parentId: string, targetId: string, targetIndex: nu if (index !== -1) { const grandparentId = parentId.substr(0, index); const childIndex = parseInt(parentId.substr(index)); - addTargetDescriptor(grandparentId, parentId, childIndex); + addTargetDescriptor(descriptors, grandparentId, parentId, childIndex); } } @@ -39,14 +44,22 @@ function createTargetDescriptor( targetId: string, targetIndex: number ): PropertyDescriptor { - const field = `_${targetId}`; + let descriptor = descriptorCache[targetId]; - return { - configurable: false, - get: function () { - return this[field] || (this[field] = this[parentId].childNodes[targetIndex]); - }, - }; + if (!descriptor) { + const field = `_${targetId}`; + + descriptorCache[targetId] = descriptor = { + configurable: false, + get: function () { + return ( + this[field] || (this[field] = this[parentId].childNodes[targetIndex]) + ); + }, + }; + } + + return descriptor; } let sharedContext: CompilationContext | null = null; @@ -61,6 +74,7 @@ class CompilationContext { public behaviorFactories!: NodeBehaviorFactory[]; public directives: ReadonlyArray; public targetIds!: string[]; + public descriptors: PropertyDescriptorMap; public addFactory( factory: NodeBehaviorFactory, @@ -72,7 +86,7 @@ class CompilationContext { this.targetIds.push(targetId); } - addTargetDescriptor(parentId, targetId, targetIndex); + addTargetDescriptor(this.descriptors, parentId, targetId, targetIndex); factory.targetId = targetId; this.behaviorFactories.push(factory); } @@ -90,6 +104,7 @@ class CompilationContext { public reset(): void { this.behaviorFactories = []; this.targetIds = []; + this.descriptors = {}; } public release(): void { @@ -318,7 +333,8 @@ export class HTMLTemplateCompilationResult { public readonly fragment: DocumentFragment, public readonly viewBehaviorFactories: NodeBehaviorFactory[], public readonly hostBehaviorFactories: NodeBehaviorFactory[], - private targetIds: string[] + private targetIds: string[], + descriptors: PropertyDescriptorMap ) { this.proto = Object.create(null, descriptors); } @@ -406,12 +422,14 @@ export function compileTemplate( const viewBehaviorFactories = context.behaviorFactories; const targetIds = context.targetIds; + const descriptors = context.descriptors; context.release(); return new HTMLTemplateCompilationResult( fragment, viewBehaviorFactories, hostBehaviorFactories, - targetIds + targetIds, + descriptors ); } From 924fda799c2cb656f9adfe954a69c3d6d7eabca7 Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Tue, 12 Oct 2021 22:44:08 -0400 Subject: [PATCH 003/135] fix(compiler): correct extraction of child index from parent id --- .../web-components/fast-element/src/templating/compiler.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/web-components/fast-element/src/templating/compiler.ts b/packages/web-components/fast-element/src/templating/compiler.ts index 1a61fff87c2..e1534a1595a 100644 --- a/packages/web-components/fast-element/src/templating/compiler.ts +++ b/packages/web-components/fast-element/src/templating/compiler.ts @@ -1,4 +1,4 @@ -import type { BehaviorTargets } from ".."; +import type { BehaviorTargets } from "./html-directive"; import { _interpolationEnd, _interpolationStart, DOM } from "../dom"; import type { Binding, ExecutionContext } from "../observation/observable"; import { HTMLBindingDirective } from "./binding"; @@ -31,7 +31,7 @@ function addTargetDescriptor( if (index !== -1) { const grandparentId = parentId.substr(0, index); - const childIndex = parseInt(parentId.substr(index)); + const childIndex = parseInt(parentId.substr(index + 1)); addTargetDescriptor(descriptors, grandparentId, parentId, childIndex); } } From c689dcb9650deff4fdc0d77ea63dc0e7ba46570c Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Tue, 12 Oct 2021 23:07:05 -0400 Subject: [PATCH 004/135] feat(compiler): unify host and view factories --- .../fast-element/docs/api-report.md | 14 ++----- .../src/templating/compiler.spec.ts | 14 +++---- .../fast-element/src/templating/compiler.ts | 38 +++++-------------- .../fast-element/src/templating/template.ts | 18 ++------- 4 files changed, 24 insertions(+), 60 deletions(-) diff --git a/packages/web-components/fast-element/docs/api-report.md b/packages/web-components/fast-element/docs/api-report.md index e0f872c35aa..a90e65c97ec 100644 --- a/packages/web-components/fast-element/docs/api-report.md +++ b/packages/web-components/fast-element/docs/api-report.md @@ -357,19 +357,13 @@ export abstract class HTMLDirective implements NodeBehaviorFactory { // @public export class HTMLTemplateCompilationResult { - constructor(fragment: DocumentFragment, viewBehaviorFactories: NodeBehaviorFactory[], hostBehaviorFactories: NodeBehaviorFactory[], targetIds: string[], descriptors: PropertyDescriptorMap); - // (undocumented) - get behaviorCount(): number; + constructor(fragment: DocumentFragment, factories: NodeBehaviorFactory[], targetIds: string[], descriptors: PropertyDescriptorMap); createTargets(root: Node, host?: Node): BehaviorTargets; // (undocumented) - readonly fragment: DocumentFragment; - // (undocumented) - get hasHostBehaviors(): boolean; - // (undocumented) - readonly hostBehaviorFactories: NodeBehaviorFactory[]; + readonly factories: NodeBehaviorFactory[]; // (undocumented) - readonly viewBehaviorFactories: NodeBehaviorFactory[]; -} + readonly fragment: DocumentFragment; + } // @public export class HTMLView implements ElementView, SyntheticView { diff --git a/packages/web-components/fast-element/src/templating/compiler.spec.ts b/packages/web-components/fast-element/src/templating/compiler.spec.ts index 26fc7b5fe4d..4e80842363f 100644 --- a/packages/web-components/fast-element/src/templating/compiler.spec.ts +++ b/packages/web-components/fast-element/src/templating/compiler.spec.ts @@ -146,7 +146,7 @@ describe("The template compiler", () => { scenarios.forEach(x => { it(`handles ${x.type} binding expression(s)`, () => { - const { fragment, viewBehaviorFactories } = compile(x.html, x.directives); + const { fragment, factories } = compile(x.html, x.directives); expect(toHTML(fragment)).to.equal(x.fragment); expect(toHTML(fragment.cloneNode(true) as DocumentFragment)).to.equal( @@ -160,7 +160,7 @@ describe("The template compiler", () => { ); } - const length = viewBehaviorFactories.length; + const length = factories.length; expect(length).to.equal(x.directives.length); @@ -168,7 +168,7 @@ describe("The template compiler", () => { expect(length).to.equal(x.targetIds.length); for (let i = 0; i < length; ++i) { - expect(viewBehaviorFactories[i].targetId).to.equal( + expect(factories[i].targetId).to.equal( x.targetIds[i] ); } @@ -314,7 +314,7 @@ describe("The template compiler", () => { scenarios.forEach(x => { it(`handles ${x.type} binding expression(s)`, () => { - const { fragment, viewBehaviorFactories } = compile(x.html, x.directives); + const { fragment, factories } = compile(x.html, x.directives); expect(toHTML(fragment)).to.equal(x.fragment); expect(toHTML(fragment.cloneNode(true) as DocumentFragment)).to.equal( @@ -323,7 +323,7 @@ describe("The template compiler", () => { if (x.result) { expect( - (viewBehaviorFactories[0] as HTMLBindingDirective).binding( + (factories[0] as HTMLBindingDirective).binding( scope, defaultExecutionContext ) @@ -331,12 +331,12 @@ describe("The template compiler", () => { } if (x.targetIds) { - const length = viewBehaviorFactories.length; + const length = factories.length; expect(length).to.equal(x.targetIds.length); for (let i = 0; i < length; ++i) { - expect(viewBehaviorFactories[i].targetId).to.equal( + expect(factories[i].targetId).to.equal( x.targetIds[i] ); } diff --git a/packages/web-components/fast-element/src/templating/compiler.ts b/packages/web-components/fast-element/src/templating/compiler.ts index e1534a1595a..b2523311103 100644 --- a/packages/web-components/fast-element/src/templating/compiler.ts +++ b/packages/web-components/fast-element/src/templating/compiler.ts @@ -28,12 +28,9 @@ function addTargetDescriptor( if (!descriptors[parentId]) { const index = parentId.lastIndexOf("."); - - if (index !== -1) { - const grandparentId = parentId.substr(0, index); - const childIndex = parseInt(parentId.substr(index + 1)); - addTargetDescriptor(descriptors, grandparentId, parentId, childIndex); - } + const grandparentId = parentId.substr(0, index); + const childIndex = parseInt(parentId.substr(index + 1)); + addTargetDescriptor(descriptors, grandparentId, parentId, childIndex); } descriptors[targetId] = createTargetDescriptor(parentId, targetId, targetIndex); @@ -71,7 +68,7 @@ const next = { }; class CompilationContext { - public behaviorFactories!: NodeBehaviorFactory[]; + public factories!: NodeBehaviorFactory[]; public directives: ReadonlyArray; public targetIds!: string[]; public descriptors: PropertyDescriptorMap; @@ -88,7 +85,7 @@ class CompilationContext { addTargetDescriptor(this.descriptors, parentId, targetId, targetIndex); factory.targetId = targetId; - this.behaviorFactories.push(factory); + this.factories.push(factory); } public captureContentBinding( @@ -102,7 +99,7 @@ class CompilationContext { } public reset(): void { - this.behaviorFactories = []; + this.factories = []; this.targetIds = []; this.descriptors = {}; } @@ -331,22 +328,13 @@ export class HTMLTemplateCompilationResult { */ public constructor( public readonly fragment: DocumentFragment, - public readonly viewBehaviorFactories: NodeBehaviorFactory[], - public readonly hostBehaviorFactories: NodeBehaviorFactory[], + public readonly factories: NodeBehaviorFactory[], private targetIds: string[], descriptors: PropertyDescriptorMap ) { this.proto = Object.create(null, descriptors); } - get hasHostBehaviors() { - return this.hostBehaviorFactories.length > 0; - } - - get behaviorCount() { - return this.viewBehaviorFactories.length + this.hostBehaviorFactories.length; - } - /** * Creates a behavior target lookup object. * @param host - The host element. @@ -391,8 +379,6 @@ export function compileTemplate( const context = CompilationContext.borrow(directives); compileAttributes(context, "", template, /* host */ "h", 0, true); - const hostBehaviorFactories = context.behaviorFactories; - context.reset(); if ( // If the first node in a fragment is a marker, that means it's an unstable first node, @@ -420,16 +406,10 @@ export function compileTemplate( next.node = null; // prevent leaks - const viewBehaviorFactories = context.behaviorFactories; + const factories = context.factories; const targetIds = context.targetIds; const descriptors = context.descriptors; context.release(); - return new HTMLTemplateCompilationResult( - fragment, - viewBehaviorFactories, - hostBehaviorFactories, - targetIds, - descriptors - ); + return new HTMLTemplateCompilationResult(fragment, factories, targetIds, descriptors); } diff --git a/packages/web-components/fast-element/src/templating/template.ts b/packages/web-components/fast-element/src/templating/template.ts index 626414713ce..4cf3724a459 100644 --- a/packages/web-components/fast-element/src/templating/template.ts +++ b/packages/web-components/fast-element/src/templating/template.ts @@ -104,22 +104,12 @@ export class ViewTemplate const result = this.result; const fragment = result.fragment.cloneNode(true) as DocumentFragment; - const viewFactories = result.viewBehaviorFactories; - const behaviors = new Array(result.behaviorCount); + const factories = result.factories; + const behaviors = new Array(factories.length); const targets = result.createTargets(fragment, hostBindingTarget); - let behaviorIndex = 0; - for (let ii = viewFactories.length; behaviorIndex < ii; ++behaviorIndex) { - const factory = viewFactories[behaviorIndex]; - behaviors[behaviorIndex] = factory.createBehavior(targets); - } - - if (result.hasHostBehaviors) { - const hostFactories = result.hostBehaviorFactories; - - for (let i = 0, ii = hostFactories.length; i < ii; ++i, ++behaviorIndex) { - behaviors[behaviorIndex] = hostFactories[i].createBehavior(targets); - } + for (let i = 0, ii = factories.length; i < ii; ++i) { + behaviors[i] = factories[i].createBehavior(targets); } return new HTMLView(fragment, behaviors); From 1240f77aa7bba507b509309f76e9aa0a8f87c3ee Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Wed, 13 Oct 2021 11:42:40 -0400 Subject: [PATCH 005/135] feat(view): unify behavior creation with first bind; clean up compiler --- .../fast-element/docs/api-report.md | 2 +- .../fast-element/src/templating/compiler.ts | 94 +++++++++---------- .../fast-element/src/templating/template.ts | 13 +-- .../fast-element/src/templating/view.ts | 48 +++++++--- 4 files changed, 82 insertions(+), 75 deletions(-) diff --git a/packages/web-components/fast-element/docs/api-report.md b/packages/web-components/fast-element/docs/api-report.md index a90e65c97ec..88d13a0023e 100644 --- a/packages/web-components/fast-element/docs/api-report.md +++ b/packages/web-components/fast-element/docs/api-report.md @@ -367,7 +367,7 @@ export class HTMLTemplateCompilationResult { // @public export class HTMLView implements ElementView, SyntheticView { - constructor(fragment: DocumentFragment, behaviors: Behavior[]); + constructor(fragment: DocumentFragment, factories: NodeBehaviorFactory[], targets: BehaviorTargets); appendTo(node: Node): void; bind(source: unknown, context: ExecutionContext): void; context: ExecutionContext | null; diff --git a/packages/web-components/fast-element/src/templating/compiler.ts b/packages/web-components/fast-element/src/templating/compiler.ts index b2523311103..0c802ecd9d0 100644 --- a/packages/web-components/fast-element/src/templating/compiler.ts +++ b/packages/web-components/fast-element/src/templating/compiler.ts @@ -10,6 +10,7 @@ type InlineDirective = HTMLDirective & { targetAtContent(): void; }; +const targetIdFrom = (parentId: string, nodeIndex: number) => `${parentId}.${nodeIndex}`; const descriptorCache: PropertyDescriptorMap = {}; function addTargetDescriptor( @@ -47,10 +48,9 @@ function createTargetDescriptor( const field = `_${targetId}`; descriptorCache[targetId] = descriptor = { - configurable: false, - get: function () { + get() { return ( - this[field] || (this[field] = this[parentId].childNodes[targetIndex]) + this[field] ?? (this[field] = this[parentId].childNodes[targetIndex]) ); }, }; @@ -68,10 +68,10 @@ const next = { }; class CompilationContext { - public factories!: NodeBehaviorFactory[]; + public factories: NodeBehaviorFactory[] = []; + public targetIds: string[] = []; + public descriptors: PropertyDescriptorMap = {}; public directives: ReadonlyArray; - public targetIds!: string[]; - public descriptors: PropertyDescriptorMap; public addFactory( factory: NodeBehaviorFactory, @@ -81,9 +81,9 @@ class CompilationContext { ): void { if (this.targetIds.indexOf(targetId) === -1) { this.targetIds.push(targetId); + addTargetDescriptor(this.descriptors, parentId, targetId, targetIndex); } - addTargetDescriptor(this.descriptors, parentId, targetId, targetIndex); factory.targetId = targetId; this.factories.push(factory); } @@ -98,23 +98,27 @@ class CompilationContext { this.addFactory(directive, parentId, targetId, targetIndex); } - public reset(): void { + public close(fragment: DocumentFragment): HTMLTemplateCompilationResult { + const result = new HTMLTemplateCompilationResult( + fragment, + this.factories, + this.targetIds, + this.descriptors + ); + this.factories = []; this.targetIds = []; this.descriptors = {}; - } - - public release(): void { - /* eslint-disable-next-line @typescript-eslint/no-this-alias */ sharedContext = this; + + return result; } - public static borrow(directives: ReadonlyArray): CompilationContext { - const shareable = sharedContext || new CompilationContext(); - shareable.directives = directives; - shareable.reset(); + public static open(directives: ReadonlyArray): CompilationContext { + const context = sharedContext ?? new CompilationContext(); + context.directives = directives; sharedContext = null; - return shareable; + return context; } } @@ -234,16 +238,15 @@ function compileContent( return next; } - let lastNode = node; + let currentNode: Text; + let lastNode = (currentNode = node); + for (let i = 0, ii = parseResult.length; i < ii; ++i) { const currentPart = parseResult[i]; - let currentNode: Text; - if (i === 0) { - currentNode = node; - } else { + if (i !== 0) { nodeIndex++; - nodeId = `${parentId}.${nodeIndex}`; + nodeId = targetIdFrom(parentId, nodeIndex); currentNode = lastNode.parentNode!.insertBefore( document.createTextNode(""), lastNode.nextSibling @@ -270,27 +273,29 @@ function compileContent( return next; } +function compileChildren(context: CompilationContext, parent: Node, parentId: string) { + let nodeIndex = 0; + let childNode = parent.firstChild; + + while (childNode) { + const result = compileNode(context, parentId, childNode, nodeIndex); + childNode = result.node; + nodeIndex = result.index; + } +} + function compileNode( context: CompilationContext, parentId: string, node: Node, nodeIndex: number ) { - const nodeId = `${parentId}.${nodeIndex}`; + const nodeId = targetIdFrom(parentId, nodeIndex); switch (node.nodeType) { case 1: // element node compileAttributes(context, parentId, node as HTMLElement, nodeId, nodeIndex); - - let child = node.firstChild; - let childIndex = 0; - - while (child) { - const result = compileNode(context, nodeId, child, childIndex); - child = result.node; - childIndex = result.index; - } - + compileChildren(context, node, nodeId); break; case 3: // text node return compileContent(context, node as Text, parentId, nodeId, nodeIndex); @@ -377,7 +382,7 @@ export function compileTemplate( // https://bugs.chromium.org/p/chromium/issues/detail?id=1111864 document.adoptNode(fragment); - const context = CompilationContext.borrow(directives); + const context = CompilationContext.open(directives); compileAttributes(context, "", template, /* host */ "h", 0, true); if ( @@ -394,22 +399,7 @@ export function compileTemplate( fragment.insertBefore(document.createComment(""), fragment.firstChild); } - let node = fragment.firstChild; - let nodeIndex = 0; - const parentId = "r"; //root - - while (node) { - const result = compileNode(context, parentId, node, nodeIndex); - node = result.node; - nodeIndex = result.index; - } - + compileChildren(context, fragment, "r"); next.node = null; // prevent leaks - - const factories = context.factories; - const targetIds = context.targetIds; - const descriptors = context.descriptors; - context.release(); - - return new HTMLTemplateCompilationResult(fragment, factories, targetIds, descriptors); + return context.close(fragment); } diff --git a/packages/web-components/fast-element/src/templating/template.ts b/packages/web-components/fast-element/src/templating/template.ts index 4cf3724a459..347d0f998a5 100644 --- a/packages/web-components/fast-element/src/templating/template.ts +++ b/packages/web-components/fast-element/src/templating/template.ts @@ -104,15 +104,12 @@ export class ViewTemplate const result = this.result; const fragment = result.fragment.cloneNode(true) as DocumentFragment; - const factories = result.factories; - const behaviors = new Array(factories.length); - const targets = result.createTargets(fragment, hostBindingTarget); - for (let i = 0, ii = factories.length; i < ii; ++i) { - behaviors[i] = factories[i].createBehavior(targets); - } - - return new HTMLView(fragment, behaviors); + return new HTMLView( + fragment, + result.factories, + result.createTargets(fragment, hostBindingTarget) + ); } /** diff --git a/packages/web-components/fast-element/src/templating/view.ts b/packages/web-components/fast-element/src/templating/view.ts index a99a05298f0..4591f7d69ba 100644 --- a/packages/web-components/fast-element/src/templating/view.ts +++ b/packages/web-components/fast-element/src/templating/view.ts @@ -1,5 +1,6 @@ -import type { Behavior } from "../observation/behavior.js"; -import type { ExecutionContext } from "../observation/observable.js"; +import type { BehaviorTargets, NodeBehaviorFactory } from ".."; +import type { Behavior } from "../observation/behavior"; +import type { ExecutionContext } from "../observation/observable"; /** * Represents a collection of DOM nodes which can be bound to a data source. @@ -90,6 +91,8 @@ const range = document.createRange(); * @public */ export class HTMLView implements ElementView, SyntheticView { + private behaviors: Behavior[] | null = null; + /** * The data that the view is bound to. */ @@ -117,7 +120,8 @@ export class HTMLView implements ElementView, SyntheticView { */ public constructor( private fragment: DocumentFragment, - private behaviors: Behavior[] + private factories: NodeBehaviorFactory[], + private targets: BehaviorTargets ) { this.firstChild = fragment.firstChild!; this.lastChild = fragment.lastChild!; @@ -192,10 +196,13 @@ export class HTMLView implements ElementView, SyntheticView { parent.removeChild(end); const behaviors = this.behaviors; - const oldSource = this.source; - for (let i = 0, ii = behaviors.length; i < ii; ++i) { - behaviors[i].unbind(oldSource); + if (behaviors !== null) { + const oldSource = this.source; + + for (let i = 0, ii = behaviors.length; i < ii; ++i) { + behaviors[i].unbind(oldSource); + } } } @@ -205,7 +212,7 @@ export class HTMLView implements ElementView, SyntheticView { * @param context - The execution context to run the behaviors within. */ public bind(source: unknown, context: ExecutionContext): void { - const behaviors = this.behaviors; + let behaviors = this.behaviors; if (this.source === source) { return; @@ -215,8 +222,8 @@ export class HTMLView implements ElementView, SyntheticView { this.source = source; this.context = context; - for (let i = 0, ii = behaviors.length; i < ii; ++i) { - const current = behaviors[i]; + for (let i = 0, ii = behaviors!.length; i < ii; ++i) { + const current = behaviors![i]; current.unbind(oldSource); current.bind(source, context); } @@ -224,8 +231,20 @@ export class HTMLView implements ElementView, SyntheticView { this.source = source; this.context = context; - for (let i = 0, ii = behaviors.length; i < ii; ++i) { - behaviors[i].bind(source, context); + if (behaviors === null) { + this.behaviors = behaviors = new Array(this.factories.length); + const targets = this.targets; + const factories = this.factories; + + for (let i = 0, ii = factories.length; i < ii; ++i) { + const behavior = factories[i].createBehavior(targets); + behavior.bind(source, context); + behaviors[i] = behavior; + } + } else { + for (let i = 0, ii = behaviors.length; i < ii; ++i) { + behaviors[i].bind(source, context); + } } } } @@ -234,12 +253,13 @@ export class HTMLView implements ElementView, SyntheticView { * Unbinds a view's behaviors from its binding source. */ public unbind(): void { - if (this.source === null) { + const oldSource = this.source; + + if (oldSource === null) { return; } - const behaviors = this.behaviors; - const oldSource = this.source; + const behaviors = this.behaviors!; for (let i = 0, ii = behaviors.length; i < ii; ++i) { behaviors[i].unbind(oldSource); From 709cf77f1b93006777dc87e28f9118ef9ee7d1b2 Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Wed, 13 Oct 2021 11:48:44 -0400 Subject: [PATCH 006/135] chore(template): clean up imports --- .../fast-element/src/templating/template.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/web-components/fast-element/src/templating/template.ts b/packages/web-components/fast-element/src/templating/template.ts index 347d0f998a5..51565f7e5d9 100644 --- a/packages/web-components/fast-element/src/templating/template.ts +++ b/packages/web-components/fast-element/src/templating/template.ts @@ -1,15 +1,10 @@ import { DOM } from "../dom"; -import type { Behavior } from "../observation/behavior"; import { Binding, defaultExecutionContext } from "../observation/observable"; import { compileTemplate } from "./compiler"; +import type { HTMLTemplateCompilationResult } from "./compiler"; import { ElementView, HTMLView, SyntheticView } from "./view"; -import { - HTMLDirective, - NodeBehaviorFactory, - TargetedHTMLDirective, -} from "./html-directive"; +import { HTMLDirective, TargetedHTMLDirective } from "./html-directive"; import { HTMLBindingDirective } from "./binding"; -import type { HTMLTemplateCompilationResult } from ".."; /** * A template capable of creating views specifically for rendering custom elements. From b40ba561cf25ea35408e07fcb2de6ada8dda4081 Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Wed, 13 Oct 2021 11:54:51 -0400 Subject: [PATCH 007/135] chore(templating): clean up imports --- packages/web-components/fast-element/src/templating/binding.ts | 3 +-- packages/web-components/fast-element/src/templating/ref.ts | 3 +-- packages/web-components/fast-element/src/templating/repeat.ts | 3 +-- packages/web-components/fast-element/src/templating/slotted.ts | 3 +-- packages/web-components/fast-element/src/templating/view.ts | 2 +- 5 files changed, 5 insertions(+), 9 deletions(-) diff --git a/packages/web-components/fast-element/src/templating/binding.ts b/packages/web-components/fast-element/src/templating/binding.ts index a2176d51720..62cf0458b6a 100644 --- a/packages/web-components/fast-element/src/templating/binding.ts +++ b/packages/web-components/fast-element/src/templating/binding.ts @@ -1,4 +1,3 @@ -import type { BehaviorTargets } from ".."; import { DOM } from "../dom"; import type { Behavior } from "../observation/behavior"; import { @@ -8,7 +7,7 @@ import { Observable, setCurrentEvent, } from "../observation/observable"; -import { TargetedHTMLDirective } from "./html-directive"; +import { BehaviorTargets, TargetedHTMLDirective } from "./html-directive"; import type { SyntheticView } from "./view"; function normalBind( diff --git a/packages/web-components/fast-element/src/templating/ref.ts b/packages/web-components/fast-element/src/templating/ref.ts index a3d90e9db41..4371edbbdb7 100644 --- a/packages/web-components/fast-element/src/templating/ref.ts +++ b/packages/web-components/fast-element/src/templating/ref.ts @@ -1,7 +1,6 @@ import type { Behavior } from "../observation/behavior"; import type { CaptureType } from "./template"; -import { AttachedBehaviorHTMLDirective } from "./html-directive"; -import type { BehaviorTargets } from ".."; +import { AttachedBehaviorHTMLDirective, BehaviorTargets } from "./html-directive"; /** * The runtime behavior for template references. diff --git a/packages/web-components/fast-element/src/templating/repeat.ts b/packages/web-components/fast-element/src/templating/repeat.ts index 19a9c5d07d3..a2c2cccbdac 100644 --- a/packages/web-components/fast-element/src/templating/repeat.ts +++ b/packages/web-components/fast-element/src/templating/repeat.ts @@ -10,10 +10,9 @@ import { enableArrayObservation } from "../observation/array-observer"; import type { Splice } from "../observation/array-change-records"; import type { Behavior } from "../observation/behavior"; import { emptyArray } from "../platform"; -import { HTMLDirective } from "./html-directive"; +import { BehaviorTargets, HTMLDirective } from "./html-directive"; import { HTMLView, SyntheticView } from "./view"; import type { CaptureType, SyntheticViewTemplate } from "./template"; -import type { BehaviorTargets } from ".."; /** * Options for configuring repeat behavior. diff --git a/packages/web-components/fast-element/src/templating/slotted.ts b/packages/web-components/fast-element/src/templating/slotted.ts index 880e2f0cf67..7b452bd64af 100644 --- a/packages/web-components/fast-element/src/templating/slotted.ts +++ b/packages/web-components/fast-element/src/templating/slotted.ts @@ -1,5 +1,4 @@ -import type { BehaviorTargets } from ".."; -import { AttachedBehaviorHTMLDirective } from "./html-directive"; +import { AttachedBehaviorHTMLDirective, BehaviorTargets } from "./html-directive"; import { NodeBehaviorOptions, NodeObservationBehavior } from "./node-observation"; import type { CaptureType } from "./template"; diff --git a/packages/web-components/fast-element/src/templating/view.ts b/packages/web-components/fast-element/src/templating/view.ts index 4591f7d69ba..e88a8e7db39 100644 --- a/packages/web-components/fast-element/src/templating/view.ts +++ b/packages/web-components/fast-element/src/templating/view.ts @@ -1,6 +1,6 @@ -import type { BehaviorTargets, NodeBehaviorFactory } from ".."; import type { Behavior } from "../observation/behavior"; import type { ExecutionContext } from "../observation/observable"; +import type { BehaviorTargets, NodeBehaviorFactory } from "./html-directive"; /** * Represents a collection of DOM nodes which can be bound to a data source. From c9a0365d052944b8c5f2f855d0c25022822962b3 Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Wed, 13 Oct 2021 16:16:06 -0400 Subject: [PATCH 008/135] refactor(view): cleanup view internals and remove range --- .../fast-element/docs/api-report.md | 1 - .../fast-element/src/components/controller.ts | 4 +- .../web-components/fast-element/src/dom.ts | 10 -- .../fast-element/src/templating/view.ts | 92 +++++++------------ 4 files changed, 38 insertions(+), 69 deletions(-) diff --git a/packages/web-components/fast-element/docs/api-report.md b/packages/web-components/fast-element/docs/api-report.md index 88d13a0023e..15f12d527fc 100644 --- a/packages/web-components/fast-element/docs/api-report.md +++ b/packages/web-components/fast-element/docs/api-report.md @@ -215,7 +215,6 @@ export const DOM: Readonly<{ nextUpdate(): Promise; setAttribute(element: HTMLElement, attributeName: string, value: any): void; setBooleanAttribute(element: HTMLElement, attributeName: string, value: boolean): void; - removeChildNodes(parent: Node): void; }>; // @public diff --git a/packages/web-components/fast-element/src/components/controller.ts b/packages/web-components/fast-element/src/components/controller.ts index 41d1e4b3c2d..08ca4165e43 100644 --- a/packages/web-components/fast-element/src/components/controller.ts +++ b/packages/web-components/fast-element/src/components/controller.ts @@ -434,7 +434,9 @@ export class Controller extends PropertyChangeNotifier { (this as Mutable).view = null; } else if (!this.needsInitialization) { // If there was previous custom rendering, we need to clear out the host. - DOM.removeChildNodes(host); + for (let child = host.firstChild; child !== null; child = host.firstChild) { + host.removeChild(child); + } } if (template) { diff --git a/packages/web-components/fast-element/src/dom.ts b/packages/web-components/fast-element/src/dom.ts index b458cdbf5b4..db2cdbcef3c 100644 --- a/packages/web-components/fast-element/src/dom.ts +++ b/packages/web-components/fast-element/src/dom.ts @@ -224,14 +224,4 @@ export const DOM = Object.freeze({ ? element.setAttribute(attributeName, "") : element.removeAttribute(attributeName); }, - - /** - * Removes all the child nodes of the provided parent node. - * @param parent - The node to remove the children from. - */ - removeChildNodes(parent: Node) { - for (let child = parent.firstChild; child !== null; child = parent.firstChild) { - parent.removeChild(child); - } - }, }); diff --git a/packages/web-components/fast-element/src/templating/view.ts b/packages/web-components/fast-element/src/templating/view.ts index e88a8e7db39..811d5738f55 100644 --- a/packages/web-components/fast-element/src/templating/view.ts +++ b/packages/web-components/fast-element/src/templating/view.ts @@ -82,9 +82,19 @@ export interface SyntheticView extends View { dispose(): void; } -// A singleton Range instance used to efficiently remove ranges of DOM nodes. -// See the implementation of HTMLView below for further details. -const range = document.createRange(); +function removeNodeSequence(firstNode: Node, lastNode: Node) { + const parent = firstNode.parentNode!; + let current = firstNode; + let next: ChildNode | null; + + while (current !== lastNode) { + next = current.nextSibling; + parent.removeChild(current); + current = next!; + } + + parent.removeChild(lastNode); +} /** * The standard View implementation, which also implements ElementView and SyntheticView. @@ -182,28 +192,8 @@ export class HTMLView implements ElementView, SyntheticView { * Once a view has been disposed, it cannot be inserted or bound again. */ public dispose(): void { - const parent = this.firstChild.parentNode!; - const end = this.lastChild!; - let current = this.firstChild!; - let next; - - while (current !== end) { - next = current.nextSibling; - parent.removeChild(current); - current = next!; - } - - parent.removeChild(end); - - const behaviors = this.behaviors; - - if (behaviors !== null) { - const oldSource = this.source; - - for (let i = 0, ii = behaviors.length; i < ii; ++i) { - behaviors[i].unbind(oldSource); - } - } + removeNodeSequence(this.firstChild, this.lastChild); + this.unbind(); } /** @@ -213,38 +203,34 @@ export class HTMLView implements ElementView, SyntheticView { */ public bind(source: unknown, context: ExecutionContext): void { let behaviors = this.behaviors; + const oldSource = this.source; - if (this.source === source) { + if (oldSource === source) { return; - } else if (this.source !== null) { - const oldSource = this.source; + } - this.source = source; - this.context = context; + this.source = source; + this.context = context; + if (oldSource !== null) { for (let i = 0, ii = behaviors!.length; i < ii; ++i) { const current = behaviors![i]; current.unbind(oldSource); current.bind(source, context); } + } else if (behaviors === null) { + this.behaviors = behaviors = new Array(this.factories.length); + const targets = this.targets; + const factories = this.factories; + + for (let i = 0, ii = factories.length; i < ii; ++i) { + const behavior = factories[i].createBehavior(targets); + behavior.bind(source, context); + behaviors[i] = behavior; + } } else { - this.source = source; - this.context = context; - - if (behaviors === null) { - this.behaviors = behaviors = new Array(this.factories.length); - const targets = this.targets; - const factories = this.factories; - - for (let i = 0, ii = factories.length; i < ii; ++i) { - const behavior = factories[i].createBehavior(targets); - behavior.bind(source, context); - behaviors[i] = behavior; - } - } else { - for (let i = 0, ii = behaviors.length; i < ii; ++i) { - behaviors[i].bind(source, context); - } + for (let i = 0, ii = behaviors.length; i < ii; ++i) { + behaviors[i].bind(source, context); } } } @@ -277,18 +263,10 @@ export class HTMLView implements ElementView, SyntheticView { return; } - range.setStartBefore(views[0].firstChild); - range.setEndAfter(views[views.length - 1].lastChild); - range.deleteContents(); + removeNodeSequence(views[0].firstChild, views[views.length - 1].lastChild); for (let i = 0, ii = views.length; i < ii; ++i) { - const view = views[i] as any; - const behaviors = view.behaviors as Behavior[]; - const oldSource = view.source; - - for (let j = 0, jj = behaviors.length; j < jj; ++j) { - behaviors[j].unbind(oldSource); - } + views[i].unbind(); } } } From 38e7d5f43b0e4159b2672778427922ed2d1a422b Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Wed, 13 Oct 2021 17:10:11 -0400 Subject: [PATCH 009/135] feat(binding): enable control over className vs. classList behavior --- .../fast-element/docs/api-report.md | 2 + .../fast-element/src/templating/binding.ts | 41 +++++++++++++++---- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/packages/web-components/fast-element/docs/api-report.md b/packages/web-components/fast-element/docs/api-report.md index 15f12d527fc..21276fff559 100644 --- a/packages/web-components/fast-element/docs/api-report.md +++ b/packages/web-components/fast-element/docs/api-report.md @@ -103,6 +103,8 @@ export class BindingBehavior implements Behavior { // @internal (undocumented) updateTarget: typeof updatePropertyTarget; // @internal (undocumented) + value: any; + // @internal (undocumented) version: number; } diff --git a/packages/web-components/fast-element/src/templating/binding.ts b/packages/web-components/fast-element/src/templating/binding.ts index 62cf0458b6a..054c0aee4de 100644 --- a/packages/web-components/fast-element/src/templating/binding.ts +++ b/packages/web-components/fast-element/src/templating/binding.ts @@ -26,7 +26,7 @@ function normalBind( ); } - this.updateTarget(this.bindingObserver!.observe(source, context)); + this.handleChange(); } function triggerBind( @@ -43,6 +43,7 @@ function normalUnbind(this: BindingBehavior): void { this.bindingObserver!.disconnect(); this.source = null; this.context = null; + this.value = null; } type ComposableView = SyntheticView & { @@ -51,9 +52,7 @@ type ComposableView = SyntheticView & { }; function contentUnbind(this: BindingBehavior): void { - this.bindingObserver!.disconnect(); - this.source = null; - this.context = null; + normalUnbind.call(this); const view = this.target.$fastView as ComposableView; @@ -145,6 +144,10 @@ function updatePropertyTarget(this: BindingBehavior, value: unknown): void { } function updateClassTarget(this: BindingBehavior, value: string): void { + this.target.className = value; +} + +function updateClassListTarget(this: BindingBehavior, value: string): void { const classVersions = this.classVersions || Object.create(null); const target = this.target; let version = this.version || 0; @@ -222,10 +225,22 @@ export class HTMLBindingDirective extends TargetedHTMLDirective { switch (value[0]) { case ":": this.cleanedTargetName = value.substr(1); - this.updateTarget = updatePropertyTarget; - if (this.cleanedTargetName === "innerHTML") { - const binding = this.binding; - this.binding = (s, c) => DOM.createHTML(binding(s, c)); + switch (this.cleanedTargetName) { + case "innerHTML": + const binding = this.binding; + /* eslint-disable-next-line */ + this.binding = (s, c) => DOM.createHTML(binding(s, c)); + this.updateTarget = updatePropertyTarget; + break; + case "classList": + this.updateTarget = updateClassListTarget; + break; + case "className": + this.updateTarget = updateClassTarget; + break; + default: + this.updateTarget = updatePropertyTarget; + break; } break; case "?": @@ -282,6 +297,9 @@ export class HTMLBindingDirective extends TargetedHTMLDirective { * @public */ export class BindingBehavior implements Behavior { + /** @internal */ + public value: any = null; + /** @internal */ public source: unknown = null; @@ -355,7 +373,12 @@ export class BindingBehavior implements Behavior { /** @internal */ public handleChange(): void { - this.updateTarget(this.bindingObserver!.observe(this.source, this.context!)); + const newValue = this.bindingObserver!.observe(this.source, this.context!); + + if (this.value !== newValue) { + this.value = newValue; + this.updateTarget(newValue); + } } /** @internal */ From ba8e1c0ee9cafaf7c06116bc8e1598b7ce7ab20c Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Wed, 13 Oct 2021 17:20:22 -0400 Subject: [PATCH 010/135] docs: add notes on changes in FAST Element 2 --- .../fast-element/docs/fast-element-2-changes.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 packages/web-components/fast-element/docs/fast-element-2-changes.md diff --git a/packages/web-components/fast-element/docs/fast-element-2-changes.md b/packages/web-components/fast-element/docs/fast-element-2-changes.md new file mode 100644 index 00000000000..a4fb9e96b0c --- /dev/null +++ b/packages/web-components/fast-element/docs/fast-element-2-changes.md @@ -0,0 +1,9 @@ +# Changes in FASTElement 2.0 + +## Breaking Changes + +* `HTMLDirective` - The `targetIndex: number` property has been replaced by a `targetId: string` property. The `createBehavior` method no longer takes a target `Node` but instead takes a `BehaviorTargets` instance. The actual target can be looked up on the `BehaviorTargets` instance by indexing with the `targetId` property. +* `compileTemplate()` - Internals have been significantly changed. The implementation no longer uses a TreeWalker. The return type has change to an `HTMLTemplateCompilationResult` with different properties. +* `HTMLView` - The constructor has a new signature based on changes to the compiler's output. Internals have been cleaned up and no longer rely on the Range type. +* `DOM` - Tree Walker methods are no longer used and are thus removed. The API for removing child nodes has been removed as well since it was only used in one place and could be inlined. +* `class` - Bindings to `class` are now more nuanced. Binding directly to `class` will simply set the `className` property. If you need to bind to `class` knowing that manual JS will also manipulate the `classList` in addition to the binding, then you should now bind to `:classList` instead. This allows for performance optimizations in the simple, most common case. \ No newline at end of file From c5aed2e421f57368cba019aa607f95ea6c0debc8 Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Thu, 14 Oct 2021 10:10:57 -0400 Subject: [PATCH 011/135] feat: add generic types to views and templates --- .../fast-element/docs/api-report.md | 40 +++++++++---------- .../docs/fast-element-2-changes.md | 3 +- .../fast-element/src/templating/template.ts | 40 ++++++++++--------- .../fast-element/src/templating/view.ts | 25 +++++++----- 4 files changed, 59 insertions(+), 49 deletions(-) diff --git a/packages/web-components/fast-element/docs/api-report.md b/packages/web-components/fast-element/docs/api-report.md index 21276fff559..b053e8cb05e 100644 --- a/packages/web-components/fast-element/docs/api-report.md +++ b/packages/web-components/fast-element/docs/api-report.md @@ -245,14 +245,14 @@ export abstract class ElementStyles { } // @public -export interface ElementView extends View { +export interface ElementView extends View { appendTo(node: Node): void; } // @public -export interface ElementViewTemplate { - create(hostBindingTarget: Element): ElementView; - render(source: any, host: Node, hostBindingTarget?: Element): HTMLView; +export interface ElementViewTemplate { + create(hostBindingTarget: Element): ElementView; + render(source: TSource, host: Node, hostBindingTarget?: Element): HTMLView; } // Warning: (ae-internal-missing-underscore) The name "emptyArray" should be prefixed with an underscore because the declaration is marked as @internal @@ -336,7 +336,7 @@ export type Global = typeof globalThis & { }; // @public -export function html(strings: TemplateStringsArray, ...values: TemplateValue[]): ViewTemplate; +export function html(strings: TemplateStringsArray, ...values: TemplateValue[]): ViewTemplate; // @public export class HTMLBindingDirective extends TargetedHTMLDirective { @@ -367,18 +367,18 @@ export class HTMLTemplateCompilationResult { } // @public -export class HTMLView implements ElementView, SyntheticView { +export class HTMLView implements ElementView, SyntheticView { constructor(fragment: DocumentFragment, factories: NodeBehaviorFactory[], targets: BehaviorTargets); appendTo(node: Node): void; - bind(source: unknown, context: ExecutionContext): void; - context: ExecutionContext | null; + bind(source: TSource, context: ExecutionContext): void; + context: ExecutionContext | null; dispose(): void; static disposeContiguousBatch(views: SyntheticView[]): void; firstChild: Node; insertBefore(node: Node): void; lastChild: Node; remove(): void; - source: any | null; + source: TSource | null; unbind(): void; } @@ -556,7 +556,7 @@ export interface SubtreeBehaviorOptions extends Omit extends View { dispose(): void; readonly firstChild: Node; insertBefore(node: Node): void; @@ -565,8 +565,8 @@ export interface SyntheticView extends View { } // @public -export interface SyntheticViewTemplate { - create(): SyntheticView; +export interface SyntheticViewTemplate { + create(): SyntheticView; } // @public @@ -576,7 +576,7 @@ export abstract class TargetedHTMLDirective extends HTMLDirective { } // @public -export type TemplateValue = Binding | string | number | HTMLDirective | CaptureType; +export type TemplateValue = Binding | string | number | HTMLDirective | CaptureType; // @public export type TrustedTypes = { @@ -595,21 +595,21 @@ export interface ValueConverter { } // @public -export interface View { - bind(source: unknown, context: ExecutionContext): void; - readonly context: ExecutionContext | null; +export interface View { + bind(source: TSource, context: ExecutionContext): void; + readonly context: ExecutionContext | null; dispose(): void; - readonly source: any | null; + readonly source: TSource | null; unbind(): void; } // @public -export class ViewTemplate implements ElementViewTemplate, SyntheticViewTemplate { +export class ViewTemplate implements ElementViewTemplate, SyntheticViewTemplate { constructor(html: string | HTMLTemplateElement, directives: ReadonlyArray); - create(hostBindingTarget?: Element): HTMLView; + create(hostBindingTarget?: Element): HTMLView; readonly directives: ReadonlyArray; readonly html: string | HTMLTemplateElement; - render(source: TSource, host: Node | string, hostBindingTarget?: Element): HTMLView; + render(source: TSource, host: Node | string, hostBindingTarget?: Element): HTMLView; } // @public diff --git a/packages/web-components/fast-element/docs/fast-element-2-changes.md b/packages/web-components/fast-element/docs/fast-element-2-changes.md index a4fb9e96b0c..0c69a7bcc72 100644 --- a/packages/web-components/fast-element/docs/fast-element-2-changes.md +++ b/packages/web-components/fast-element/docs/fast-element-2-changes.md @@ -4,6 +4,7 @@ * `HTMLDirective` - The `targetIndex: number` property has been replaced by a `targetId: string` property. The `createBehavior` method no longer takes a target `Node` but instead takes a `BehaviorTargets` instance. The actual target can be looked up on the `BehaviorTargets` instance by indexing with the `targetId` property. * `compileTemplate()` - Internals have been significantly changed. The implementation no longer uses a TreeWalker. The return type has change to an `HTMLTemplateCompilationResult` with different properties. -* `HTMLView` - The constructor has a new signature based on changes to the compiler's output. Internals have been cleaned up and no longer rely on the Range type. +* `View` and `HTMLView` - Type parameters added to enable strongly typed views based on their data source. The constructor of `HTMLView` has a new signature based on changes to the compiler's output. Internals have been cleaned up and no longer rely on the Range type. +* `ElementViewTemplate`, `SyntheticViewTemplate`, and `ViewTemplate` - Added type parameters throughout. Logic to instantiate and apply behaviors moved out of the template and into the view where it can be lazily executed. * `DOM` - Tree Walker methods are no longer used and are thus removed. The API for removing child nodes has been removed as well since it was only used in one place and could be inlined. * `class` - Bindings to `class` are now more nuanced. Binding directly to `class` will simply set the `className` property. If you need to bind to `class` knowing that manual JS will also manipulate the `classList` in addition to the binding, then you should now bind to `:classList` instead. This allows for performance optimizations in the simple, most common case. \ No newline at end of file diff --git a/packages/web-components/fast-element/src/templating/template.ts b/packages/web-components/fast-element/src/templating/template.ts index 51565f7e5d9..9a44ea872ba 100644 --- a/packages/web-components/fast-element/src/templating/template.ts +++ b/packages/web-components/fast-element/src/templating/template.ts @@ -10,12 +10,12 @@ import { HTMLBindingDirective } from "./binding"; * A template capable of creating views specifically for rendering custom elements. * @public */ -export interface ElementViewTemplate { +export interface ElementViewTemplate { /** * Creates an ElementView instance based on this template definition. * @param hostBindingTarget - The element that host behaviors will be bound to. */ - create(hostBindingTarget: Element): ElementView; + create(hostBindingTarget: Element): ElementView; /** * Creates an HTMLView from this template, binds it to the source, and then appends it to the host. @@ -24,28 +24,32 @@ export interface ElementViewTemplate { * @param hostBindingTarget - An HTML element to target the host bindings at if different from the * host that the template is being attached to. */ - render(source: any, host: Node, hostBindingTarget?: Element): HTMLView; + render( + source: TSource, + host: Node, + hostBindingTarget?: Element + ): HTMLView; } /** * A template capable of rendering views not specifically connected to custom elements. * @public */ -/* eslint-disable-next-line @typescript-eslint/no-unused-vars */ -export interface SyntheticViewTemplate { +export interface SyntheticViewTemplate { /** * Creates a SyntheticView instance based on this template definition. */ - create(): SyntheticView; + create(): SyntheticView; } /** * A template capable of creating HTMLView instances or rendering directly to DOM. * @public */ -/* eslint-disable-next-line @typescript-eslint/no-unused-vars */ -export class ViewTemplate - implements ElementViewTemplate, SyntheticViewTemplate { +export class ViewTemplate + implements + ElementViewTemplate, + SyntheticViewTemplate { private result: HTMLTemplateCompilationResult | null = null; /** @@ -76,7 +80,7 @@ export class ViewTemplate * Creates an HTMLView instance based on this template definition. * @param hostBindingTarget - The element that host behaviors will be bound to. */ - public create(hostBindingTarget?: Element): HTMLView { + public create(hostBindingTarget?: Element): HTMLView { if (this.result === null) { let template: HTMLTemplateElement; const html = this.html; @@ -100,7 +104,7 @@ export class ViewTemplate const result = this.result; const fragment = result.fragment.cloneNode(true) as DocumentFragment; - return new HTMLView( + return new HTMLView( fragment, result.factories, result.createTargets(fragment, hostBindingTarget) @@ -118,7 +122,7 @@ export class ViewTemplate source: TSource, host: Node | string, hostBindingTarget?: Element - ): HTMLView { + ): HTMLView { if (typeof host === "string") { host = document.getElementById(host)!; } @@ -152,12 +156,12 @@ export interface CaptureType {} * Represents the types of values that can be interpolated into a template. * @public */ -export type TemplateValue = - | Binding +export type TemplateValue = + | Binding | string | number | HTMLDirective - | CaptureType; + | CaptureType; /** * Transforms a template literal string into a renderable ViewTemplate. @@ -168,10 +172,10 @@ export type TemplateValue = * other template instances, and Directive instances. * @public */ -export function html( +export function html( strings: TemplateStringsArray, ...values: TemplateValue[] -): ViewTemplate { +): ViewTemplate { const directives: HTMLDirective[] = []; let html = ""; @@ -210,5 +214,5 @@ export function html( html += strings[strings.length - 1]; - return new ViewTemplate(html, directives); + return new ViewTemplate(html, directives); } diff --git a/packages/web-components/fast-element/src/templating/view.ts b/packages/web-components/fast-element/src/templating/view.ts index 811d5738f55..09d02257cb6 100644 --- a/packages/web-components/fast-element/src/templating/view.ts +++ b/packages/web-components/fast-element/src/templating/view.ts @@ -6,23 +6,23 @@ import type { BehaviorTargets, NodeBehaviorFactory } from "./html-directive"; * Represents a collection of DOM nodes which can be bound to a data source. * @public */ -export interface View { +export interface View { /** * The execution context the view is running within. */ - readonly context: ExecutionContext | null; + readonly context: ExecutionContext | null; /** * The data that the view is bound to. */ - readonly source: any | null; + readonly source: TSource | null; /** * Binds a view's behaviors to its binding source. * @param source - The binding source for the view's binding behaviors. * @param context - The execution context to run the view within. */ - bind(source: unknown, context: ExecutionContext): void; + bind(source: TSource, context: ExecutionContext): void; /** * Unbinds a view's behaviors from its binding source and context. @@ -40,7 +40,8 @@ export interface View { * A View representing DOM nodes specifically for rendering the view of a custom element. * @public */ -export interface ElementView extends View { +export interface ElementView + extends View { /** * Appends the view's DOM nodes to the referenced node. * @param node - The parent node to append the view's DOM nodes to. @@ -52,7 +53,8 @@ export interface ElementView extends View { * A view representing a range of DOM nodes which can be added/removed ad hoc. * @public */ -export interface SyntheticView extends View { +export interface SyntheticView + extends View { /** * The first DOM node in the range of nodes that make up the view. */ @@ -100,18 +102,21 @@ function removeNodeSequence(firstNode: Node, lastNode: Node) { * The standard View implementation, which also implements ElementView and SyntheticView. * @public */ -export class HTMLView implements ElementView, SyntheticView { +export class HTMLView + implements + ElementView, + SyntheticView { private behaviors: Behavior[] | null = null; /** * The data that the view is bound to. */ - public source: any | null = null; + public source: TSource | null = null; /** * The execution context the view is running within. */ - public context: ExecutionContext | null = null; + public context: ExecutionContext | null = null; /** * The first DOM node in the range of nodes that make up the view. @@ -201,7 +206,7 @@ export class HTMLView implements ElementView, SyntheticView { * @param source - The binding source for the view's binding behaviors. * @param context - The execution context to run the behaviors within. */ - public bind(source: unknown, context: ExecutionContext): void { + public bind(source: TSource, context: ExecutionContext): void { let behaviors = this.behaviors; const oldSource = this.source; From 36622336aad1ab449e60c027f32cca3b698eaeaa Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Thu, 14 Oct 2021 11:09:19 -0400 Subject: [PATCH 012/135] refactor: internal cleanup and fixing router to match new APIs. --- .../fast-element/docs/api-report.md | 51 ++----- .../web-components/fast-element/src/dom.ts | 144 ++++++++---------- .../fast-router/src/commands.ts | 9 +- .../fast-router/src/contributors.ts | 9 +- 4 files changed, 89 insertions(+), 124 deletions(-) diff --git a/packages/web-components/fast-element/docs/api-report.md b/packages/web-components/fast-element/docs/api-report.md index b053e8cb05e..f30a13a791f 100644 --- a/packages/web-components/fast-element/docs/api-report.md +++ b/packages/web-components/fast-element/docs/api-report.md @@ -111,7 +111,7 @@ export class BindingBehavior implements Behavior { // @public export interface BindingObserver extends Notifier { disconnect(): void; - observe(source: TSource, context: ExecutionContext): TReturn; + observe(source: TSource, context: ExecutionContext): TReturn; records(): IterableIterator; } @@ -212,9 +212,9 @@ export const DOM: Readonly<{ createInterpolationPlaceholder(index: number): string; createCustomAttributePlaceholder(attributeName: string, index: number): string; createBlockPlaceholder(index: number): string; - queueUpdate: (callable: Callable) => void; - processUpdates: () => void; + queueUpdate(callable: Callable): void; nextUpdate(): Promise; + processUpdates(): void; setAttribute(element: HTMLElement, attributeName: string, value: any): void; setBooleanAttribute(element: HTMLElement, attributeName: string, value: boolean): void; }>; @@ -275,15 +275,8 @@ export class ExecutionContext { length: number; parent: TParent; parentContext: ExecutionContext; - // @internal - static setEvent(event: Event | null): void; } -// Warning: (ae-internal-missing-underscore) The name "FAST" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal -export const FAST: FASTGlobal; - // @public export interface FASTElement { $emit(type: string, detail?: any, options?: Omit): boolean | void; @@ -309,8 +302,8 @@ export class FASTElementDefinition { readonly attributes: ReadonlyArray; define(registry?: CustomElementRegistry): this; readonly elementOptions?: ElementDefinitionOptions; - static readonly forType: (key: TType_1) => FASTElementDefinition | undefined; - get isDefined(): boolean; + static forType(type: TType): FASTElementDefinition | undefined; + readonly isDefined: boolean; readonly name: string; readonly propertyLookup: Record; readonly shadowOptions?: ShadowRootInit; @@ -319,20 +312,9 @@ export class FASTElementDefinition { readonly type: TType; } -// Warning: (ae-internal-missing-underscore) The name "FASTGlobal" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal -export interface FASTGlobal { - getById(id: string | number): T | null; - // (undocumented) - getById(id: string | number, initialize: () => T): T; - readonly versions: string[]; -} - // @public export type Global = typeof globalThis & { trustedTypes: TrustedTypes; - readonly FAST: FASTGlobal; }; // @public @@ -382,20 +364,6 @@ export class HTMLView implemen unbind(): void; } -// Warning: (ae-internal-missing-underscore) The name "KernelServiceId" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal -export const enum KernelServiceId { - // (undocumented) - contextEvent = 3, - // (undocumented) - elementRegistry = 4, - // (undocumented) - observable = 2, - // (undocumented) - updateQueue = 1 -} - // Warning: (ae-internal-missing-underscore) The name "Mutable" should be prefixed with an underscore because the declaration is marked as @internal // // @internal @@ -429,12 +397,12 @@ export const nullableNumberConverter: ValueConverter; // @public export const Observable: Readonly<{ setArrayObserverFactory(factory: (collection: any[]) => Notifier): void; - getNotifier: (source: any) => Notifier; + getNotifier(source: any): Notifier; track(source: unknown, propertyName: string): void; trackVolatile(): void; notify(source: unknown, args: any): void; defineProperty(target: {}, nameOrAccessor: string | Accessor): void; - getAccessors: (target: {}) => Accessor[]; + getAccessors(target: {}): Accessor[]; binding(binding: Binding, initialSubscriber?: Subscriber | undefined, isVolatileBinding?: boolean): BindingObserver; isVolatileBinding(binding: Binding): boolean; }>; @@ -502,6 +470,11 @@ export interface RepeatOptions { recycle?: boolean; } +// Warning: (ae-internal-missing-underscore) The name "setCurrentEvent" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export function setCurrentEvent(event: Event | null): void; + // @public export function slotted(propertyOrOptions: (keyof T & string) | SlottedBehaviorOptions): CaptureType; diff --git a/packages/web-components/fast-element/src/dom.ts b/packages/web-components/fast-element/src/dom.ts index db2cdbcef3c..9f583f95aec 100644 --- a/packages/web-components/fast-element/src/dom.ts +++ b/packages/web-components/fast-element/src/dom.ts @@ -1,70 +1,5 @@ -import type { Callable } from "./interfaces.js"; -import { $global, KernelServiceId, TrustedTypesPolicy } from "./platform.js"; - -const updateQueue = $global.FAST.getById(KernelServiceId.updateQueue, () => { - const tasks = [] as Callable[]; - const pendingErrors: any[] = []; - - function throwFirstError(): void { - if (pendingErrors.length) { - throw pendingErrors.shift(); - } - } - - function tryRunTask(task: Callable): void { - try { - (task as any).call(); - } catch (error) { - pendingErrors.push(error); - setTimeout(throwFirstError, 0); - } - } - - function process(): void { - const capacity = 1024; - let index = 0; - - while (index < tasks.length) { - tryRunTask(tasks[index]); - index++; - - // Prevent leaking memory for long chains of recursive calls to `DOM.queueUpdate`. - // If we call `DOM.queueUpdate` within a task scheduled by `DOM.queueUpdate`, the queue will - // grow, but to avoid an O(n) walk for every task we execute, we don't - // shift tasks off the queue after they have been executed. - // Instead, we periodically shift 1024 tasks off the queue. - if (index > capacity) { - // Manually shift all values starting at the index back to the - // beginning of the queue. - for ( - let scan = 0, newLength = tasks.length - index; - scan < newLength; - scan++ - ) { - tasks[scan] = tasks[scan + index]; - } - - tasks.length -= index; - index = 0; - } - } - - tasks.length = 0; - } - - function enqueue(callable: Callable): void { - if (tasks.length < 1) { - $global.requestAnimationFrame(process); - } - - tasks.push(callable); - } - - return Object.freeze({ - enqueue, - process, - }); -}); +import type { Callable } from "./interfaces"; +import { $global, TrustedTypesPolicy } from "./platform"; /* eslint-disable */ const fastHTMLPolicy: TrustedTypesPolicy = $global.trustedTypes.createPolicy( @@ -76,6 +11,23 @@ const fastHTMLPolicy: TrustedTypesPolicy = $global.trustedTypes.createPolicy( /* eslint-enable */ let htmlPolicy: TrustedTypesPolicy = fastHTMLPolicy; +const updateQueue: Callable[] = []; +const pendingErrors: any[] = []; + +function throwFirstError(): void { + if (pendingErrors.length) { + throw pendingErrors.shift(); + } +} + +function tryRunTask(task: Callable): void { + try { + (task as any).call(); + } catch (error) { + pendingErrors.push(error); + setTimeout(throwFirstError, 0); + } +} const marker = `fast-${Math.random().toString(36).substring(2, 8)}`; @@ -176,7 +128,20 @@ export const DOM = Object.freeze({ * Schedules DOM update work in the next async batch. * @param callable - The callable function or object to queue. */ - queueUpdate: updateQueue.enqueue, + queueUpdate(callable: Callable) { + if (updateQueue.length < 1) { + $global.requestAnimationFrame(DOM.processUpdates); + } + + updateQueue.push(callable); + }, + + /** + * Resolves with the next DOM update. + */ + nextUpdate(): Promise { + return new Promise(DOM.queueUpdate); + }, /** * Immediately processes all work previously scheduled @@ -185,13 +150,36 @@ export const DOM = Object.freeze({ * This also forces nextUpdate promises * to resolve. */ - processUpdates: updateQueue.process, + processUpdates(): void { + const capacity = 1024; + let index = 0; - /** - * Resolves with the next DOM update. - */ - nextUpdate(): Promise { - return new Promise(updateQueue.enqueue); + while (index < updateQueue.length) { + tryRunTask(updateQueue[index]); + index++; + + // Prevent leaking memory for long chains of recursive calls to `DOM.queueUpdate`. + // If we call `DOM.queueUpdate` within a task scheduled by `DOM.queueUpdate`, the queue will + // grow, but to avoid an O(n) walk for every task we execute, we don't + // shift tasks off the queue after they have been executed. + // Instead, we periodically shift 1024 tasks off the queue. + if (index > capacity) { + // Manually shift all values starting at the index back to the + // beginning of the queue. + for ( + let scan = 0, newLength = updateQueue.length - index; + scan < newLength; + scan++ + ) { + updateQueue[scan] = updateQueue[scan + index]; + } + + updateQueue.length -= index; + index = 0; + } + } + + updateQueue.length = 0; }, /** @@ -204,11 +192,9 @@ export const DOM = Object.freeze({ * it is set to the provided value using the standard `setAttribute` API. */ setAttribute(element: HTMLElement, attributeName: string, value: any) { - if (value === null || value === undefined) { - element.removeAttribute(attributeName); - } else { - element.setAttribute(attributeName, value); - } + value === null || value === undefined + ? element.removeAttribute(attributeName) + : element.setAttribute(attributeName, value); }, /** diff --git a/packages/web-components/fast-router/src/commands.ts b/packages/web-components/fast-router/src/commands.ts index f80e7495235..5add86956d1 100644 --- a/packages/web-components/fast-router/src/commands.ts +++ b/packages/web-components/fast-router/src/commands.ts @@ -90,9 +90,12 @@ function factoryFromElementInstance(element: HTMLElement): ViewFactory { const fragment = document.createDocumentFragment(); fragment.appendChild(element); - const view = new HTMLView(fragment, [ - navigationContributor().createBehavior(element), - ]); + const factory = navigationContributor(); + factory.targetId = "h"; + + const view = new HTMLView(fragment, [factory], { + [factory.targetId]: element, + }); return { create() { diff --git a/packages/web-components/fast-router/src/contributors.ts b/packages/web-components/fast-router/src/contributors.ts index 9a1db47381e..9d5c77b695d 100644 --- a/packages/web-components/fast-router/src/contributors.ts +++ b/packages/web-components/fast-router/src/contributors.ts @@ -1,4 +1,4 @@ -import { Behavior, DOM, HTMLDirective } from "@microsoft/fast-element"; +import { Behavior, HTMLDirective, DOM, BehaviorTargets } from "@microsoft/fast-element"; import { NavigationCommitPhaseHook, NavigationPhaseHook, @@ -48,8 +48,11 @@ class NavigationContributorDirective extends HTMLDirective { return DOM.createCustomAttributePlaceholder("fast-navigation-contributor", index); } - createBehavior(target: HTMLElement) { - return new NavigationContributorBehavior(target, this.options); + createBehavior(targets: BehaviorTargets) { + return new NavigationContributorBehavior( + targets[this.targetId] as HTMLElement & NavigationContributor, + this.options + ); } } From 56a14ee13219dd8b4aab00b44d46d60ffd8bbeba Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Thu, 14 Oct 2021 12:40:48 -0400 Subject: [PATCH 013/135] feat: improve types related to behaviors --- .../fast-element/docs/api-report.md | 18 +++++----- .../fast-element/src/components/controller.ts | 10 +++--- .../src/components/fast-element.ts | 4 +-- .../fast-element/src/observation/behavior.ts | 6 ++-- .../fast-element/src/styles/css-directive.ts | 2 +- .../fast-element/src/styles/css.ts | 10 +++--- .../fast-element/src/styles/element-styles.ts | 34 +++++++++++-------- .../fast-element/src/styles/styles.spec.ts | 7 ++-- 8 files changed, 49 insertions(+), 42 deletions(-) diff --git a/packages/web-components/fast-element/docs/api-report.md b/packages/web-components/fast-element/docs/api-report.md index f30a13a791f..3e1374ddbaf 100644 --- a/packages/web-components/fast-element/docs/api-report.md +++ b/packages/web-components/fast-element/docs/api-report.md @@ -58,9 +58,9 @@ export class AttributeDefinition implements Accessor { export type AttributeMode = "reflect" | "boolean" | "fromView"; // @public -export interface Behavior { - bind(source: unknown, context: ExecutionContext): void; - unbind(source: unknown): void; +export interface Behavior { + bind(source: TSource, context: ExecutionContext): void; + unbind(source: TSource): void; } // @public @@ -162,7 +162,7 @@ export type Constructable = { export class Controller extends PropertyChangeNotifier { // @internal constructor(element: HTMLElement, definition: FASTElementDefinition); - addBehaviors(behaviors: ReadonlyArray): void; + addBehaviors(behaviors: ReadonlyArray>): void; addStyles(styles: ElementStyles | HTMLStyleElement): void; readonly definition: FASTElementDefinition; readonly element: HTMLElement; @@ -172,7 +172,7 @@ export class Controller extends PropertyChangeNotifier { onAttributeChangedCallback(name: string, oldValue: string, newValue: string): void; onConnectedCallback(): void; onDisconnectedCallback(): void; - removeBehaviors(behaviors: ReadonlyArray, force?: boolean): void; + removeBehaviors(behaviors: ReadonlyArray>, force?: boolean): void; removeStyles(styles: ElementStyles | HTMLStyleElement): void; get styles(): ElementStyles | null; set styles(value: ElementStyles | null); @@ -186,7 +186,7 @@ export function css(strings: TemplateStringsArray, ...values: (ComposableStyles // @public export class CSSDirective { - createBehavior(): Behavior | undefined; + createBehavior(): Behavior | undefined; createCSS(): ComposableStyles; } @@ -233,7 +233,7 @@ export abstract class ElementStyles { // @internal (undocumented) addStylesTo(target: StyleTarget): void; // @internal (undocumented) - abstract readonly behaviors: ReadonlyArray | null; + abstract readonly behaviors: ReadonlyArray> | null; static readonly create: ElementStyleFactory; // @internal (undocumented) isAttachedTo(target: StyleTarget): boolean; @@ -241,7 +241,7 @@ export abstract class ElementStyles { removeStylesFrom(target: StyleTarget): void; // @internal (undocumented) abstract readonly styles: ReadonlyArray; - withBehaviors(...behaviors: Behavior[]): this; + withBehaviors(...behaviors: Behavior[]): this; } // @public @@ -278,7 +278,7 @@ export class ExecutionContext { } // @public -export interface FASTElement { +export interface FASTElement extends HTMLElement { $emit(type: string, detail?: any, options?: Omit): boolean | void; readonly $fastController: Controller; attributeChangedCallback(name: string, oldValue: string, newValue: string): void; diff --git a/packages/web-components/fast-element/src/components/controller.ts b/packages/web-components/fast-element/src/components/controller.ts index 08ca4165e43..357319d15f7 100644 --- a/packages/web-components/fast-element/src/components/controller.ts +++ b/packages/web-components/fast-element/src/components/controller.ts @@ -25,7 +25,7 @@ function getShadowRoot(element: HTMLElement): ShadowRoot | null { */ export class Controller extends PropertyChangeNotifier { private boundObservables: Record | null = null; - private behaviors: Map | null = null; + private behaviors: Map, number> | null = null; private needsInitialization: boolean = true; private _template: ElementViewTemplate | null = null; private _styles: ElementStyles | null = null; @@ -209,10 +209,10 @@ export class Controller extends PropertyChangeNotifier { * Adds behaviors to this element. * @param behaviors - The behaviors to add. */ - public addBehaviors(behaviors: ReadonlyArray): void { + public addBehaviors(behaviors: ReadonlyArray>): void { const targetBehaviors = this.behaviors || (this.behaviors = new Map()); const length = behaviors.length; - const behaviorsToBind: Behavior[] = []; + const behaviorsToBind: Behavior[] = []; for (let i = 0; i < length; ++i) { const behavior = behaviors[i]; @@ -240,7 +240,7 @@ export class Controller extends PropertyChangeNotifier { * @param force - Forces unbinding of behaviors. */ public removeBehaviors( - behaviors: ReadonlyArray, + behaviors: ReadonlyArray>, force: boolean = false ): void { const targetBehaviors = this.behaviors; @@ -250,7 +250,7 @@ export class Controller extends PropertyChangeNotifier { } const length = behaviors.length; - const behaviorsToUnbind: Behavior[] = []; + const behaviorsToUnbind: Behavior[] = []; for (let i = 0; i < length; ++i) { const behavior = behaviors[i]; diff --git a/packages/web-components/fast-element/src/components/fast-element.ts b/packages/web-components/fast-element/src/components/fast-element.ts index 14ccf6ab538..71946541456 100644 --- a/packages/web-components/fast-element/src/components/fast-element.ts +++ b/packages/web-components/fast-element/src/components/fast-element.ts @@ -8,7 +8,7 @@ import { * Represents a custom element based on the FASTElement infrastructure. * @public */ -export interface FASTElement { +export interface FASTElement extends HTMLElement { /** * The underlying controller that handles the lifecycle and rendering of * this FASTElement. @@ -61,7 +61,7 @@ export interface FASTElement { function createFASTElement( BaseType: T ): { new (): InstanceType & FASTElement } { - return class extends (BaseType as any) implements FASTElement { + return class extends (BaseType as any) { public readonly $fastController!: Controller; public constructor() { diff --git a/packages/web-components/fast-element/src/observation/behavior.ts b/packages/web-components/fast-element/src/observation/behavior.ts index 7ebbf0078bb..7cac2d93dd5 100644 --- a/packages/web-components/fast-element/src/observation/behavior.ts +++ b/packages/web-components/fast-element/src/observation/behavior.ts @@ -5,17 +5,17 @@ import type { ExecutionContext } from "./observable.js"; * element's bind/unbind operations. * @public */ -export interface Behavior { +export interface Behavior { /** * Bind this behavior to the source. * @param source - The source to bind to. * @param context - The execution context that the binding is operating within. */ - bind(source: unknown, context: ExecutionContext): void; + bind(source: TSource, context: ExecutionContext): void; /** * Unbinds this behavior from the source. * @param source - The source to unbind from. */ - unbind(source: unknown): void; + unbind(source: TSource): void; } diff --git a/packages/web-components/fast-element/src/styles/css-directive.ts b/packages/web-components/fast-element/src/styles/css-directive.ts index d498865aca4..0e51a85c995 100644 --- a/packages/web-components/fast-element/src/styles/css-directive.ts +++ b/packages/web-components/fast-element/src/styles/css-directive.ts @@ -19,7 +19,7 @@ export class CSSDirective { * Creates a behavior to bind to the host element. * @returns - the behavior to bind to the host element, or undefined. */ - public createBehavior(): Behavior | undefined { + public createBehavior(): Behavior | undefined { return undefined; } } diff --git a/packages/web-components/fast-element/src/styles/css.ts b/packages/web-components/fast-element/src/styles/css.ts index f1ba966b79e..e97968abf3f 100644 --- a/packages/web-components/fast-element/src/styles/css.ts +++ b/packages/web-components/fast-element/src/styles/css.ts @@ -6,10 +6,10 @@ import { ComposableStyles, ElementStyles } from "./element-styles.js"; function collectStyles( strings: TemplateStringsArray, values: (ComposableStyles | CSSDirective)[] -): { styles: ComposableStyles[]; behaviors: Behavior[] } { +): { styles: ComposableStyles[]; behaviors: Behavior[] } { const styles: ComposableStyles[] = []; let cssString = ""; - const behaviors: Behavior[] = []; + const behaviors: Behavior[] = []; for (let i = 0, ii = strings.length - 1; i < ii; ++i) { cssString += strings[i]; @@ -71,10 +71,10 @@ export function css( return elementStyles; } -class CSSPartial extends CSSDirective implements Behavior { +class CSSPartial extends CSSDirective implements Behavior { private css: string = ""; private styles?: ElementStyles; - constructor(styles: ComposableStyles[], private behaviors: Behavior[]) { + constructor(styles: ComposableStyles[], private behaviors: Behavior[]) { super(); const stylesheets: ReadonlyArray { return this; } diff --git a/packages/web-components/fast-element/src/styles/element-styles.ts b/packages/web-components/fast-element/src/styles/element-styles.ts index cfe9b6cd4d5..58e578162de 100644 --- a/packages/web-components/fast-element/src/styles/element-styles.ts +++ b/packages/web-components/fast-element/src/styles/element-styles.ts @@ -62,7 +62,7 @@ export abstract class ElementStyles { public abstract readonly styles: ReadonlyArray; /** @internal */ - public abstract readonly behaviors: ReadonlyArray | null; + public abstract readonly behaviors: ReadonlyArray> | null; /** @internal */ public addStylesTo(target: StyleTarget): void { @@ -83,7 +83,7 @@ export abstract class ElementStyles { * Associates behaviors with this set of styles. * @param behaviors - The behaviors to associate. */ - public withBehaviors(...behaviors: Behavior[]): this { + public withBehaviors(...behaviors: Behavior[]): this { (this.behaviors as any) = this.behaviors === null ? behaviors : this.behaviors.concat(behaviors); @@ -118,20 +118,26 @@ function reduceStyles( function reduceBehaviors( styles: ReadonlyArray -): ReadonlyArray | null { +): ReadonlyArray> | null { return styles .map((x: ComposableStyles) => (x instanceof ElementStyles ? x.behaviors : null)) - .reduce((prev: Behavior[] | null, curr: Behavior[] | null) => { - if (curr === null) { - return prev; - } + .reduce( + ( + prev: Behavior[] | null, + curr: Behavior[] | null + ) => { + if (curr === null) { + return prev; + } - if (prev === null) { - prev = []; - } + if (prev === null) { + prev = []; + } - return prev.concat(curr); - }, null as Behavior[] | null); + return prev.concat(curr); + }, + null as Behavior[] | null + ); } /** @@ -167,7 +173,7 @@ export class AdoptedStyleSheetsStyles extends ElementStyles { return this._styleSheets; } - public readonly behaviors: ReadonlyArray | null; + public readonly behaviors: ReadonlyArray> | null; public constructor( public styles: ComposableStyles[], @@ -203,7 +209,7 @@ function getNextStyleClass(): string { export class StyleElementStyles extends ElementStyles { private readonly styleSheets: string[]; private readonly styleClass: string; - public readonly behaviors: ReadonlyArray | null = null; + public readonly behaviors: ReadonlyArray> | null = null; public constructor(public styles: ComposableStyles[]) { super(); diff --git a/packages/web-components/fast-element/src/styles/styles.spec.ts b/packages/web-components/fast-element/src/styles/styles.spec.ts index cd6612d4682..9814ddb0037 100644 --- a/packages/web-components/fast-element/src/styles/styles.spec.ts +++ b/packages/web-components/fast-element/src/styles/styles.spec.ts @@ -10,6 +10,7 @@ import { CSSDirective } from "./css-directive"; import { css, cssPartial } from "./css"; import type { Behavior } from "../observation/behavior"; import { defaultExecutionContext } from "../observation/observable"; +import type { FASTElement } from ".."; if (DOM.supportsAdoptedStyleSheets) { describe("AdoptedStyleSheetsStyles", () => { @@ -331,12 +332,12 @@ describe("cssPartial", () => { const partial = cssPartial`${new directive}${new directive2}`; const el = { $fastController: { - addBehaviors(behaviors: Behavior[]) { + addBehaviors(behaviors: Behavior[]) { expect(behaviors[0]).to.equal(behavior); expect(behaviors[1]).to.equal(behavior2); } } - } + } as FASTElement; partial.createBehavior()?.bind(el, defaultExecutionContext) }); @@ -352,7 +353,7 @@ describe("cssPartial", () => { called = true; } } - } + } as FASTElement; partial.createBehavior()?.bind(el, defaultExecutionContext) From 104a663418057f08dade37ef36bc30e1582dada4 Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Thu, 14 Oct 2021 12:57:47 -0400 Subject: [PATCH 014/135] refactor: clean up styles code a bit --- .../fast-element/docs/api-report.md | 7 ++++-- .../fast-element/src/styles/element-styles.ts | 24 ++++++++----------- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/packages/web-components/fast-element/docs/api-report.md b/packages/web-components/fast-element/docs/api-report.md index 3e1374ddbaf..234d1e888cf 100644 --- a/packages/web-components/fast-element/docs/api-report.md +++ b/packages/web-components/fast-element/docs/api-report.md @@ -230,17 +230,20 @@ export type ElementStyleFactory = (styles: ReadonlyArray) => E // @public export abstract class ElementStyles { + constructor( + styles: ReadonlyArray, + behaviors: ReadonlyArray> | null); // @internal (undocumented) addStylesTo(target: StyleTarget): void; // @internal (undocumented) - abstract readonly behaviors: ReadonlyArray> | null; + readonly behaviors: ReadonlyArray> | null; static readonly create: ElementStyleFactory; // @internal (undocumented) isAttachedTo(target: StyleTarget): boolean; // @internal (undocumented) removeStylesFrom(target: StyleTarget): void; // @internal (undocumented) - abstract readonly styles: ReadonlyArray; + readonly styles: ReadonlyArray; withBehaviors(...behaviors: Behavior[]): this; } diff --git a/packages/web-components/fast-element/src/styles/element-styles.ts b/packages/web-components/fast-element/src/styles/element-styles.ts index 58e578162de..95f22f5703f 100644 --- a/packages/web-components/fast-element/src/styles/element-styles.ts +++ b/packages/web-components/fast-element/src/styles/element-styles.ts @@ -58,11 +58,12 @@ export type ElementStyleFactory = ( export abstract class ElementStyles { private targets: WeakSet = new WeakSet(); - /** @internal */ - public abstract readonly styles: ReadonlyArray; - - /** @internal */ - public abstract readonly behaviors: ReadonlyArray> | null; + constructor( + /** @internal */ + public readonly styles: ReadonlyArray, + /** @internal */ + public readonly behaviors: ReadonlyArray> | null + ) {} /** @internal */ public addStylesTo(target: StyleTarget): void { @@ -173,14 +174,11 @@ export class AdoptedStyleSheetsStyles extends ElementStyles { return this._styleSheets; } - public readonly behaviors: ReadonlyArray> | null; - public constructor( - public styles: ComposableStyles[], + styles: ComposableStyles[], private styleSheetCache: Map ) { - super(); - this.behaviors = reduceBehaviors(styles); + super(styles, reduceBehaviors(styles)); } public addStylesTo(target: StyleTarget): void { @@ -209,11 +207,9 @@ function getNextStyleClass(): string { export class StyleElementStyles extends ElementStyles { private readonly styleSheets: string[]; private readonly styleClass: string; - public readonly behaviors: ReadonlyArray> | null = null; - public constructor(public styles: ComposableStyles[]) { - super(); - this.behaviors = reduceBehaviors(styles); + public constructor(styles: ComposableStyles[]) { + super(styles, reduceBehaviors(styles)); this.styleSheets = reduceStyles(styles) as string[]; this.styleClass = getNextStyleClass(); } From 9591df4b4724a1fdc0a726acd15de7bce353966c Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Thu, 14 Oct 2021 16:10:15 -0400 Subject: [PATCH 015/135] feat: introduce ViewBehavior to enable stateless behaviors --- .../fast-element/docs/api-report.md | 56 ++++++++++--------- .../fast-element/src/components/controller.ts | 4 +- .../fast-element/src/observation/behavior.ts | 4 +- .../fast-element/src/templating/binding.ts | 4 +- .../fast-element/src/templating/children.ts | 4 +- .../fast-element/src/templating/compiler.ts | 12 ++-- .../src/templating/html-directive.ts | 49 +++++++++++++--- .../fast-element/src/templating/ref.ts | 4 +- .../fast-element/src/templating/repeat.ts | 4 +- .../fast-element/src/templating/slotted.ts | 4 +- .../src/templating/template.spec.ts | 4 +- .../fast-element/src/templating/view.ts | 24 ++++---- 12 files changed, 107 insertions(+), 66 deletions(-) diff --git a/packages/web-components/fast-element/docs/api-report.md b/packages/web-components/fast-element/docs/api-report.md index 234d1e888cf..fd8f8051544 100644 --- a/packages/web-components/fast-element/docs/api-report.md +++ b/packages/web-components/fast-element/docs/api-report.md @@ -17,12 +17,12 @@ export interface Accessor { // @public export class AttachedBehaviorHTMLDirective extends HTMLDirective { constructor(name: string, behavior: AttachedBehaviorType, options: T); - createBehavior(targets: BehaviorTargets): Behavior; + createBehavior(targets: ViewBehaviorTargets): ViewBehavior; createPlaceholder(index: number): string; } // @public -export type AttachedBehaviorType = new (targets: BehaviorTargets, targetId: string, options: T) => Behavior; +export type AttachedBehaviorType = new (targets: ViewBehaviorTargets, targetId: string, options: T) => Behavior; // @public export function attr(config?: DecoratorAttributeConfiguration): (target: {}, property: string) => void; @@ -60,14 +60,9 @@ export type AttributeMode = "reflect" | "boolean" | "fromView"; // @public export interface Behavior { bind(source: TSource, context: ExecutionContext): void; - unbind(source: TSource): void; + unbind(source: TSource, context: ExecutionContext): void; } -// @public -export type BehaviorTargets = { - [id: string]: Node; -}; - // @public export type Binding = (source: TSource, context: ExecutionContext) => TReturn; @@ -138,7 +133,7 @@ export function children(propertyOrOptions: (keyof T & string) | Childr // // @public export class ChildrenBehavior extends NodeObservationBehavior { - constructor(targets: BehaviorTargets, targetId: string, options: ChildrenBehaviorOptions); + constructor(targets: ViewBehaviorTargets, targetId: string, options: ChildrenBehaviorOptions); disconnect(): void; protected getNodes(): ChildNode[]; observe(): void; @@ -328,32 +323,32 @@ export class HTMLBindingDirective extends TargetedHTMLDirective { constructor(binding: Binding); // (undocumented) binding: Binding; - createBehavior(targets: BehaviorTargets): BindingBehavior; + createBehavior(targets: ViewBehaviorTargets): BindingBehavior; targetAtContent(): void; get targetName(): string | undefined; set targetName(value: string | undefined); } // @public -export abstract class HTMLDirective implements NodeBehaviorFactory { - abstract createBehavior(targets: BehaviorTargets): Behavior; +export abstract class HTMLDirective implements ViewBehaviorFactory { + abstract createBehavior(targets: ViewBehaviorTargets): Behavior | ViewBehavior; abstract createPlaceholder(index: number): string; targetId: string; } // @public export class HTMLTemplateCompilationResult { - constructor(fragment: DocumentFragment, factories: NodeBehaviorFactory[], targetIds: string[], descriptors: PropertyDescriptorMap); - createTargets(root: Node, host?: Node): BehaviorTargets; + constructor(fragment: DocumentFragment, factories: ViewBehaviorFactory[], targetIds: string[], descriptors: PropertyDescriptorMap); + createTargets(root: Node, host?: Node): ViewBehaviorTargets; // (undocumented) - readonly factories: NodeBehaviorFactory[]; + readonly factories: ViewBehaviorFactory[]; // (undocumented) readonly fragment: DocumentFragment; } // @public export class HTMLView implements ElementView, SyntheticView { - constructor(fragment: DocumentFragment, factories: NodeBehaviorFactory[], targets: BehaviorTargets); + constructor(fragment: DocumentFragment, factories: ViewBehaviorFactory[], targets: ViewBehaviorTargets); appendTo(node: Node): void; bind(source: TSource, context: ExecutionContext): void; context: ExecutionContext | null; @@ -374,12 +369,6 @@ export type Mutable = { -readonly [P in keyof T]: T[P]; }; -// @public -export interface NodeBehaviorFactory { - createBehavior(targets: BehaviorTargets): Behavior; - targetId: string; -} - // @public export interface NodeBehaviorOptions { filter?: ElementsFilter; @@ -443,7 +432,7 @@ export function ref(propertyName: keyof T & string): CaptureType; // @public export class RefBehavior implements Behavior { - constructor(targets: BehaviorTargets, targetId: string, propertyName: string); + constructor(targets: ViewBehaviorTargets, targetId: string, propertyName: string); bind(source: any): void; unbind(): void; } @@ -463,7 +452,7 @@ export class RepeatBehavior implements Behavior, Subscriber { // @public export class RepeatDirective extends HTMLDirective { constructor(itemsBinding: Binding, templateBinding: Binding, options: RepeatOptions); - createBehavior(targets: BehaviorTargets): RepeatBehavior; + createBehavior(targets: ViewBehaviorTargets): RepeatBehavior; createPlaceholder: (index: number) => string; } @@ -483,7 +472,7 @@ export function slotted(propertyOrOptions: (keyof T & string) | Slotted // @public export class SlottedBehavior extends NodeObservationBehavior { - constructor(targets: BehaviorTargets, targetId: string, options: SlottedBehaviorOptions); + constructor(targets: ViewBehaviorTargets, targetId: string, options: SlottedBehaviorOptions); disconnect(): void; protected getNodes(): Node[]; observe(): void; @@ -579,6 +568,23 @@ export interface View { unbind(): void; } +// @public +export interface ViewBehavior { + bind(source: TSource, context: ExecutionContext, targets: ViewBehaviorTargets): void; + unbind(source: TSource, context: ExecutionContext, targets: ViewBehaviorTargets): void; +} + +// @public +export interface ViewBehaviorFactory { + createBehavior(targets: ViewBehaviorTargets): Behavior | ViewBehavior; + targetId: string; +} + +// @public +export type ViewBehaviorTargets = { + [id: string]: Node; +}; + // @public export class ViewTemplate implements ElementViewTemplate, SyntheticViewTemplate { constructor(html: string | HTMLTemplateElement, directives: ReadonlyArray); diff --git a/packages/web-components/fast-element/src/components/controller.ts b/packages/web-components/fast-element/src/components/controller.ts index 357319d15f7..150911fb744 100644 --- a/packages/web-components/fast-element/src/components/controller.ts +++ b/packages/web-components/fast-element/src/components/controller.ts @@ -268,7 +268,7 @@ export class Controller extends PropertyChangeNotifier { const element = this.element; for (let i = 0; i < behaviorsToUnbind.length; ++i) { - behaviorsToUnbind[i].unbind(element); + behaviorsToUnbind[i].unbind(element, defaultExecutionContext); } } } @@ -321,7 +321,7 @@ export class Controller extends PropertyChangeNotifier { if (behaviors !== null) { const element = this.element; for (const [behavior] of behaviors) { - behavior.unbind(element); + behavior.unbind(element, defaultExecutionContext); } } } diff --git a/packages/web-components/fast-element/src/observation/behavior.ts b/packages/web-components/fast-element/src/observation/behavior.ts index 7cac2d93dd5..b1eea785632 100644 --- a/packages/web-components/fast-element/src/observation/behavior.ts +++ b/packages/web-components/fast-element/src/observation/behavior.ts @@ -1,7 +1,7 @@ import type { ExecutionContext } from "./observable.js"; /** - * Represents and object that can contribute behavior to a view or + * Represents an object that can contribute behavior to a view or * element's bind/unbind operations. * @public */ @@ -17,5 +17,5 @@ export interface Behavior { * Unbinds this behavior from the source. * @param source - The source to unbind from. */ - unbind(source: TSource): void; + unbind(source: TSource, context: ExecutionContext): void; } diff --git a/packages/web-components/fast-element/src/templating/binding.ts b/packages/web-components/fast-element/src/templating/binding.ts index 054c0aee4de..0f8831d6630 100644 --- a/packages/web-components/fast-element/src/templating/binding.ts +++ b/packages/web-components/fast-element/src/templating/binding.ts @@ -7,7 +7,7 @@ import { Observable, setCurrentEvent, } from "../observation/observable"; -import { BehaviorTargets, TargetedHTMLDirective } from "./html-directive"; +import { ViewBehaviorTargets, TargetedHTMLDirective } from "./html-directive"; import type { SyntheticView } from "./view"; function normalBind( @@ -277,7 +277,7 @@ export class HTMLBindingDirective extends TargetedHTMLDirective { * information stored in the BindingDirective. * @param target - The target node that the binding behavior should attach to. */ - createBehavior(targets: BehaviorTargets): BindingBehavior { + createBehavior(targets: ViewBehaviorTargets): BindingBehavior { /* eslint-disable-next-line @typescript-eslint/no-use-before-define */ return new BindingBehavior( targets[this.targetId], diff --git a/packages/web-components/fast-element/src/templating/children.ts b/packages/web-components/fast-element/src/templating/children.ts index 00f872378f5..aeb797810cc 100644 --- a/packages/web-components/fast-element/src/templating/children.ts +++ b/packages/web-components/fast-element/src/templating/children.ts @@ -1,4 +1,4 @@ -import { AttachedBehaviorHTMLDirective, BehaviorTargets } from "./html-directive"; +import { AttachedBehaviorHTMLDirective, ViewBehaviorTargets } from "./html-directive"; import { NodeBehaviorOptions, NodeObservationBehavior } from "./node-observation"; import type { CaptureType } from "./template"; @@ -50,7 +50,7 @@ export class ChildrenBehavior extends NodeObservationBehavior; public addFactory( - factory: NodeBehaviorFactory, + factory: ViewBehaviorFactory, parentId: string, targetId: string, targetIndex: number @@ -333,7 +333,7 @@ export class HTMLTemplateCompilationResult { */ public constructor( public readonly fragment: DocumentFragment, - public readonly factories: NodeBehaviorFactory[], + public readonly factories: ViewBehaviorFactory[], private targetIds: string[], descriptors: PropertyDescriptorMap ) { @@ -346,7 +346,7 @@ export class HTMLTemplateCompilationResult { * @param root - The root element. * @returns A lookup object for behavior targets. */ - public createTargets(root: Node, host?: Node): BehaviorTargets { + public createTargets(root: Node, host?: Node): ViewBehaviorTargets { const targets = Object.create(this.proto, { r: { value: root }, h: { value: host || root }, 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 9ac9aae3314..fe63a78ed14 100644 --- a/packages/web-components/fast-element/src/templating/html-directive.ts +++ b/packages/web-components/fast-element/src/templating/html-directive.ts @@ -1,20 +1,51 @@ -import { DOM } from "../dom.js"; -import type { Behavior } from "../observation/behavior.js"; +import { DOM } from "../dom"; +import type { Behavior } from "../observation/behavior"; +import type { ExecutionContext } from "../observation/observable"; /** * The target nodes available to a behavior. * @public */ -export type BehaviorTargets = { +export type ViewBehaviorTargets = { [id: string]: Node; }; +/** + * Represents an object that can contribute behavior to a view. + * @public + */ +export interface ViewBehavior { + /** + * Bind this behavior to the source. + * @param source - The source to bind to. + * @param context - The execution context that the binding is operating within. + * @param targets - The targets that behaviors in a view can attach to. + */ + bind( + source: TSource, + context: ExecutionContext, + targets: ViewBehaviorTargets + ): void; + + /** + * Unbinds this behavior from the source. + * @param source - The source to unbind from. + * @param context - The execution context that the binding is operating within. + * @param targets - The targets that behaviors in a view can attach to. + */ + unbind( + source: TSource, + context: ExecutionContext, + targets: ViewBehaviorTargets + ): void; +} + /** * A factory that can create a {@link Behavior} associated with a particular * location within a DOM fragment. * @public */ -export interface NodeBehaviorFactory { +export interface ViewBehaviorFactory { /** * The structural id of the DOM node to which the created behavior will apply. */ @@ -24,14 +55,14 @@ export interface NodeBehaviorFactory { * Creates a behavior. * @param target - The targets available for behaviors to be attached to. */ - createBehavior(targets: BehaviorTargets): Behavior; + createBehavior(targets: ViewBehaviorTargets): Behavior | ViewBehavior; } /** * Instructs the template engine to apply behavior to a node. * @public */ -export abstract class HTMLDirective implements NodeBehaviorFactory { +export abstract class HTMLDirective implements ViewBehaviorFactory { /** * The structural id of the DOM node to which the created behavior will apply. */ @@ -47,7 +78,7 @@ export abstract class HTMLDirective implements NodeBehaviorFactory { * Creates a behavior. * @param targets - The targets available for behaviors to be attached to. */ - public abstract createBehavior(targets: BehaviorTargets): Behavior; + public abstract createBehavior(targets: ViewBehaviorTargets): Behavior | ViewBehavior; } /** @@ -75,7 +106,7 @@ export abstract class TargetedHTMLDirective extends HTMLDirective { * @public */ export type AttachedBehaviorType = new ( - targets: BehaviorTargets, + targets: ViewBehaviorTargets, targetId: string, options: T ) => Behavior; @@ -116,7 +147,7 @@ export class AttachedBehaviorHTMLDirective extends HTMLDirective { * Creates an instance of the `behavior` type this directive was constructed with * and passes the targets, targetId, and options to that `behavior`'s constructor. */ - public createBehavior(targets: BehaviorTargets): Behavior { + public createBehavior(targets: ViewBehaviorTargets): ViewBehavior { return new this.behavior(targets, this.targetId, this.options); } } diff --git a/packages/web-components/fast-element/src/templating/ref.ts b/packages/web-components/fast-element/src/templating/ref.ts index 4371edbbdb7..4a97e2487ba 100644 --- a/packages/web-components/fast-element/src/templating/ref.ts +++ b/packages/web-components/fast-element/src/templating/ref.ts @@ -1,6 +1,6 @@ import type { Behavior } from "../observation/behavior"; import type { CaptureType } from "./template"; -import { AttachedBehaviorHTMLDirective, BehaviorTargets } from "./html-directive"; +import { AttachedBehaviorHTMLDirective, ViewBehaviorTargets } from "./html-directive"; /** * The runtime behavior for template references. @@ -15,7 +15,7 @@ export class RefBehavior implements Behavior { * @param propertyName - The name of the property to assign the reference to. */ public constructor( - targets: BehaviorTargets, + targets: ViewBehaviorTargets, targetId: string, private propertyName: string ) { diff --git a/packages/web-components/fast-element/src/templating/repeat.ts b/packages/web-components/fast-element/src/templating/repeat.ts index a2c2cccbdac..ce9193fe98c 100644 --- a/packages/web-components/fast-element/src/templating/repeat.ts +++ b/packages/web-components/fast-element/src/templating/repeat.ts @@ -10,7 +10,7 @@ import { enableArrayObservation } from "../observation/array-observer"; import type { Splice } from "../observation/array-change-records"; import type { Behavior } from "../observation/behavior"; import { emptyArray } from "../platform"; -import { BehaviorTargets, HTMLDirective } from "./html-directive"; +import { ViewBehaviorTargets, HTMLDirective } from "./html-directive"; import { HTMLView, SyntheticView } from "./view"; import type { CaptureType, SyntheticViewTemplate } from "./template"; @@ -329,7 +329,7 @@ export class RepeatDirective extends HTMLDirective { * Creates a behavior for the provided target node. * @param target - The node instance to create the behavior for. */ - public createBehavior(targets: BehaviorTargets): RepeatBehavior { + public createBehavior(targets: ViewBehaviorTargets): RepeatBehavior { return new RepeatBehavior( targets[this.targetId], this.itemsBinding, diff --git a/packages/web-components/fast-element/src/templating/slotted.ts b/packages/web-components/fast-element/src/templating/slotted.ts index 7b452bd64af..38beb15f8eb 100644 --- a/packages/web-components/fast-element/src/templating/slotted.ts +++ b/packages/web-components/fast-element/src/templating/slotted.ts @@ -1,4 +1,4 @@ -import { AttachedBehaviorHTMLDirective, BehaviorTargets } from "./html-directive"; +import { AttachedBehaviorHTMLDirective, ViewBehaviorTargets } from "./html-directive"; import { NodeBehaviorOptions, NodeObservationBehavior } from "./node-observation"; import type { CaptureType } from "./template"; @@ -21,7 +21,7 @@ export class SlottedBehavior extends NodeObservationBehavior { it(`transforms a string into a ViewTemplate.`, () => { @@ -231,7 +231,7 @@ describe(`The html tag template helper`, () => { it(`captures a case-sensitive property name when used with a named target directive`, () => { class TestDirective extends TargetedHTMLDirective { targetName: string | undefined; - createBehavior(targets: BehaviorTargets) { + createBehavior(targets: ViewBehaviorTargets) { return { bind() {}, unbind() {} }; } } diff --git a/packages/web-components/fast-element/src/templating/view.ts b/packages/web-components/fast-element/src/templating/view.ts index 09d02257cb6..a4c85dec7ae 100644 --- a/packages/web-components/fast-element/src/templating/view.ts +++ b/packages/web-components/fast-element/src/templating/view.ts @@ -1,6 +1,7 @@ +import type { ViewBehavior } from ".."; import type { Behavior } from "../observation/behavior"; import type { ExecutionContext } from "../observation/observable"; -import type { BehaviorTargets, NodeBehaviorFactory } from "./html-directive"; +import type { ViewBehaviorTargets, ViewBehaviorFactory } from "./html-directive"; /** * Represents a collection of DOM nodes which can be bound to a data source. @@ -106,7 +107,7 @@ export class HTMLView implements ElementView, SyntheticView { - private behaviors: Behavior[] | null = null; + private behaviors: ViewBehavior[] | null = null; /** * The data that the view is bound to. @@ -135,8 +136,8 @@ export class HTMLView */ public constructor( private fragment: DocumentFragment, - private factories: NodeBehaviorFactory[], - private targets: BehaviorTargets + private factories: ViewBehaviorFactory[], + private targets: ViewBehaviorTargets ) { this.firstChild = fragment.firstChild!; this.lastChild = fragment.lastChild!; @@ -216,26 +217,26 @@ export class HTMLView this.source = source; this.context = context; + const targets = this.targets; if (oldSource !== null) { for (let i = 0, ii = behaviors!.length; i < ii; ++i) { const current = behaviors![i]; - current.unbind(oldSource); - current.bind(source, context); + current.unbind(oldSource, context, targets); + current.bind(source, context, targets); } } else if (behaviors === null) { this.behaviors = behaviors = new Array(this.factories.length); - const targets = this.targets; const factories = this.factories; for (let i = 0, ii = factories.length; i < ii; ++i) { const behavior = factories[i].createBehavior(targets); - behavior.bind(source, context); + behavior.bind(source, context, targets); behaviors[i] = behavior; } } else { for (let i = 0, ii = behaviors.length; i < ii; ++i) { - behaviors[i].bind(source, context); + behaviors[i].bind(source, context, targets); } } } @@ -250,13 +251,16 @@ export class HTMLView return; } + const targets = this.targets; + const context = this.context; const behaviors = this.behaviors!; for (let i = 0, ii = behaviors.length; i < ii; ++i) { - behaviors[i].unbind(oldSource); + behaviors[i].unbind(oldSource, context!, targets); } this.source = null; + this.context = null; } /** From 605e2cdba7901e3b80fa3c0b108739b5af77ca9f Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Thu, 14 Oct 2021 19:52:43 -0400 Subject: [PATCH 016/135] fix: remove unnecessary render based on element id --- .../web-components/fast-element/docs/api-report.md | 2 +- .../fast-element/docs/fast-element-2-changes.md | 5 +++-- .../fast-element/src/templating/template.ts | 13 ++----------- 3 files changed, 6 insertions(+), 14 deletions(-) diff --git a/packages/web-components/fast-element/docs/api-report.md b/packages/web-components/fast-element/docs/api-report.md index fd8f8051544..35f84ab2846 100644 --- a/packages/web-components/fast-element/docs/api-report.md +++ b/packages/web-components/fast-element/docs/api-report.md @@ -591,7 +591,7 @@ export class ViewTemplate impl create(hostBindingTarget?: Element): HTMLView; readonly directives: ReadonlyArray; readonly html: string | HTMLTemplateElement; - render(source: TSource, host: Node | string, hostBindingTarget?: Element): HTMLView; + render(source: TSource, host: Node, hostBindingTarget?: Element): HTMLView; } // @public diff --git a/packages/web-components/fast-element/docs/fast-element-2-changes.md b/packages/web-components/fast-element/docs/fast-element-2-changes.md index 0c69a7bcc72..8937c3003ae 100644 --- a/packages/web-components/fast-element/docs/fast-element-2-changes.md +++ b/packages/web-components/fast-element/docs/fast-element-2-changes.md @@ -5,6 +5,7 @@ * `HTMLDirective` - The `targetIndex: number` property has been replaced by a `targetId: string` property. The `createBehavior` method no longer takes a target `Node` but instead takes a `BehaviorTargets` instance. The actual target can be looked up on the `BehaviorTargets` instance by indexing with the `targetId` property. * `compileTemplate()` - Internals have been significantly changed. The implementation no longer uses a TreeWalker. The return type has change to an `HTMLTemplateCompilationResult` with different properties. * `View` and `HTMLView` - Type parameters added to enable strongly typed views based on their data source. The constructor of `HTMLView` has a new signature based on changes to the compiler's output. Internals have been cleaned up and no longer rely on the Range type. -* `ElementViewTemplate`, `SyntheticViewTemplate`, and `ViewTemplate` - Added type parameters throughout. Logic to instantiate and apply behaviors moved out of the template and into the view where it can be lazily executed. +* `ElementViewTemplate`, `SyntheticViewTemplate`, and `ViewTemplate` - Added type parameters throughout. Logic to instantiate and apply behaviors moved out of the template and into the view where it can be lazily executed. Removed the ability of the `render` method to take a string id of the node to render to. You must provide a node. * `DOM` - Tree Walker methods are no longer used and are thus removed. The API for removing child nodes has been removed as well since it was only used in one place and could be inlined. -* `class` - Bindings to `class` are now more nuanced. Binding directly to `class` will simply set the `className` property. If you need to bind to `class` knowing that manual JS will also manipulate the `classList` in addition to the binding, then you should now bind to `:classList` instead. This allows for performance optimizations in the simple, most common case. \ No newline at end of file +* `class` - Bindings to `class` are now more nuanced. Binding directly to `class` will simply set the `className` property. If you need to bind to `class` knowing that manual JS will also manipulate the `classList` in addition to the binding, then you should now bind to `:classList` instead. This allows for performance optimizations in the simple, most common case. +* `Behavior` and `ViewBehavior` - `Behavior` now requires an `ExecutionContext` for `unbind`. Behaviors can be used for elements or views. `ViewBehavior` has been introduced for use exclusively with views, and provides some optimization opportunities. \ No newline at end of file diff --git a/packages/web-components/fast-element/src/templating/template.ts b/packages/web-components/fast-element/src/templating/template.ts index 9a44ea872ba..3157a0a7dc7 100644 --- a/packages/web-components/fast-element/src/templating/template.ts +++ b/packages/web-components/fast-element/src/templating/template.ts @@ -120,21 +120,12 @@ export class ViewTemplate */ public render( source: TSource, - host: Node | string, + host: Node, hostBindingTarget?: Element ): HTMLView { - if (typeof host === "string") { - host = document.getElementById(host)!; - } - - if (hostBindingTarget === void 0) { - hostBindingTarget = host as Element; - } - - const view = this.create(hostBindingTarget); + const view = this.create(hostBindingTarget ?? (host as any)); view.bind(source, defaultExecutionContext); view.appendTo(host); - return view; } } From e9e59cd09184428b7afce0349fe95970265fd114 Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Thu, 14 Oct 2021 21:12:18 -0400 Subject: [PATCH 017/135] perf: switch targetId to set and use normal props for r/h lookups --- .../fast-element/docs/api-report.md | 2 +- .../web-components/fast-element/src/dom.ts | 4 +-- .../fast-element/src/templating/compiler.ts | 29 ++++++++----------- 3 files changed, 15 insertions(+), 20 deletions(-) diff --git a/packages/web-components/fast-element/docs/api-report.md b/packages/web-components/fast-element/docs/api-report.md index 35f84ab2846..2effc538026 100644 --- a/packages/web-components/fast-element/docs/api-report.md +++ b/packages/web-components/fast-element/docs/api-report.md @@ -338,7 +338,7 @@ export abstract class HTMLDirective implements ViewBehaviorFactory { // @public export class HTMLTemplateCompilationResult { - constructor(fragment: DocumentFragment, factories: ViewBehaviorFactory[], targetIds: string[], descriptors: PropertyDescriptorMap); + constructor(fragment: DocumentFragment, factories: ViewBehaviorFactory[], targetIds: Set, descriptors: PropertyDescriptorMap); createTargets(root: Node, host?: Node): ViewBehaviorTargets; // (undocumented) readonly factories: ViewBehaviorFactory[]; diff --git a/packages/web-components/fast-element/src/dom.ts b/packages/web-components/fast-element/src/dom.ts index 9f583f95aec..60a880f7082 100644 --- a/packages/web-components/fast-element/src/dom.ts +++ b/packages/web-components/fast-element/src/dom.ts @@ -110,7 +110,7 @@ export const DOM = Object.freeze({ * @remarks * Used internally by attribute directives such as `ref`, `slotted`, and `children`. */ - createCustomAttributePlaceholder(attributeName: string, index: number) { + createCustomAttributePlaceholder(attributeName: string, index: number): string { return `${attributeName}="${this.createInterpolationPlaceholder(index)}"`; }, @@ -120,7 +120,7 @@ export const DOM = Object.freeze({ * @remarks * Used internally by structural directives such as `repeat`. */ - createBlockPlaceholder(index: number) { + createBlockPlaceholder(index: number): string { return ``; }, diff --git a/packages/web-components/fast-element/src/templating/compiler.ts b/packages/web-components/fast-element/src/templating/compiler.ts index 1417a0c658f..e8a67b203ff 100644 --- a/packages/web-components/fast-element/src/templating/compiler.ts +++ b/packages/web-components/fast-element/src/templating/compiler.ts @@ -69,7 +69,7 @@ const next = { class CompilationContext { public factories: ViewBehaviorFactory[] = []; - public targetIds: string[] = []; + public targetIds = new Set(); public descriptors: PropertyDescriptorMap = {}; public directives: ReadonlyArray; @@ -79,8 +79,8 @@ class CompilationContext { targetId: string, targetIndex: number ): void { - if (this.targetIds.indexOf(targetId) === -1) { - this.targetIds.push(targetId); + if (!this.targetIds.has(targetId)) { + this.targetIds.add(targetId); addTargetDescriptor(this.descriptors, parentId, targetId, targetIndex); } @@ -107,7 +107,7 @@ class CompilationContext { ); this.factories = []; - this.targetIds = []; + this.targetIds = new Set(); this.descriptors = {}; sharedContext = this; @@ -334,7 +334,7 @@ export class HTMLTemplateCompilationResult { public constructor( public readonly fragment: DocumentFragment, public readonly factories: ViewBehaviorFactory[], - private targetIds: string[], + private targetIds: Set, descriptors: PropertyDescriptorMap ) { this.proto = Object.create(null, descriptors); @@ -347,15 +347,12 @@ export class HTMLTemplateCompilationResult { * @returns A lookup object for behavior targets. */ public createTargets(root: Node, host?: Node): ViewBehaviorTargets { - const targets = Object.create(this.proto, { - r: { value: root }, - h: { value: host || root }, - }); + const targets = Object.create(this.proto); + targets.r = root; + targets.h = host ?? root; - const ids = this.targetIds; - - for (let i = 0, ii = ids.length; i < ii; ++i) { - targets[ids[i]]; // trigger locators + for (const id of this.targetIds) { + targets[id]; // trigger locator } return targets; @@ -378,10 +375,8 @@ export function compileTemplate( template: HTMLTemplateElement, directives: ReadonlyArray ): HTMLTemplateCompilationResult { - const fragment = template.content; // https://bugs.chromium.org/p/chromium/issues/detail?id=1111864 - document.adoptNode(fragment); - + const fragment = document.adoptNode(template.content); const context = CompilationContext.open(directives); compileAttributes(context, "", template, /* host */ "h", 0, true); @@ -399,7 +394,7 @@ export function compileTemplate( fragment.insertBefore(document.createComment(""), fragment.firstChild); } - compileChildren(context, fragment, "r"); + compileChildren(context, fragment, /* root */ "r"); next.node = null; // prevent leaks return context.close(fragment); } From ce417cbe7fca98864f388f731123a8cc9820908f Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Thu, 14 Oct 2021 21:42:30 -0400 Subject: [PATCH 018/135] refactor(observable): tighten up some code --- .../src/observation/observable.ts | 681 +++++++++--------- 1 file changed, 325 insertions(+), 356 deletions(-) diff --git a/packages/web-components/fast-element/src/observation/observable.ts b/packages/web-components/fast-element/src/observation/observable.ts index 353b560e9a5..7032f270427 100644 --- a/packages/web-components/fast-element/src/observation/observable.ts +++ b/packages/web-components/fast-element/src/observation/observable.ts @@ -1,7 +1,14 @@ -import { DOM } from "../dom.js"; -import { FAST, KernelServiceId } from "../platform.js"; -import { PropertyChangeNotifier, SubscriberSet } from "./notifier.js"; -import type { Notifier, Subscriber } from "./notifier.js"; +import { DOM } from "../dom"; +import { PropertyChangeNotifier, SubscriberSet } from "./notifier"; +import type { Notifier, Subscriber } from "./notifier"; + +const volatileRegex = /(:|&&|\|\||if)/; +const notifierLookup = new WeakMap(); +const accessorLookup = new WeakMap(); +let watcher: BindingObserverImplementation | undefined = void 0; +let createArrayObserver = (array: any[]): Notifier => { + throw new Error("Must call enableArrayObservation before observing arrays."); +}; /** * Represents a getter/setter property accessor on an object. @@ -27,92 +34,131 @@ export interface Accessor { setValue(source: any, value: any): void; } -/** - * The signature of an arrow function capable of being evaluated - * as part of a template binding update. - * @public - */ -export type Binding = ( - source: TSource, - context: ExecutionContext -) => TReturn; +class DefaultObservableAccessor implements Accessor { + private field: string; + private callback: string; + + constructor(public name: string) { + this.field = `_${name}`; + this.callback = `${name}Changed`; + } + + getValue(source: any): any { + if (watcher !== void 0) { + watcher.watch(source, this.name); + } + + return source[this.field]; + } + + setValue(source: any, newValue: any): void { + const field = this.field; + const oldValue = source[field]; + + if (oldValue !== newValue) { + source[field] = newValue; + + const callback = source[this.callback]; + + if (typeof callback === "function") { + callback.call(source, oldValue, newValue); + } + + /* eslint-disable-next-line @typescript-eslint/no-use-before-define */ + getNotifier(source).notify(this.name); + } + } +} /** - * A record of observable property access. + * Common Observable APIs. * @public */ -export interface ObservationRecord { +export const Observable = Object.freeze({ /** - * The source object with an observable property that was accessed. + * @internal + * @param factory - The factory used to create array observers. */ - propertySource: any; + setArrayObserverFactory(factory: (collection: any[]) => Notifier): void { + createArrayObserver = factory; + }, /** - * The name of the observable property on {@link ObservationRecord.propertySource} that was accessed. + * Gets a notifier for an object or Array. + * @param source - The object or Array to get the notifier for. */ - propertyName: string; -} + getNotifier(source: any): Notifier { + let found = source.$fastController ?? notifierLookup.get(source); -interface SubscriptionRecord extends ObservationRecord { - notifier: Notifier; - next: SubscriptionRecord | undefined; -} + if (found === void 0) { + Array.isArray(source) + ? (found = createArrayObserver(source)) + : notifierLookup.set( + source, + (found = new PropertyChangeNotifier(source)) + ); + } + + return found; + }, -/** - * Enables evaluation of and subscription to a binding. - * @public - */ -export interface BindingObserver - extends Notifier { /** - * Begins observing the binding for the source and returns the current value. - * @param source - The source that the binding is based on. - * @param context - The execution context to execute the binding within. - * @returns The value of the binding. + * Records a property change for a source object. + * @param source - The object to record the change against. + * @param propertyName - The property to track as changed. */ - observe(source: TSource, context: ExecutionContext): TReturn; + track(source: unknown, propertyName: string): void { + watcher && watcher.watch(source, propertyName); + }, /** - * Unsubscribe from all dependent observables of the binding. + * Notifies watchers that the currently executing property getter or function is volatile + * with respect to its observable dependencies. */ - disconnect(): void; + trackVolatile(): void { + watcher && (watcher.needsRefresh = true); + }, /** - * Gets {@link ObservationRecord|ObservationRecords} that the {@link BindingObserver} - * is observing. + * Notifies subscribers of a source object of changes. + * @param source - the object to notify of changes. + * @param args - The change args to pass to subscribers. */ - records(): IterableIterator; -} + notify(source: unknown, args: any): void { + /* eslint-disable-next-line @typescript-eslint/no-use-before-define */ + getNotifier(source).notify(args); + }, -/** - * Common Observable APIs. - * @public - */ -export const Observable = FAST.getById(KernelServiceId.observable, () => { - const volatileRegex = /(:|&&|\|\||if)/; - const notifierLookup = new WeakMap(); - const accessorLookup = new WeakMap(); - const queueUpdate = DOM.queueUpdate; - let watcher: BindingObserverImplementation | undefined = void 0; - let createArrayObserver = (array: any[]): Notifier => { - throw new Error("Must call enableArrayObservation before observing arrays."); - }; - - function getNotifier(source: any): Notifier { - let found = source.$fastController || notifierLookup.get(source); - - if (found === void 0) { - if (Array.isArray(source)) { - found = createArrayObserver(source); - } else { - notifierLookup.set(source, (found = new PropertyChangeNotifier(source))); - } + /** + * Defines an observable property on an object or prototype. + * @param target - The target object to define the observable on. + * @param nameOrAccessor - The name of the property to define as observable; + * or a custom accessor that specifies the property name and accessor implementation. + */ + defineProperty(target: {}, nameOrAccessor: string | Accessor): void { + if (typeof nameOrAccessor === "string") { + nameOrAccessor = new DefaultObservableAccessor(nameOrAccessor); } - return found; - } + this.getAccessors(target).push(nameOrAccessor); - function getAccessors(target: {}): Accessor[] { + Reflect.defineProperty(target, nameOrAccessor.name, { + enumerable: true, + get(this: any) { + return (nameOrAccessor as Accessor).getValue(this); + }, + set(this: any, newValue: any) { + (nameOrAccessor as Accessor).setValue(this, newValue); + }, + }); + }, + + /** + * Finds all the observable accessors defined on the target, + * including its prototype chain. + * @param target - The target object to search for accessor on. + */ + getAccessors(target: {}): Accessor[] { let accessors = accessorLookup.get(target); if (accessors === void 0) { @@ -123,282 +169,50 @@ export const Observable = FAST.getById(KernelServiceId.observable, () => { currentTarget = Reflect.getPrototypeOf(currentTarget); } - if (accessors === void 0) { - accessors = []; - } else { - accessors = accessors.slice(0); - } + accessors = accessors === void 0 ? [] : accessors.slice(0); accessorLookup.set(target, accessors); } return accessors; - } - - class DefaultObservableAccessor implements Accessor { - private field: string; - private callback: string; - - constructor(public name: string) { - this.field = `_${name}`; - this.callback = `${name}Changed`; - } - - getValue(source: any): any { - if (watcher !== void 0) { - watcher.watch(source, this.name); - } - - return source[this.field]; - } - - setValue(source: any, newValue: any): void { - const field = this.field; - const oldValue = source[field]; - - if (oldValue !== newValue) { - source[field] = newValue; - - const callback = source[this.callback]; - - if (typeof callback === "function") { - callback.call(source, oldValue, newValue); - } - - getNotifier(source).notify(this.name); - } - } - } - - class BindingObserverImplementation - extends SubscriberSet - implements BindingObserver { - public needsRefresh: boolean = true; - private needsQueue: boolean = true; - - private first: SubscriptionRecord = this as any; - private last: SubscriptionRecord | null = null; - private propertySource: any = void 0; - private propertyName: string | undefined = void 0; - private notifier: Notifier | undefined = void 0; - private next: SubscriptionRecord | undefined = void 0; - - constructor( - private binding: Binding, - initialSubscriber?: Subscriber, - private isVolatileBinding: boolean = false - ) { - super(binding, initialSubscriber); - } - - public observe(source: TSource, context: ExecutionContext): TReturn { - if (this.needsRefresh && this.last !== null) { - this.disconnect(); - } - - const previousWatcher = watcher; - watcher = this.needsRefresh ? this : void 0; - this.needsRefresh = this.isVolatileBinding; - const result = this.binding(source, context); - watcher = previousWatcher; - - return result; - } - - public disconnect(): void { - if (this.last !== null) { - let current = this.first; - - while (current !== void 0) { - current.notifier.unsubscribe(this, current.propertyName); - current = current.next!; - } - - this.last = null; - this.needsRefresh = this.needsQueue = true; - } - } - - public watch(propertySource: unknown, propertyName: string): void { - const prev = this.last; - const notifier = getNotifier(propertySource); - const current: SubscriptionRecord = prev === null ? this.first : ({} as any); - - current.propertySource = propertySource; - current.propertyName = propertyName; - current.notifier = notifier; - - notifier.subscribe(this, propertyName); - - if (prev !== null) { - if (!this.needsRefresh) { - // Declaring the variable prior to assignment below circumvents - // a bug in Angular's optimization process causing infinite recursion - // of this watch() method. Details https://github.com/microsoft/fast/issues/4969 - let prevValue; - watcher = void 0; - /* eslint-disable-next-line */ - prevValue = prev.propertySource[prev.propertyName]; - /* eslint-disable-next-line @typescript-eslint/no-this-alias */ - watcher = this; - - if (propertySource === prevValue) { - this.needsRefresh = true; - } - } - - prev.next = current; - } - - this.last = current!; - } - - handleChange(): void { - if (this.needsQueue) { - this.needsQueue = false; - queueUpdate(this); - } - } - - call(): void { - if (this.last !== null) { - this.needsQueue = true; - this.notify(this); - } - } - - public records(): IterableIterator { - let next = this.first; - - return { - next: () => { - const current = next; - - if (current === undefined) { - return { value: void 0, done: true }; - } else { - next = next.next!; - return { - value: current, - done: false, - }; - } - }, - [Symbol.iterator]: function () { - return this; - }, - }; - } - } - - return Object.freeze({ - /** - * @internal - * @param factory - The factory used to create array observers. - */ - setArrayObserverFactory(factory: (collection: any[]) => Notifier): void { - createArrayObserver = factory; - }, + }, - /** - * Gets a notifier for an object or Array. - * @param source - The object or Array to get the notifier for. - */ - getNotifier, - - /** - * Records a property change for a source object. - * @param source - The object to record the change against. - * @param propertyName - The property to track as changed. - */ - track(source: unknown, propertyName: string): void { - if (watcher !== void 0) { - watcher.watch(source, propertyName); - } - }, - - /** - * Notifies watchers that the currently executing property getter or function is volatile - * with respect to its observable dependencies. - */ - trackVolatile(): void { - if (watcher !== void 0) { - watcher.needsRefresh = true; - } - }, - - /** - * Notifies subscribers of a source object of changes. - * @param source - the object to notify of changes. - * @param args - The change args to pass to subscribers. - */ - notify(source: unknown, args: any): void { - getNotifier(source).notify(args); - }, - - /** - * Defines an observable property on an object or prototype. - * @param target - The target object to define the observable on. - * @param nameOrAccessor - The name of the property to define as observable; - * or a custom accessor that specifies the property name and accessor implementation. - */ - defineProperty(target: {}, nameOrAccessor: string | Accessor): void { - if (typeof nameOrAccessor === "string") { - nameOrAccessor = new DefaultObservableAccessor(nameOrAccessor); - } - - getAccessors(target).push(nameOrAccessor); - - Reflect.defineProperty(target, nameOrAccessor.name, { - enumerable: true, - get: function (this: any) { - return (nameOrAccessor as Accessor).getValue(this); - }, - set: function (this: any, newValue: any) { - (nameOrAccessor as Accessor).setValue(this, newValue); - }, - }); - }, - - /** - * Finds all the observable accessors defined on the target, - * including its prototype chain. - * @param target - The target object to search for accessor on. - */ - getAccessors, - - /** - * Creates a {@link BindingObserver} that can watch the - * provided {@link Binding} for changes. - * @param binding - The binding to observe. - * @param initialSubscriber - An initial subscriber to changes in the binding value. - * @param isVolatileBinding - Indicates whether the binding's dependency list must be re-evaluated on every value evaluation. - */ - binding( - binding: Binding, - initialSubscriber?: Subscriber, - isVolatileBinding: boolean = this.isVolatileBinding(binding) - ): BindingObserver { - return new BindingObserverImplementation( - binding, - initialSubscriber, - isVolatileBinding - ); - }, + /** + * Creates a {@link BindingObserver} that can watch the + * provided {@link Binding} for changes. + * @param binding - The binding to observe. + * @param initialSubscriber - An initial subscriber to changes in the binding value. + * @param isVolatileBinding - Indicates whether the binding's dependency list must be re-evaluated on every value evaluation. + */ + binding( + binding: Binding, + initialSubscriber?: Subscriber, + isVolatileBinding: boolean = this.isVolatileBinding(binding) + ): BindingObserver { + /* eslint-disable-next-line @typescript-eslint/no-use-before-define */ + return new BindingObserverImplementation( + binding, + initialSubscriber, + isVolatileBinding + ); + }, - /** - * Determines whether a binding expression is volatile and needs to have its dependency list re-evaluated - * on every evaluation of the value. - * @param binding - The binding to inspect. - */ - isVolatileBinding( - binding: Binding - ): boolean { - return volatileRegex.test(binding.toString()); - }, - }); + /** + * Determines whether a binding expression is volatile and needs to have its dependency list re-evaluated + * on every evaluation of the value. + * @param binding - The binding to inspect. + */ + isVolatileBinding( + binding: Binding + ): boolean { + return volatileRegex.test(binding.toString()); + }, }); +const getNotifier = Observable.getNotifier; +const trackVolatile = Observable.trackVolatile; +const queueUpdate = DOM.queueUpdate; + /** * Decorator: Defines an observable property on the target. * @param target - The target to define the observable on. @@ -423,24 +237,21 @@ export function volatile( ): PropertyDescriptor { return Object.assign({}, descriptor, { get: function (this: any) { - Observable.trackVolatile(); + trackVolatile(); return descriptor.get!.apply(this); }, }); } -const contextEvent = FAST.getById(KernelServiceId.contextEvent, () => { - let current: Event | null = null; +let currentEvent: Event | null = null; - return { - get() { - return current; - }, - set(event: Event | null) { - current = event; - }, - }; -}); +/** + * @param event - The event to set as current for the context. + * @internal + */ +export function setCurrentEvent(event: Event | null): void { + currentEvent = event; +} /** * Provides additional contextual information available to behaviors and expressions. @@ -471,7 +282,7 @@ export class ExecutionContext { * The current event within an event handler. */ public get event(): Event { - return contextEvent.get()!; + return currentEvent!; } /** @@ -513,15 +324,6 @@ export class ExecutionContext { public get isLast(): boolean { return this.index === this.length - 1; } - - /** - * Sets the event for the current execution context. - * @param event - The event to set. - * @internal - */ - public static setEvent(event: Event | null): void { - contextEvent.set(event); - } } Observable.defineProperty(ExecutionContext.prototype, "index"); @@ -532,3 +334,170 @@ Observable.defineProperty(ExecutionContext.prototype, "length"); * @public */ export const defaultExecutionContext = Object.seal(new ExecutionContext()); + +/** + * The signature of an arrow function capable of being evaluated + * as part of a template binding update. + * @public + */ +export type Binding = ( + source: TSource, + context: ExecutionContext +) => TReturn; + +/** + * A record of observable property access. + * @public + */ +export interface ObservationRecord { + /** + * The source object with an observable property that was accessed. + */ + propertySource: any; + + /** + * The name of the observable property on {@link ObservationRecord.propertySource} that was accessed. + */ + propertyName: string; +} + +interface SubscriptionRecord extends ObservationRecord { + notifier: Notifier; + next: SubscriptionRecord | undefined; +} + +/** + * Enables evaluation of and subscription to a binding. + * @public + */ +/* eslint-disable-next-line @typescript-eslint/no-unused-vars */ +export interface BindingObserver + extends Notifier { + /** + * Begins observing the binding for the source and returns the current value. + * @param source - The source that the binding is based on. + * @param context - The execution context to execute the binding within. + * @returns The value of the binding. + */ + observe(source: TSource, context: ExecutionContext): TReturn; + + /** + * Unsubscribe from all dependent observables of the binding. + */ + disconnect(): void; + + /** + * Gets {@link ObservationRecord|ObservationRecords} that the {@link BindingObserver} + * is observing. + */ + records(): IterableIterator; +} + +class BindingObserverImplementation + extends SubscriberSet + implements BindingObserver { + public needsRefresh: boolean = true; + private needsQueue: boolean = true; + + private first: SubscriptionRecord = this as any; + private last: SubscriptionRecord | null = null; + private propertySource: any = void 0; + private propertyName: string | undefined = void 0; + private notifier: Notifier | undefined = void 0; + private next: SubscriptionRecord | undefined = void 0; + + constructor( + private binding: Binding, + initialSubscriber?: Subscriber, + private isVolatileBinding: boolean = false + ) { + super(binding, initialSubscriber); + } + + public observe(source: TSource, context: ExecutionContext): TReturn { + if (this.needsRefresh && this.last !== null) { + this.disconnect(); + } + + const previousWatcher = watcher; + watcher = this.needsRefresh ? this : void 0; + this.needsRefresh = this.isVolatileBinding; + const result = this.binding(source, context); + watcher = previousWatcher; + + return result; + } + + public disconnect(): void { + if (this.last !== null) { + let current = this.first; + + while (current !== void 0) { + current.notifier.unsubscribe(this, current.propertyName); + current = current.next!; + } + + this.last = null; + this.needsRefresh = this.needsQueue = true; + } + } + + /** @internal */ + public watch(propertySource: unknown, propertyName: string): void { + const prev = this.last; + const notifier = getNotifier(propertySource); + const current: SubscriptionRecord = prev === null ? this.first : ({} as any); + + current.propertySource = propertySource; + current.propertyName = propertyName; + current.notifier = notifier; + + notifier.subscribe(this, propertyName); + + if (prev !== null) { + if (!this.needsRefresh) { + // Declaring the variable prior to assignment below circumvents + // a bug in Angular's optimization process causing infinite recursion + // of this watch() method. Details https://github.com/microsoft/fast/issues/4969 + let prevValue; + watcher = void 0; + /* eslint-disable-next-line */ + prevValue = prev.propertySource[prev.propertyName]; + watcher = this; + + if (propertySource === prevValue) { + this.needsRefresh = true; + } + } + + prev.next = current; + } + + this.last = current!; + } + + /** @internal */ + handleChange(): void { + if (this.needsQueue) { + this.needsQueue = false; + queueUpdate(this); + } + } + + /** @internal */ + call(): void { + if (this.last !== null) { + this.needsQueue = true; + this.notify(this); + } + } + + public *records(): IterableIterator { + let next = this.first; + + while (next !== void 0) { + yield next; + next = next.next!; + } + } +} From e87dcfb5e908d339b28bae6e7764375d1199ffa1 Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Thu, 14 Oct 2021 23:12:49 -0400 Subject: [PATCH 019/135] refactor: tighten up notifier --- .../fast-element/src/observation/notifier.ts | 35 +++++++------------ 1 file changed, 12 insertions(+), 23 deletions(-) diff --git a/packages/web-components/fast-element/src/observation/notifier.ts b/packages/web-components/fast-element/src/observation/notifier.ts index 483521a0419..62912188caa 100644 --- a/packages/web-components/fast-element/src/observation/notifier.ts +++ b/packages/web-components/fast-element/src/observation/notifier.ts @@ -198,12 +198,7 @@ export class PropertyChangeNotifier implements Notifier { * @param propertyName - The property name, passed along to subscribers during notification. */ public notify(propertyName: string): void { - const subscribers = this.subscribers[propertyName]; - - if (subscribers !== void 0) { - subscribers.notify(propertyName); - } - + this.subscribers[propertyName]?.notify(propertyName); this.sourceSubscribers?.notify(propertyName); } @@ -213,22 +208,19 @@ export class PropertyChangeNotifier implements Notifier { * @param propertyToWatch - The name of the property that the subscriber is interested in watching for changes. */ public subscribe(subscriber: Subscriber, propertyToWatch?: string): void { - if (propertyToWatch) { - let subscribers = this.subscribers[propertyToWatch]; - - if (subscribers === void 0) { - this.subscribers[propertyToWatch] = subscribers = new SubscriberSet( - this.source - ); - } + let subscribers: SubscriberSet; - subscribers.subscribe(subscriber); + if (propertyToWatch) { + subscribers = + this.subscribers[propertyToWatch] ?? + (this.subscribers[propertyToWatch] = new SubscriberSet(this.source)); } else { - this.sourceSubscribers = - this.sourceSubscribers ?? new SubscriberSet(this.source); - - this.sourceSubscribers.subscribe(subscriber); + subscribers = + this.sourceSubscribers ?? + (this.sourceSubscribers = new SubscriberSet(this.source)); } + + subscribers.subscribe(subscriber); } /** @@ -238,10 +230,7 @@ export class PropertyChangeNotifier implements Notifier { */ public unsubscribe(subscriber: Subscriber, propertyToUnwatch?: string): void { if (propertyToUnwatch) { - const subscribers = this.subscribers[propertyToUnwatch]; - if (subscribers !== void 0) { - subscribers.unsubscribe(subscriber); - } + this.subscribers[propertyToUnwatch]?.unsubscribe(subscriber); } else { this.sourceSubscribers?.unsubscribe(subscriber); } From fdc4373b8b1b497a12a6dc520bab5da45c717bd4 Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Fri, 15 Oct 2021 09:08:36 -0400 Subject: [PATCH 020/135] refactor: clean up array observer --- .../src/observation/array-observer.ts | 177 ++++++++---------- 1 file changed, 81 insertions(+), 96 deletions(-) diff --git a/packages/web-components/fast-element/src/observation/array-observer.ts b/packages/web-components/fast-element/src/observation/array-observer.ts index 453378c0c14..06c88dcd025 100644 --- a/packages/web-components/fast-element/src/observation/array-observer.ts +++ b/packages/web-components/fast-element/src/observation/array-observer.ts @@ -5,8 +5,6 @@ import { Subscriber, SubscriberSet } from "./notifier.js"; import type { Notifier } from "./notifier.js"; import { Observable } from "./observable.js"; -let arrayObservationEnabled = false; - function adjustIndex(changeRecord: Splice, array: any[]): Splice { let index = changeRecord.index; const arrayLength = array.length; @@ -34,11 +32,7 @@ class ArrayObserver extends SubscriberSet { constructor(source: any[]) { super(source); - - Reflect.defineProperty(source, "$fastController", { - value: this, - enumerable: false, - }); + (source as any).$fastController = this; } public subscribe(subscriber: Subscriber): void { @@ -53,19 +47,12 @@ class ArrayObserver extends SubscriberSet { this.splices.push(splice); } - if (this.needsQueue) { - this.needsQueue = false; - DOM.queueUpdate(this); - } + this.enqueue(); } public reset(oldCollection: any[] | undefined): void { this.oldCollection = oldCollection; - - if (this.needsQueue) { - this.needsQueue = false; - DOM.queueUpdate(this); - } + this.enqueue(); } public flush(): void { @@ -94,67 +81,39 @@ class ArrayObserver extends SubscriberSet { this.notify(finalSplices); } -} -/* 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 (arrayObservationEnabled) { - return; - } - - arrayObservationEnabled = true; - - Observable.setArrayObserverFactory( - (collection: any[]): Notifier => { - return new ArrayObserver(collection); + private enqueue() { + if (this.needsQueue) { + this.needsQueue = false; + DOM.queueUpdate(this); } - ); - - const proto = Array.prototype; - - // Don't patch Array if it has already been patched - // by another copy of fast-element. - if ((proto as any).$fastPatch) { - return; } +} - Reflect.defineProperty(proto, "$fastPatch", { - value: 1, - enumerable: false, - }); - - 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; - - proto.pop = function () { +const proto = Array.prototype; +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 arrayOverrides = { + pop() { const notEmpty = this.length > 0; - const methodCallResult = pop.apply(this, arguments as any); - const o = (this as any).$fastController as ArrayObserver; + const result = pop.apply(this, arguments); + const o = this.$fastController as ArrayObserver; if (o !== void 0 && notEmpty) { - o.addSplice(newSplice(this.length, [methodCallResult], 0)); + o.addSplice(newSplice(this.length, [result], 0)); } - return methodCallResult; - }; + return result; + }, - proto.push = function () { - const methodCallResult = push.apply(this, arguments as any); - const o = (this as any).$fastController as ArrayObserver; + push() { + const result = push.apply(this, arguments); + const o = this.$fastController as ArrayObserver; if (o !== void 0) { o.addSplice( @@ -165,67 +124,67 @@ export function enableArrayObservation(): void { ); } - return methodCallResult; - }; + return result; + }, - proto.reverse = function () { + reverse() { let oldArray; - const o = (this as any).$fastController as ArrayObserver; + const o = this.$fastController as ArrayObserver; if (o !== void 0) { o.flush(); oldArray = this.slice(); } - const methodCallResult = reverse.apply(this, arguments as any); + const result = reverse.apply(this, arguments); if (o !== void 0) { o.reset(oldArray); } - return methodCallResult; - }; + return result; + }, - proto.shift = function () { + shift() { const notEmpty = this.length > 0; - const methodCallResult = shift.apply(this, arguments as any); - const o = (this as any).$fastController as ArrayObserver; + const result = shift.apply(this, arguments); + const o = this.$fastController as ArrayObserver; if (o !== void 0 && notEmpty) { - o.addSplice(newSplice(0, [methodCallResult], 0)); + o.addSplice(newSplice(0, [result], 0)); } - return methodCallResult; - }; + return result; + }, - proto.sort = function () { + sort() { let oldArray; - const o = (this as any).$fastController as ArrayObserver; + const o = this.$fastController as ArrayObserver; if (o !== void 0) { o.flush(); oldArray = this.slice(); } - const methodCallResult = sort.apply(this, arguments as any); + const result = sort.apply(this, arguments); if (o !== void 0) { o.reset(oldArray); } - return methodCallResult; - }; + return result; + }, - proto.splice = function () { - const methodCallResult = splice.apply(this, arguments as any); - const o = (this as any).$fastController as ArrayObserver; + splice() { + const result = splice.apply(this, arguments); + const o = this.$fastController as ArrayObserver; if (o !== void 0) { o.addSplice( adjustIndex( newSplice( +arguments[0], - methodCallResult, + result, arguments.length > 2 ? arguments.length - 2 : 0 ), this @@ -233,19 +192,45 @@ export function enableArrayObservation(): void { ); } - return methodCallResult; - }; + return result; + }, - proto.unshift = function () { - const methodCallResult = unshift.apply(this, arguments as any); - const o = (this as any).$fastController as ArrayObserver; + unshift() { + const result = unshift.apply(this, arguments); + const o = this.$fastController as ArrayObserver; if (o !== void 0) { o.addSplice(adjustIndex(newSplice(0, [], arguments.length), this)); } - return methodCallResult; - }; + return result; + }, +}; + +/* 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 ((proto as any).$fastObservation) { + return; + } + + (proto as any).$fastObservation = true; + + Observable.setArrayObserverFactory( + (collection: any[]): Notifier => { + return new ArrayObserver(collection); + } + ); + + Object.assign(proto, arrayOverrides); } /* eslint-enable prefer-rest-params */ /* eslint-enable @typescript-eslint/explicit-function-return-type */ From a29f9514cd9fa15a8600413c0040e5bd98841641 Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Fri, 15 Oct 2021 15:12:09 -0400 Subject: [PATCH 021/135] refactor: clean up class binding scenarios --- .../fast-element/src/observation/array-observer.ts | 4 +++- .../fast-element/src/templating/binding.ts | 10 ++-------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/packages/web-components/fast-element/src/observation/array-observer.ts b/packages/web-components/fast-element/src/observation/array-observer.ts index 06c88dcd025..eea12acf01d 100644 --- a/packages/web-components/fast-element/src/observation/array-observer.ts +++ b/packages/web-components/fast-element/src/observation/array-observer.ts @@ -69,7 +69,9 @@ class ArrayObserver extends SubscriberSet { const finalSplices = oldCollection === void 0 - ? projectArraySplices(this.source, splices!) + ? splices!.length > 1 + ? projectArraySplices(this.source, splices!) + : splices : calcSplices( this.source, 0, diff --git a/packages/web-components/fast-element/src/templating/binding.ts b/packages/web-components/fast-element/src/templating/binding.ts index 0f8831d6630..fa619a40105 100644 --- a/packages/web-components/fast-element/src/templating/binding.ts +++ b/packages/web-components/fast-element/src/templating/binding.ts @@ -143,10 +143,6 @@ function updatePropertyTarget(this: BindingBehavior, value: unknown): void { this.target[this.targetName!] = value; } -function updateClassTarget(this: BindingBehavior, value: string): void { - this.target.className = value; -} - function updateClassListTarget(this: BindingBehavior, value: string): void { const classVersions = this.classVersions || Object.create(null); const target = this.target; @@ -235,9 +231,6 @@ export class HTMLBindingDirective extends TargetedHTMLDirective { case "classList": this.updateTarget = updateClassListTarget; break; - case "className": - this.updateTarget = updateClassTarget; - break; default: this.updateTarget = updatePropertyTarget; break; @@ -256,7 +249,8 @@ export class HTMLBindingDirective extends TargetedHTMLDirective { this.cleanedTargetName = value; if (value === "class") { - this.updateTarget = updateClassTarget; + this.cleanedTargetName = "className"; + this.updateTarget = updatePropertyTarget; } break; From cffaa0499fb0bae6a88382a511ce0443e472bc94 Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Fri, 15 Oct 2021 15:47:44 -0400 Subject: [PATCH 022/135] refactor: clean up more internal of css and observable --- .../fast-element/src/observation/observable.ts | 2 +- packages/web-components/fast-element/src/styles/css.ts | 9 +-------- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/packages/web-components/fast-element/src/observation/observable.ts b/packages/web-components/fast-element/src/observation/observable.ts index 7032f270427..bea3a8016bf 100644 --- a/packages/web-components/fast-element/src/observation/observable.ts +++ b/packages/web-components/fast-element/src/observation/observable.ts @@ -236,7 +236,7 @@ export function volatile( descriptor: PropertyDescriptor ): PropertyDescriptor { return Object.assign({}, descriptor, { - get: function (this: any) { + get(this: any) { trackVolatile(); return descriptor.get!.apply(this); }, diff --git a/packages/web-components/fast-element/src/styles/css.ts b/packages/web-components/fast-element/src/styles/css.ts index e97968abf3f..da413894150 100644 --- a/packages/web-components/fast-element/src/styles/css.ts +++ b/packages/web-components/fast-element/src/styles/css.ts @@ -61,14 +61,8 @@ export function css( ...values: (ComposableStyles | CSSDirective)[] ): ElementStyles { const { styles, behaviors } = collectStyles(strings, values); - const elementStyles = ElementStyles.create(styles); - - if (behaviors.length) { - elementStyles.withBehaviors(...behaviors); - } - - return elementStyles; + return behaviors.length ? elementStyles.withBehaviors(...behaviors) : elementStyles; } class CSSPartial extends CSSDirective implements Behavior { @@ -140,6 +134,5 @@ export function cssPartial( ...values: (ComposableStyles | CSSDirective)[] ): CSSDirective { const { styles, behaviors } = collectStyles(strings, values); - return new CSSPartial(styles, behaviors); } From 7f665146482b4462c49b0ee4664b729ae7e8567f Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Fri, 15 Oct 2021 16:06:32 -0400 Subject: [PATCH 023/135] feat: switch ref over to the new stateless model for view behaviors --- .../fast-element/docs/api-report.md | 8 ++-- .../docs/fast-element-2-changes.md | 3 +- .../fast-element/src/templating/ref.ts | 48 ++++++++++++------- 3 files changed, 39 insertions(+), 20 deletions(-) diff --git a/packages/web-components/fast-element/docs/api-report.md b/packages/web-components/fast-element/docs/api-report.md index 2effc538026..4d8413edb5d 100644 --- a/packages/web-components/fast-element/docs/api-report.md +++ b/packages/web-components/fast-element/docs/api-report.md @@ -431,9 +431,11 @@ export class PropertyChangeNotifier implements Notifier { export function ref(propertyName: keyof T & string): CaptureType; // @public -export class RefBehavior implements Behavior { - constructor(targets: ViewBehaviorTargets, targetId: string, propertyName: string); - bind(source: any): void; +export class RefDirective extends HTMLDirective implements ViewBehavior { + constructor(propertyName: string); + bind(source: any, context: ExecutionContext, targets: ViewBehaviorTargets): void; + createBehavior(): this; + createPlaceholder(index: number): string; unbind(): void; } diff --git a/packages/web-components/fast-element/docs/fast-element-2-changes.md b/packages/web-components/fast-element/docs/fast-element-2-changes.md index 8937c3003ae..a7a1baed71b 100644 --- a/packages/web-components/fast-element/docs/fast-element-2-changes.md +++ b/packages/web-components/fast-element/docs/fast-element-2-changes.md @@ -8,4 +8,5 @@ * `ElementViewTemplate`, `SyntheticViewTemplate`, and `ViewTemplate` - Added type parameters throughout. Logic to instantiate and apply behaviors moved out of the template and into the view where it can be lazily executed. Removed the ability of the `render` method to take a string id of the node to render to. You must provide a node. * `DOM` - Tree Walker methods are no longer used and are thus removed. The API for removing child nodes has been removed as well since it was only used in one place and could be inlined. * `class` - Bindings to `class` are now more nuanced. Binding directly to `class` will simply set the `className` property. If you need to bind to `class` knowing that manual JS will also manipulate the `classList` in addition to the binding, then you should now bind to `:classList` instead. This allows for performance optimizations in the simple, most common case. -* `Behavior` and `ViewBehavior` - `Behavior` now requires an `ExecutionContext` for `unbind`. Behaviors can be used for elements or views. `ViewBehavior` has been introduced for use exclusively with views, and provides some optimization opportunities. \ No newline at end of file +* `Behavior` and `ViewBehavior` - `Behavior` now requires an `ExecutionContext` for `unbind`. Behaviors can be used for elements or views. `ViewBehavior` has been introduced for use exclusively with views, and provides some optimization opportunities. +* `RefBehavior` has been replaced with `RefDirective`. The directive also implements `ViewBehavior` allowing a single directive instance to be shared across all template instances that use the ref. \ No newline at end of file diff --git a/packages/web-components/fast-element/src/templating/ref.ts b/packages/web-components/fast-element/src/templating/ref.ts index 4a97e2487ba..cc750666d0f 100644 --- a/packages/web-components/fast-element/src/templating/ref.ts +++ b/packages/web-components/fast-element/src/templating/ref.ts @@ -1,34 +1,50 @@ -import type { Behavior } from "../observation/behavior"; import type { CaptureType } from "./template"; -import { AttachedBehaviorHTMLDirective, ViewBehaviorTargets } from "./html-directive"; +import { HTMLDirective, ViewBehavior, ViewBehaviorTargets } from "./html-directive"; +import type { ExecutionContext } from "../observation/observable"; +import { DOM } from "../dom"; /** * The runtime behavior for template references. * @public */ -export class RefBehavior implements Behavior { - private target: Node; - +export class RefDirective extends HTMLDirective implements ViewBehavior { /** - * Creates an instance of RefBehavior. - * @param target - The element to reference. + * Creates an instance of RefDirective. * @param propertyName - The name of the property to assign the reference to. */ - public constructor( - targets: ViewBehaviorTargets, - targetId: string, - private propertyName: string - ) { - this.target = targets[targetId]; + public constructor(private propertyName: string) { + super(); + } + + /** + * Creates a behavior. + */ + createBehavior() { + return this; + } + + /** + * Creates a placeholder string based on the directive's index within the template. + * @param index - The index of the directive within the template. + * @remarks + * Creates a custom attribute placeholder. + */ + public createPlaceholder(index: number): string { + return DOM.createCustomAttributePlaceholder("fast-ref", index); } /** * Bind this behavior to the source. * @param source - The source to bind to. * @param context - The execution context that the binding is operating within. + * @param targets - The targets that behaviors in a view can attach to. */ - public bind(source: any): void { - source[this.propertyName] = this.target; + public bind( + source: any, + context: ExecutionContext, + targets: ViewBehaviorTargets + ): void { + source[this.propertyName] = targets[this.targetId]; } /** @@ -45,5 +61,5 @@ export class RefBehavior implements Behavior { * @public */ export function ref(propertyName: keyof T & string): CaptureType { - return new AttachedBehaviorHTMLDirective("fast-ref", RefBehavior, propertyName); + return new RefDirective(propertyName); } From 61a4e28cce2c85e7d98682e736a37fcba755ba40 Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Fri, 15 Oct 2021 16:19:45 -0400 Subject: [PATCH 024/135] refactor: cleanup in node observation --- .../src/templating/node-observation.ts | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/packages/web-components/fast-element/src/templating/node-observation.ts b/packages/web-components/fast-element/src/templating/node-observation.ts index c3e136eae37..f20d98c197b 100644 --- a/packages/web-components/fast-element/src/templating/node-observation.ts +++ b/packages/web-components/fast-element/src/templating/node-observation.ts @@ -29,21 +29,17 @@ export interface NodeBehaviorOptions { */ export type ElementsFilter = (value: Node, index: number, array: Node[]) => boolean; +const selectElements = value => value.nodeType === 1; + /** * Creates a function that can be used to filter a Node array, selecting only elements. * @param selector - An optional selector to restrict the filter to. * @public */ export function elements(selector?: string): ElementsFilter { - if (selector) { - return function (value: Node, index: number, array: Node[]): boolean { - return value.nodeType === 1 && (value as HTMLElement).matches(selector); - }; - } - - return function (value: Node, index: number, array: Node[]): boolean { - return value.nodeType === 1; - }; + return selector + ? value => value.nodeType === 1 && (value as HTMLElement).matches(selector) + : selectElements; } /** From ab57fa54450edb15ea1249e5b445bb6a71198c59 Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Mon, 18 Oct 2021 10:31:27 -0400 Subject: [PATCH 025/135] feat: createCustomAttributePlaceholder no longer requires an attr name --- packages/web-components/fast-element/docs/api-report.md | 4 ++-- .../fast-element/docs/fast-element-2-changes.md | 2 +- packages/web-components/fast-element/src/dom.ts | 5 +++-- .../fast-element/src/templating/children.ts | 6 +----- .../fast-element/src/templating/html-directive.ts | 9 ++------- .../web-components/fast-element/src/templating/ref.ts | 2 +- .../fast-element/src/templating/slotted.ts | 6 +----- 7 files changed, 11 insertions(+), 23 deletions(-) diff --git a/packages/web-components/fast-element/docs/api-report.md b/packages/web-components/fast-element/docs/api-report.md index 4d8413edb5d..387866dd636 100644 --- a/packages/web-components/fast-element/docs/api-report.md +++ b/packages/web-components/fast-element/docs/api-report.md @@ -16,7 +16,7 @@ export interface Accessor { // @public export class AttachedBehaviorHTMLDirective extends HTMLDirective { - constructor(name: string, behavior: AttachedBehaviorType, options: T); + constructor(behavior: AttachedBehaviorType, options: T); createBehavior(targets: ViewBehaviorTargets): ViewBehavior; createPlaceholder(index: number): string; } @@ -205,7 +205,7 @@ export const DOM: Readonly<{ isMarker(node: Node): node is Comment; extractDirectiveIndexFromMarker(node: Comment): number; createInterpolationPlaceholder(index: number): string; - createCustomAttributePlaceholder(attributeName: string, index: number): string; + createCustomAttributePlaceholder(index: number): string; createBlockPlaceholder(index: number): string; queueUpdate(callable: Callable): void; nextUpdate(): Promise; diff --git a/packages/web-components/fast-element/docs/fast-element-2-changes.md b/packages/web-components/fast-element/docs/fast-element-2-changes.md index a7a1baed71b..6d5ff5f77da 100644 --- a/packages/web-components/fast-element/docs/fast-element-2-changes.md +++ b/packages/web-components/fast-element/docs/fast-element-2-changes.md @@ -6,7 +6,7 @@ * `compileTemplate()` - Internals have been significantly changed. The implementation no longer uses a TreeWalker. The return type has change to an `HTMLTemplateCompilationResult` with different properties. * `View` and `HTMLView` - Type parameters added to enable strongly typed views based on their data source. The constructor of `HTMLView` has a new signature based on changes to the compiler's output. Internals have been cleaned up and no longer rely on the Range type. * `ElementViewTemplate`, `SyntheticViewTemplate`, and `ViewTemplate` - Added type parameters throughout. Logic to instantiate and apply behaviors moved out of the template and into the view where it can be lazily executed. Removed the ability of the `render` method to take a string id of the node to render to. You must provide a node. -* `DOM` - Tree Walker methods are no longer used and are thus removed. The API for removing child nodes has been removed as well since it was only used in one place and could be inlined. +* `DOM` - Tree Walker methods are no longer used and are thus removed. The API for removing child nodes has been removed as well since it was only used in one place and could be inlined. The helper `createCustomAttributePlaceholder()` no longer requires an attribute name. It will be uniquely generated internally. * `class` - Bindings to `class` are now more nuanced. Binding directly to `class` will simply set the `className` property. If you need to bind to `class` knowing that manual JS will also manipulate the `classList` in addition to the binding, then you should now bind to `:classList` instead. This allows for performance optimizations in the simple, most common case. * `Behavior` and `ViewBehavior` - `Behavior` now requires an `ExecutionContext` for `unbind`. Behaviors can be used for elements or views. `ViewBehavior` has been introduced for use exclusively with views, and provides some optimization opportunities. * `RefBehavior` has been replaced with `RefDirective`. The directive also implements `ViewBehavior` allowing a single directive instance to be shared across all template instances that use the ref. \ No newline at end of file diff --git a/packages/web-components/fast-element/src/dom.ts b/packages/web-components/fast-element/src/dom.ts index 60a880f7082..a47d2e20d30 100644 --- a/packages/web-components/fast-element/src/dom.ts +++ b/packages/web-components/fast-element/src/dom.ts @@ -30,6 +30,7 @@ function tryRunTask(task: Callable): void { } const marker = `fast-${Math.random().toString(36).substring(2, 8)}`; +let attrId = 0; /** @internal */ export const _interpolationStart = `${marker}{`; @@ -110,8 +111,8 @@ export const DOM = Object.freeze({ * @remarks * Used internally by attribute directives such as `ref`, `slotted`, and `children`. */ - createCustomAttributePlaceholder(attributeName: string, index: number): string { - return `${attributeName}="${this.createInterpolationPlaceholder(index)}"`; + createCustomAttributePlaceholder(index: number): string { + return `${marker}-${++attrId}="${this.createInterpolationPlaceholder(index)}"`; }, /** diff --git a/packages/web-components/fast-element/src/templating/children.ts b/packages/web-components/fast-element/src/templating/children.ts index aeb797810cc..b10c1dbe875 100644 --- a/packages/web-components/fast-element/src/templating/children.ts +++ b/packages/web-components/fast-element/src/templating/children.ts @@ -103,9 +103,5 @@ export function children( }; } - return new AttachedBehaviorHTMLDirective( - "fast-children", - ChildrenBehavior, - propertyOrOptions as any - ); + return new AttachedBehaviorHTMLDirective(ChildrenBehavior, propertyOrOptions as any); } 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 fe63a78ed14..a193269ddc9 100644 --- a/packages/web-components/fast-element/src/templating/html-directive.ts +++ b/packages/web-components/fast-element/src/templating/html-directive.ts @@ -118,15 +118,10 @@ export type AttachedBehaviorType = new ( export class AttachedBehaviorHTMLDirective extends HTMLDirective { /** * - * @param name - The name of the behavior; used as a custom attribute on the element. * @param behavior - The behavior to instantiate and attach to the element. * @param options - Options to pass to the behavior during creation. */ - public constructor( - private name: string, - private behavior: AttachedBehaviorType, - private options: T - ) { + public constructor(private behavior: AttachedBehaviorType, private options: T) { super(); } @@ -137,7 +132,7 @@ export class AttachedBehaviorHTMLDirective extends HTMLDirective { * Creates a custom attribute placeholder. */ public createPlaceholder(index: number): string { - return DOM.createCustomAttributePlaceholder(this.name, index); + return DOM.createCustomAttributePlaceholder(index); } /** diff --git a/packages/web-components/fast-element/src/templating/ref.ts b/packages/web-components/fast-element/src/templating/ref.ts index cc750666d0f..3da12b67960 100644 --- a/packages/web-components/fast-element/src/templating/ref.ts +++ b/packages/web-components/fast-element/src/templating/ref.ts @@ -30,7 +30,7 @@ export class RefDirective extends HTMLDirective implements ViewBehavior { * Creates a custom attribute placeholder. */ public createPlaceholder(index: number): string { - return DOM.createCustomAttributePlaceholder("fast-ref", index); + return DOM.createCustomAttributePlaceholder(index); } /** diff --git a/packages/web-components/fast-element/src/templating/slotted.ts b/packages/web-components/fast-element/src/templating/slotted.ts index 38beb15f8eb..947857db410 100644 --- a/packages/web-components/fast-element/src/templating/slotted.ts +++ b/packages/web-components/fast-element/src/templating/slotted.ts @@ -63,9 +63,5 @@ export function slotted( propertyOrOptions = { property: propertyOrOptions }; } - return new AttachedBehaviorHTMLDirective( - "fast-slotted", - SlottedBehavior, - propertyOrOptions as any - ); + return new AttachedBehaviorHTMLDirective(SlottedBehavior, propertyOrOptions as any); } From f3fb0b798ddedea1d3eebaad3c0d93dfd0af3f65 Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Mon, 18 Oct 2021 14:22:56 -0400 Subject: [PATCH 026/135] feat: update children and slotted to share instances --- .../fast-element/docs/api-report.md | 47 +++---- .../docs/fast-element-2-changes.md | 4 +- .../src/templating/children.spec.ts | 48 ++++--- .../fast-element/src/templating/children.ts | 75 ++++++----- .../src/templating/html-directive.ts | 47 ------- .../src/templating/node-observation.ts | 125 +++++++++++------- .../src/templating/slotted.spec.ts | 42 +++--- .../fast-element/src/templating/slotted.ts | 50 ++++--- 8 files changed, 215 insertions(+), 223 deletions(-) diff --git a/packages/web-components/fast-element/docs/api-report.md b/packages/web-components/fast-element/docs/api-report.md index 387866dd636..374e4fddf20 100644 --- a/packages/web-components/fast-element/docs/api-report.md +++ b/packages/web-components/fast-element/docs/api-report.md @@ -14,16 +14,6 @@ export interface Accessor { setValue(source: any, value: any): void; } -// @public -export class AttachedBehaviorHTMLDirective extends HTMLDirective { - constructor(behavior: AttachedBehaviorType, options: T); - createBehavior(targets: ViewBehaviorTargets): ViewBehavior; - createPlaceholder(index: number): string; - } - -// @public -export type AttachedBehaviorType = new (targets: ViewBehaviorTargets, targetId: string, options: T) => Behavior; - // @public export function attr(config?: DecoratorAttributeConfiguration): (target: {}, property: string) => void; @@ -123,24 +113,24 @@ export interface CaptureType { } // @public -export interface ChildListBehaviorOptions extends NodeBehaviorOptions, Omit { +export interface ChildListDirectiveOptions extends NodeBehaviorOptions, Omit { } // @public -export function children(propertyOrOptions: (keyof T & string) | ChildrenBehaviorOptions): CaptureType; +export function children(propertyOrOptions: (keyof T & string) | ChildListDirectiveOptions): CaptureType; -// Warning: (ae-forgotten-export) The symbol "NodeObservationBehavior" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "NodeObservationDirective" needs to be exported by the entry point index.d.ts // // @public -export class ChildrenBehavior extends NodeObservationBehavior { - constructor(targets: ViewBehaviorTargets, targetId: string, options: ChildrenBehaviorOptions); - disconnect(): void; - protected getNodes(): ChildNode[]; - observe(): void; - } +export class ChildrenDirective extends NodeObservationDirective { + constructor(options: ChildrenDirectiveOptions); + disconnect(target: any): void; + getNodes(target: Element): Node[]; + observe(target: any): void; +} // @public -export type ChildrenBehaviorOptions = ChildListBehaviorOptions | SubtreeBehaviorOptions; +export type ChildrenDirectiveOptions = ChildListDirectiveOptions | SubtreeDirectiveOptions; // @public export function compileTemplate(template: HTMLTemplateElement, directives: ReadonlyArray): HTMLTemplateCompilationResult; @@ -470,18 +460,19 @@ export interface RepeatOptions { export function setCurrentEvent(event: Event | null): void; // @public -export function slotted(propertyOrOptions: (keyof T & string) | SlottedBehaviorOptions): CaptureType; +export function slotted(propertyOrOptions: (keyof T & string) | SlottedDirectiveOptions): CaptureType; // @public -export class SlottedBehavior extends NodeObservationBehavior { - constructor(targets: ViewBehaviorTargets, targetId: string, options: SlottedBehaviorOptions); - disconnect(): void; - protected getNodes(): Node[]; - observe(): void; +export class SlottedDirective extends NodeObservationDirective { + disconnect(target: EventSource): void; + getNodes(target: HTMLSlotElement): Node[]; + // @internal (undocumented) + handleEvent(event: Event): void; + observe(target: EventSource): void; } // @public -export interface SlottedBehaviorOptions extends NodeBehaviorOptions, AssignedNodesOptions { +export interface SlottedDirectiveOptions extends NodeBehaviorOptions, AssignedNodesOptions { } // @public @@ -517,7 +508,7 @@ export class SubscriberSet implements Notifier { } // @public -export interface SubtreeBehaviorOptions extends Omit, "filter">, Omit { +export interface SubtreeDirectiveOptions extends Omit, "filter">, Omit { selector: string; subtree: boolean; } diff --git a/packages/web-components/fast-element/docs/fast-element-2-changes.md b/packages/web-components/fast-element/docs/fast-element-2-changes.md index 6d5ff5f77da..ef65794c438 100644 --- a/packages/web-components/fast-element/docs/fast-element-2-changes.md +++ b/packages/web-components/fast-element/docs/fast-element-2-changes.md @@ -9,4 +9,6 @@ * `DOM` - Tree Walker methods are no longer used and are thus removed. The API for removing child nodes has been removed as well since it was only used in one place and could be inlined. The helper `createCustomAttributePlaceholder()` no longer requires an attribute name. It will be uniquely generated internally. * `class` - Bindings to `class` are now more nuanced. Binding directly to `class` will simply set the `className` property. If you need to bind to `class` knowing that manual JS will also manipulate the `classList` in addition to the binding, then you should now bind to `:classList` instead. This allows for performance optimizations in the simple, most common case. * `Behavior` and `ViewBehavior` - `Behavior` now requires an `ExecutionContext` for `unbind`. Behaviors can be used for elements or views. `ViewBehavior` has been introduced for use exclusively with views, and provides some optimization opportunities. -* `RefBehavior` has been replaced with `RefDirective`. The directive also implements `ViewBehavior` allowing a single directive instance to be shared across all template instances that use the ref. \ No newline at end of file +* `RefBehavior` has been replaced with `RefDirective`. The directive also implements `ViewBehavior` allowing a single directive instance to be shared across all template instances that use the ref. +* Removed `SlottedBehavior` and `ChildrenBehavior` have been replaced with `SlottedDirective` and `ChildrenDirective`. These directives allow a single directive instance to be shared across all template instances that use the ref. +* Removed `AttachedBehaviorHTMLDirective` and `AttachedBehaviorType` since they are no longer used in the new directive/behavior architecture for ref, slotted, and children. \ No newline at end of file diff --git a/packages/web-components/fast-element/src/templating/children.spec.ts b/packages/web-components/fast-element/src/templating/children.spec.ts index 0689e7f7f5e..0b68b60a665 100644 --- a/packages/web-components/fast-element/src/templating/children.spec.ts +++ b/packages/web-components/fast-element/src/templating/children.spec.ts @@ -1,27 +1,25 @@ import { expect } from "chai"; -import { children, ChildrenBehavior } from "./children"; -import { AttachedBehaviorHTMLDirective } from "./html-directive"; -import { observable } from "../observation/observable"; +import { children, ChildrenDirective } from "./children"; +import { defaultExecutionContext, observable } from "../observation/observable"; import { elements } from "./node-observation"; import { DOM } from "../dom"; describe("The children", () => { context("template function", () => { - it("returns an AttachedBehaviorDirective", () => { + it("returns an ChildrenDirective", () => { const directive = children("test"); - expect(directive).to.be.instanceOf(AttachedBehaviorHTMLDirective); + expect(directive).to.be.instanceOf(ChildrenDirective); }); }); context("directive", () => { - it("creates a ChildrenBehavior", () => { + it("creates a behavior by returning itself", () => { const targetId = 'r'; - const directive = children("test") as AttachedBehaviorHTMLDirective; + const directive = children("test") as ChildrenDirective; const target = document.createElement("div"); const targets = { [targetId]: target }; const behavior = directive.createBehavior(targets); - - expect(behavior).to.be.instanceOf(ChildrenBehavior); + expect(behavior).to.equal(behavior); }); }); @@ -53,37 +51,40 @@ describe("The children", () => { it("gathers child nodes", () => { const { host, children, targets, targetId } = createDOM(); - const behavior = new ChildrenBehavior(targets, targetId, { + const behavior = new ChildrenDirective({ property: "nodes", }); + behavior.targetId = targetId; const model = new Model(); - behavior.bind(model); + behavior.bind(model, defaultExecutionContext, targets); expect(model.nodes).members(children); }); it("gathers child nodes with a filter", () => { const { host, children, targets, targetId } = createDOM("foo-bar"); - const behavior = new ChildrenBehavior(targets, targetId, { + const behavior = new ChildrenDirective({ property: "nodes", filter: elements("foo-bar"), }); + behavior.targetId = targetId; const model = new Model(); - behavior.bind(model); + behavior.bind(model, defaultExecutionContext, targets); expect(model.nodes).members(children.filter(elements("foo-bar"))); }); it("updates child nodes when they change", async () => { const { host, children, targets, targetId } = createDOM("foo-bar"); - const behavior = new ChildrenBehavior(targets, targetId, { + const behavior = new ChildrenDirective({ property: "nodes", }); + behavior.targetId = targetId; const model = new Model(); - behavior.bind(model); + behavior.bind(model, defaultExecutionContext, targets); expect(model.nodes).members(children); @@ -96,13 +97,14 @@ describe("The children", () => { it("updates child nodes when they change with a filter", async () => { const { host, children, targets, targetId } = createDOM("foo-bar"); - const behavior = new ChildrenBehavior(targets, targetId, { + const behavior = new ChildrenDirective({ property: "nodes", filter: elements("foo-bar"), }); + behavior.targetId = targetId; const model = new Model(); - behavior.bind(model); + behavior.bind(model, defaultExecutionContext, targets); expect(model.nodes).members(children); @@ -126,15 +128,16 @@ describe("The children", () => { } } - const behavior = new ChildrenBehavior(targets, targetId, { + const behavior = new ChildrenDirective({ property: "nodes", subtree: true, selector: subtreeElement, }); + behavior.targetId = targetId; const model = new Model(); - behavior.bind(model); + behavior.bind(model, defaultExecutionContext, targets); expect(model.nodes).members(subtreeChildren); @@ -155,16 +158,17 @@ describe("The children", () => { it("clears and unwatches when unbound", async () => { const { host, children, targets, targetId } = createDOM("foo-bar"); - const behavior = new ChildrenBehavior(targets, targetId, { + const behavior = new ChildrenDirective({ property: "nodes", }); + behavior.targetId = targetId; const model = new Model(); - behavior.bind(model); + behavior.bind(model, defaultExecutionContext, targets); expect(model.nodes).members(children); - behavior.unbind(); + behavior.unbind(model, defaultExecutionContext, targets); expect(model.nodes).members([]); diff --git a/packages/web-components/fast-element/src/templating/children.ts b/packages/web-components/fast-element/src/templating/children.ts index b10c1dbe875..437f8097cfa 100644 --- a/packages/web-components/fast-element/src/templating/children.ts +++ b/packages/web-components/fast-element/src/templating/children.ts @@ -1,12 +1,11 @@ -import { AttachedBehaviorHTMLDirective, ViewBehaviorTargets } from "./html-directive"; -import { NodeBehaviorOptions, NodeObservationBehavior } from "./node-observation"; +import { NodeObservationDirective, NodeBehaviorOptions } from "./node-observation"; import type { CaptureType } from "./template"; /** * The options used to configure child list observation. * @public */ -export interface ChildListBehaviorOptions +export interface ChildListDirectiveOptions extends NodeBehaviorOptions, Omit {} @@ -14,7 +13,7 @@ export interface ChildListBehaviorOptions * The options used to configure subtree observation. * @public */ -export interface SubtreeBehaviorOptions +export interface SubtreeDirectiveOptions extends Omit, "filter">, Omit { /** @@ -33,59 +32,67 @@ export interface SubtreeBehaviorOptions * The options used to configure child/subtree node observation. * @public */ -export type ChildrenBehaviorOptions = - | ChildListBehaviorOptions - | SubtreeBehaviorOptions; +export type ChildrenDirectiveOptions = + | ChildListDirectiveOptions + | SubtreeDirectiveOptions; /** * The runtime behavior for child node observation. * @public */ -export class ChildrenBehavior extends NodeObservationBehavior { - private observer: MutationObserver | null = null; - +export class ChildrenDirective extends NodeObservationDirective< + ChildrenDirectiveOptions +> { /** - * Creates an instance of ChildrenBehavior. - * @param target - The element target to observe children on. - * @param options - The options to use when observing the element children. + * Creates an instance of ChildrenDirective. + * @param options - The options to use in configuring the child observation behavior. */ - public constructor( - targets: ViewBehaviorTargets, - targetId: string, - options: ChildrenBehaviorOptions - ) { - super(targets[targetId] as HTMLElement, options); + constructor(options: ChildrenDirectiveOptions) { + super(options); (options as MutationObserverInit).childList = true; } /** * Begins observation of the nodes. + * @param target - The target to observe. */ - public observe(): void { - if (this.observer === null) { - this.observer = new MutationObserver(this.handleEvent.bind(this)); - } - - this.observer.observe(this.target, this.options); + observe(target: any) { + const observerId = `${this.targetId}-observer`; + const observer = + target[observerId] ?? + (target[observerId] = new MutationObserver(this.handleEvent)); + observer.$fastTarget = target; + observer.observe(target, this.options); } /** * Disconnects observation of the nodes. + * @param target - The target to unobserve. */ - public disconnect(): void { - this.observer!.disconnect(); + disconnect(target: any) { + const observerId = `${this.targetId}-observer`; + const observer = target[observerId]; + observer.$fastTarget = null; + observer.disconnect(); } /** - * Retrieves the nodes that should be assigned to the target. + * Retrieves the raw nodes that should be assigned to the source property. + * @param target - The target to get the node to. */ - protected getNodes(): ChildNode[] { + getNodes(target: Element): Node[] { if ("subtree" in this.options) { - return Array.from(this.target.querySelectorAll(this.options.selector)); + return Array.from(target.querySelectorAll(this.options.selector)); } - return Array.from(this.target.childNodes); + return Array.from(target.childNodes); } + + private handleEvent = (mutations: MutationRecord[], observer: any): void => { + const target = observer.$fastTarget; + const source = target.$fastSource; + this.updateTarget(source, this.computeNodes(target)); + }; } /** @@ -95,7 +102,7 @@ export class ChildrenBehavior extends NodeObservationBehavior( - propertyOrOptions: (keyof T & string) | ChildrenBehaviorOptions + propertyOrOptions: (keyof T & string) | ChildListDirectiveOptions ): CaptureType { if (typeof propertyOrOptions === "string") { propertyOrOptions = { @@ -103,5 +110,7 @@ export function children( }; } - return new AttachedBehaviorHTMLDirective(ChildrenBehavior, propertyOrOptions as any); + return new ChildrenDirective( + propertyOrOptions as ChildListDirectiveOptions + ); } 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 a193269ddc9..b08affb38db 100644 --- a/packages/web-components/fast-element/src/templating/html-directive.ts +++ b/packages/web-components/fast-element/src/templating/html-directive.ts @@ -99,50 +99,3 @@ export abstract class TargetedHTMLDirective extends HTMLDirective { public createPlaceholder: (index: number) => string = DOM.createInterpolationPlaceholder; } - -/** - * Describes the shape of a behavior constructor that can be created by - * an {@link AttachedBehaviorHTMLDirective}. - * @public - */ -export type AttachedBehaviorType = new ( - targets: ViewBehaviorTargets, - targetId: string, - options: T -) => Behavior; - -/** - * A directive that attaches special behavior to an element via a custom attribute. - * @public - */ -export class AttachedBehaviorHTMLDirective extends HTMLDirective { - /** - * - * @param behavior - The behavior to instantiate and attach to the element. - * @param options - Options to pass to the behavior during creation. - */ - public constructor(private behavior: AttachedBehaviorType, private options: T) { - super(); - } - - /** - * Creates a placeholder string based on the directive's index within the template. - * @param index - The index of the directive within the template. - * @remarks - * Creates a custom attribute placeholder. - */ - public createPlaceholder(index: number): string { - return DOM.createCustomAttributePlaceholder(index); - } - - /** - * Creates a behavior. - * @param targets - The targets available for behaviors to be attached to. - * @remarks - * Creates an instance of the `behavior` type this directive was constructed with - * and passes the targets, targetId, and options to that `behavior`'s constructor. - */ - public createBehavior(targets: ViewBehaviorTargets): ViewBehavior { - return new this.behavior(targets, this.targetId, this.options); - } -} diff --git a/packages/web-components/fast-element/src/templating/node-observation.ts b/packages/web-components/fast-element/src/templating/node-observation.ts index f20d98c197b..05f9b87dfc4 100644 --- a/packages/web-components/fast-element/src/templating/node-observation.ts +++ b/packages/web-components/fast-element/src/templating/node-observation.ts @@ -1,6 +1,7 @@ -import type { Behavior } from "../observation/behavior.js"; -import { Accessor, Observable } from "../observation/observable.js"; -import { emptyArray } from "../platform.js"; +import { DOM } from "../dom"; +import type { ExecutionContext } from "../observation/observable"; +import { emptyArray } from "../platform"; +import { HTMLDirective, ViewBehavior, ViewBehaviorTargets } from "./html-directive"; /** * Options for configuring node observation behavior. @@ -46,80 +47,110 @@ export function elements(selector?: string): ElementsFilter { * A base class for node observation. * @internal */ -export abstract class NodeObservationBehavior - implements Behavior { - private source: any = null; - private shouldUpdate!: boolean; - +export abstract class NodeObservationDirective + extends HTMLDirective + implements ViewBehavior { /** - * Creates an instance of NodeObservationBehavior. - * @param target - The target to assign the nodes property on. + * Creates an instance of NodeObservationDirective. * @param options - The options to use in configuring node observation. */ - constructor(protected target: HTMLElement, protected options: T) {} - - /** - * Begins observation of the nodes. - */ - public abstract observe(): void; + constructor(protected options: T) { + super(); + } /** - * Disconnects observation of the nodes. + * Creates a placeholder string based on the directive's index within the template. + * @param index - The index of the directive within the template. + * @remarks + * Creates a custom attribute placeholder. */ - public abstract disconnect(): void; + createPlaceholder(index: number): string { + return DOM.createCustomAttributePlaceholder(index); + } /** - * Retrieves the nodes that should be assigned to the target. + * Creates a behavior. + * @param targets - The targets available for behaviors to be attached to. */ - protected abstract getNodes(): Node[]; + createBehavior(targets: ViewBehaviorTargets): ViewBehavior { + return this; + } /** * Bind this behavior to the source. * @param source - The source to bind to. * @param context - The execution context that the binding is operating within. + * @param targets - The targets that behaviors in a view can attach to. */ - public bind(source: any): void { - const name = this.options.property; - this.shouldUpdate = Observable.getAccessors(source).some( - (x: Accessor) => x.name === name - ); - this.source = source; - this.updateTarget(this.computeNodes()); - - if (this.shouldUpdate) { - this.observe(); - } + bind( + source: any, + context: ExecutionContext, + targets: ViewBehaviorTargets + ): void { + const target = targets[this.targetId] as any; + target.$fastSource = source; + this.updateTarget(source, this.computeNodes(target)); + this.observe(target); } /** * Unbinds this behavior from the source. * @param source - The source to unbind from. + * @param context - The execution context that the binding is operating within. + * @param targets - The targets that behaviors in a view can attach to. */ - public unbind(): void { - this.updateTarget(emptyArray); - this.source = null; - - if (this.shouldUpdate) { - this.disconnect(); - } + unbind( + source: any, + context: ExecutionContext, + targets: ViewBehaviorTargets + ): void { + const target = targets[this.targetId] as any; + this.updateTarget(source, emptyArray); + this.disconnect(target); + target.$fastSource = null; } - /** @internal */ - public handleEvent(): void { - this.updateTarget(this.computeNodes()); + /** + * Updates the source property with the computed nodes. + * @param source - The source object to assign the nodes property to. + * @param value - The nodes to assign to the source object property. + */ + protected updateTarget(source: any, value: ReadonlyArray): void { + source[this.options.property] = value; } - private computeNodes(): Node[] { - let nodes = this.getNodes(); + /** + * Computes the set of nodes that should be assigned to the source property. + * @param target - The target to compute the nodes for. + * @returns The computed nodes. + * @remarks + * Applies filters if provided. + */ + protected computeNodes(target: any): Node[] { + let nodes = this.getNodes(target); - if (this.options.filter !== void 0) { + if ("filter" in this.options) { nodes = nodes.filter(this.options.filter!); } return nodes; } - private updateTarget(value: ReadonlyArray): void { - this.source[this.options.property] = value; - } + /** + * Begins observation of the nodes. + * @param target - The target to observe. + */ + protected abstract observe(target: any): void; + + /** + * Disconnects observation of the nodes. + * @param target - The target to unobserve. + */ + protected abstract disconnect(target: any): void; + + /** + * Retrieves the raw nodes that should be assigned to the source property. + * @param target - The target to get the node to. + */ + protected abstract getNodes(target: any): Node[]; } diff --git a/packages/web-components/fast-element/src/templating/slotted.spec.ts b/packages/web-components/fast-element/src/templating/slotted.spec.ts index bfcbd4c448f..6b1969e23b0 100644 --- a/packages/web-components/fast-element/src/templating/slotted.spec.ts +++ b/packages/web-components/fast-element/src/templating/slotted.spec.ts @@ -1,28 +1,27 @@ import { expect } from "chai"; -import { slotted, SlottedBehavior } from "./slotted"; -import { AttachedBehaviorHTMLDirective } from "./html-directive"; -import { observable } from "../observation/observable"; +import { slotted, SlottedDirective } from "./slotted"; +import { defaultExecutionContext, observable } from "../observation/observable"; import { elements } from "./node-observation"; import { DOM } from "../dom"; describe("The slotted", () => { context("template function", () => { - it("returns an AttachedBehaviorDirective", () => { + it("returns an ChildrenDirective", () => { const directive = slotted("test"); - expect(directive).to.be.instanceOf(AttachedBehaviorHTMLDirective); + expect(directive).to.be.instanceOf(SlottedDirective); }); }); context("directive", () => { - it("creates a SlottedBehavior", () => { + it("creates a behavior by returning itself", () => { const targetId = 'r'; - const directive = slotted("test") as AttachedBehaviorHTMLDirective; + const directive = slotted("test") as SlottedDirective; directive.targetId = targetId; const target = document.createElement("slot"); const targets = { [targetId]: target } const behavior = directive.createBehavior(targets); - expect(behavior).to.be.instanceOf(SlottedBehavior); + expect(behavior).to.equal(directive); }); }); @@ -58,33 +57,36 @@ describe("The slotted", () => { it("gathers nodes from a slot", () => { const { children, targets, targetId } = createDOM(); - const behavior = new SlottedBehavior(targets, targetId, { property: "nodes" }); + const behavior = new SlottedDirective({ property: "nodes" }); + behavior.targetId = targetId; const model = new Model(); - behavior.bind(model); + behavior.bind(model, defaultExecutionContext, targets); expect(model.nodes).members(children); }); it("gathers nodes from a slot with a filter", () => { const { targets, targetId, children } = createDOM("foo-bar"); - const behavior = new SlottedBehavior(targets, targetId, { + const behavior = new SlottedDirective({ property: "nodes", filter: elements("foo-bar"), }); + behavior.targetId = targetId; const model = new Model(); - behavior.bind(model); + behavior.bind(model, defaultExecutionContext, targets); expect(model.nodes).members(children.filter(elements("foo-bar"))); }); it("updates when slotted nodes change", async () => { const { host, slot, children, targets, targetId } = createDOM("foo-bar"); - const behavior = new SlottedBehavior(targets, targetId, { property: "nodes" }); + const behavior = new SlottedDirective({ property: "nodes" }); + behavior.targetId = targetId; const model = new Model(); - behavior.bind(model); + behavior.bind(model, defaultExecutionContext, targets); expect(model.nodes).members(children); @@ -97,13 +99,14 @@ describe("The slotted", () => { it("updates when slotted nodes change with a filter", async () => { const { host, slot, children, targets, targetId } = createDOM("foo-bar"); - const behavior = new SlottedBehavior(targets, targetId, { + const behavior = new SlottedDirective({ property: "nodes", filter: elements("foo-bar"), }); + behavior.targetId = targetId; const model = new Model(); - behavior.bind(model); + behavior.bind(model, defaultExecutionContext, targets); expect(model.nodes).members(children); @@ -116,14 +119,15 @@ describe("The slotted", () => { it("clears and unwatches when unbound", async () => { const { host, slot, children, targets, targetId } = createDOM("foo-bar"); - const behavior = new SlottedBehavior(targets, targetId, { property: "nodes" }); + const behavior = new SlottedDirective({ property: "nodes" }); + behavior.targetId = targetId; const model = new Model(); - behavior.bind(model); + behavior.bind(model, defaultExecutionContext, targets); expect(model.nodes).members(children); - behavior.unbind(); + behavior.unbind(model, defaultExecutionContext, targets); expect(model.nodes).members([]); diff --git a/packages/web-components/fast-element/src/templating/slotted.ts b/packages/web-components/fast-element/src/templating/slotted.ts index 947857db410..a7feff6c71a 100644 --- a/packages/web-components/fast-element/src/templating/slotted.ts +++ b/packages/web-components/fast-element/src/templating/slotted.ts @@ -1,12 +1,11 @@ -import { AttachedBehaviorHTMLDirective, ViewBehaviorTargets } from "./html-directive"; -import { NodeBehaviorOptions, NodeObservationBehavior } from "./node-observation"; +import { NodeObservationDirective, NodeBehaviorOptions } from "./node-observation"; import type { CaptureType } from "./template"; /** * The options used to configure slotted node observation. * @public */ -export interface SlottedBehaviorOptions +export interface SlottedDirectiveOptions extends NodeBehaviorOptions, AssignedNodesOptions {} @@ -14,39 +13,36 @@ export interface SlottedBehaviorOptions * The runtime behavior for slotted node observation. * @public */ -export class SlottedBehavior extends NodeObservationBehavior { - /** - * Creates an instance of SlottedBehavior. - * @param target - The slot element target to observe. - * @param options - The options to use when observing the slot. - */ - public constructor( - targets: ViewBehaviorTargets, - targetId: string, - options: SlottedBehaviorOptions - ) { - super(targets[targetId] as HTMLElement, options); - } - +export class SlottedDirective extends NodeObservationDirective { /** * Begins observation of the nodes. + * @param target - The target to observe. */ - public observe(): void { - this.target.addEventListener("slotchange", this); + observe(target: EventSource) { + target.addEventListener("slotchange", this); } /** * Disconnects observation of the nodes. + * @param target - The target to unobserve. */ - public disconnect(): void { - this.target.removeEventListener("slotchange", this); + disconnect(target: EventSource) { + target.removeEventListener("slotchange", this); } /** - * Retrieves the nodes that should be assigned to the target. + * Retrieves the raw nodes that should be assigned to the source property. + * @param target - The target to get the node to. */ - protected getNodes(): Node[] { - return (this.target as HTMLSlotElement).assignedNodes(this.options); + getNodes(target: HTMLSlotElement): Node[] { + return target.assignedNodes(this.options); + } + + /** @internal */ + handleEvent(event: Event): void { + const target = event.currentTarget as any; + const source = target.$fastSource; + this.updateTarget(source, this.computeNodes(target)); } } @@ -57,11 +53,13 @@ export class SlottedBehavior extends NodeObservationBehavior( - propertyOrOptions: (keyof T & string) | SlottedBehaviorOptions + propertyOrOptions: (keyof T & string) | SlottedDirectiveOptions ): CaptureType { if (typeof propertyOrOptions === "string") { propertyOrOptions = { property: propertyOrOptions }; } - return new AttachedBehaviorHTMLDirective(SlottedBehavior, propertyOrOptions as any); + return new SlottedDirective( + propertyOrOptions as SlottedDirectiveOptions + ); } From 37e0cfd2e787106bde512dfd258e3b7583f6bf6e Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Mon, 18 Oct 2021 15:21:46 -0400 Subject: [PATCH 027/135] refactor: dry up ref and node observation directives --- .../fast-element/docs/api-report.md | 20 +++++-- .../src/templating/html-directive.ts | 52 +++++++++++++++++++ .../src/templating/node-observation.ts | 38 +++----------- .../fast-element/src/templating/ref.ts | 35 +++---------- 4 files changed, 81 insertions(+), 64 deletions(-) diff --git a/packages/web-components/fast-element/docs/api-report.md b/packages/web-components/fast-element/docs/api-report.md index 374e4fddf20..1254e449d55 100644 --- a/packages/web-components/fast-element/docs/api-report.md +++ b/packages/web-components/fast-element/docs/api-report.md @@ -420,12 +420,11 @@ export class PropertyChangeNotifier implements Notifier { // @public export function ref(propertyName: keyof T & string): CaptureType; +// Warning: (ae-incompatible-release-tags) The symbol "RefDirective" is marked as @public, but its signature references "StatelessAttachedAttributeDirective" which is marked as @internal +// // @public -export class RefDirective extends HTMLDirective implements ViewBehavior { - constructor(propertyName: string); +export class RefDirective extends StatelessAttachedAttributeDirective { bind(source: any, context: ExecutionContext, targets: ViewBehaviorTargets): void; - createBehavior(): this; - createPlaceholder(index: number): string; unbind(): void; } @@ -482,6 +481,19 @@ export interface Splice { removed: any[]; } +// Warning: (ae-internal-missing-underscore) The name "StatelessAttachedAttributeDirective" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export abstract class StatelessAttachedAttributeDirective extends HTMLDirective implements ViewBehavior { + constructor(options: T); + abstract bind(source: any, context: ExecutionContext, targets: ViewBehaviorTargets): void; + createBehavior(targets: ViewBehaviorTargets): ViewBehavior; + createPlaceholder(index: number): string; + // (undocumented) + protected options: T; + abstract unbind(source: any, context: ExecutionContext, targets: ViewBehaviorTargets): void; +} + // @public export interface StyleTarget { adoptedStyleSheets?: CSSStyleSheet[]; 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 b08affb38db..954a5094166 100644 --- a/packages/web-components/fast-element/src/templating/html-directive.ts +++ b/packages/web-components/fast-element/src/templating/html-directive.ts @@ -99,3 +99,55 @@ export abstract class TargetedHTMLDirective extends HTMLDirective { public createPlaceholder: (index: number) => string = DOM.createInterpolationPlaceholder; } + +/** @internal */ +export abstract class StatelessAttachedAttributeDirective extends HTMLDirective + implements ViewBehavior { + /** + * Creates an instance of RefDirective. + * @param options - The options to use in configuring the directive. + */ + public constructor(protected options: T) { + super(); + } + + /** + * Creates a behavior. + * @param targets - The targets available for behaviors to be attached to. + */ + createBehavior(targets: ViewBehaviorTargets): ViewBehavior { + return this; + } + + /** + * Creates a placeholder string based on the directive's index within the template. + * @param index - The index of the directive within the template. + * @remarks + * Creates a custom attribute placeholder. + */ + public createPlaceholder(index: number): string { + return DOM.createCustomAttributePlaceholder(index); + } + + /** + * Bind this behavior to the source. + * @param source - The source to bind to. + * @param context - The execution context that the binding is operating within. + * @param targets - The targets that behaviors in a view can attach to. + */ + abstract bind( + source: any, + context: ExecutionContext, + targets: ViewBehaviorTargets + ): void; + + /** + * Unbinds this behavior from the source. + * @param source - The source to unbind from. + */ + abstract unbind( + source: any, + context: ExecutionContext, + targets: ViewBehaviorTargets + ): void; +} diff --git a/packages/web-components/fast-element/src/templating/node-observation.ts b/packages/web-components/fast-element/src/templating/node-observation.ts index 05f9b87dfc4..76e222dbdeb 100644 --- a/packages/web-components/fast-element/src/templating/node-observation.ts +++ b/packages/web-components/fast-element/src/templating/node-observation.ts @@ -1,7 +1,9 @@ -import { DOM } from "../dom"; import type { ExecutionContext } from "../observation/observable"; import { emptyArray } from "../platform"; -import { HTMLDirective, ViewBehavior, ViewBehaviorTargets } from "./html-directive"; +import { + StatelessAttachedAttributeDirective, + ViewBehaviorTargets, +} from "./html-directive"; /** * Options for configuring node observation behavior. @@ -47,35 +49,9 @@ export function elements(selector?: string): ElementsFilter { * A base class for node observation. * @internal */ -export abstract class NodeObservationDirective - extends HTMLDirective - implements ViewBehavior { - /** - * Creates an instance of NodeObservationDirective. - * @param options - The options to use in configuring node observation. - */ - constructor(protected options: T) { - super(); - } - - /** - * Creates a placeholder string based on the directive's index within the template. - * @param index - The index of the directive within the template. - * @remarks - * Creates a custom attribute placeholder. - */ - createPlaceholder(index: number): string { - return DOM.createCustomAttributePlaceholder(index); - } - - /** - * Creates a behavior. - * @param targets - The targets available for behaviors to be attached to. - */ - createBehavior(targets: ViewBehaviorTargets): ViewBehavior { - return this; - } - +export abstract class NodeObservationDirective< + T extends NodeBehaviorOptions +> extends StatelessAttachedAttributeDirective { /** * Bind this behavior to the source. * @param source - The source to bind to. diff --git a/packages/web-components/fast-element/src/templating/ref.ts b/packages/web-components/fast-element/src/templating/ref.ts index 3da12b67960..e0c176b7795 100644 --- a/packages/web-components/fast-element/src/templating/ref.ts +++ b/packages/web-components/fast-element/src/templating/ref.ts @@ -1,38 +1,15 @@ import type { CaptureType } from "./template"; -import { HTMLDirective, ViewBehavior, ViewBehaviorTargets } from "./html-directive"; +import { + StatelessAttachedAttributeDirective, + ViewBehaviorTargets, +} from "./html-directive"; import type { ExecutionContext } from "../observation/observable"; -import { DOM } from "../dom"; /** * The runtime behavior for template references. * @public */ -export class RefDirective extends HTMLDirective implements ViewBehavior { - /** - * Creates an instance of RefDirective. - * @param propertyName - The name of the property to assign the reference to. - */ - public constructor(private propertyName: string) { - super(); - } - - /** - * Creates a behavior. - */ - createBehavior() { - return this; - } - - /** - * Creates a placeholder string based on the directive's index within the template. - * @param index - The index of the directive within the template. - * @remarks - * Creates a custom attribute placeholder. - */ - public createPlaceholder(index: number): string { - return DOM.createCustomAttributePlaceholder(index); - } - +export class RefDirective extends StatelessAttachedAttributeDirective { /** * Bind this behavior to the source. * @param source - The source to bind to. @@ -44,7 +21,7 @@ export class RefDirective extends HTMLDirective implements ViewBehavior { context: ExecutionContext, targets: ViewBehaviorTargets ): void { - source[this.propertyName] = targets[this.targetId]; + source[this.options] = targets[this.targetId]; } /** From e6d71e9a5d84eb59e47bb60c6735f12d6c5f9dae Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Tue, 19 Oct 2021 09:53:35 -0400 Subject: [PATCH 028/135] refactor: improve compiler internals by combining context and result --- .../fast-element/docs/api-report.md | 11 +- .../fast-element/src/templating/compiler.ts | 200 ++++++++---------- .../fast-element/src/templating/view.ts | 2 +- 3 files changed, 89 insertions(+), 124 deletions(-) diff --git a/packages/web-components/fast-element/docs/api-report.md b/packages/web-components/fast-element/docs/api-report.md index 1254e449d55..ca19c6f02c6 100644 --- a/packages/web-components/fast-element/docs/api-report.md +++ b/packages/web-components/fast-element/docs/api-report.md @@ -327,18 +327,15 @@ export abstract class HTMLDirective implements ViewBehaviorFactory { } // @public -export class HTMLTemplateCompilationResult { - constructor(fragment: DocumentFragment, factories: ViewBehaviorFactory[], targetIds: Set, descriptors: PropertyDescriptorMap); +export interface HTMLTemplateCompilationResult { createTargets(root: Node, host?: Node): ViewBehaviorTargets; - // (undocumented) - readonly factories: ViewBehaviorFactory[]; - // (undocumented) + readonly factories: ReadonlyArray; readonly fragment: DocumentFragment; - } +} // @public export class HTMLView implements ElementView, SyntheticView { - constructor(fragment: DocumentFragment, factories: ViewBehaviorFactory[], targets: ViewBehaviorTargets); + constructor(fragment: DocumentFragment, factories: ReadonlyArray, targets: ViewBehaviorTargets); appendTo(node: Node): void; bind(source: TSource, context: ExecutionContext): void; context: ExecutionContext | null; diff --git a/packages/web-components/fast-element/src/templating/compiler.ts b/packages/web-components/fast-element/src/templating/compiler.ts index e8a67b203ff..31dc22cc3a1 100644 --- a/packages/web-components/fast-element/src/templating/compiler.ts +++ b/packages/web-components/fast-element/src/templating/compiler.ts @@ -13,65 +13,46 @@ type InlineDirective = HTMLDirective & { const targetIdFrom = (parentId: string, nodeIndex: number) => `${parentId}.${nodeIndex}`; const descriptorCache: PropertyDescriptorMap = {}; -function addTargetDescriptor( - descriptors: PropertyDescriptorMap, - parentId: string, - targetId: string, - targetIndex: number -) { - if ( - targetId === "r" || // root - targetId === "h" || // host - descriptors[targetId] - ) { - return; - } - - if (!descriptors[parentId]) { - const index = parentId.lastIndexOf("."); - const grandparentId = parentId.substr(0, index); - const childIndex = parseInt(parentId.substr(index + 1)); - addTargetDescriptor(descriptors, grandparentId, parentId, childIndex); - } - - descriptors[targetId] = createTargetDescriptor(parentId, targetId, targetIndex); -} - -function createTargetDescriptor( - parentId: string, - targetId: string, - targetIndex: number -): PropertyDescriptor { - let descriptor = descriptorCache[targetId]; - - if (!descriptor) { - const field = `_${targetId}`; - - descriptorCache[targetId] = descriptor = { - get() { - return ( - this[field] ?? (this[field] = this[parentId].childNodes[targetIndex]) - ); - }, - }; - } - - return descriptor; -} - -let sharedContext: CompilationContext | null = null; - // used to prevent creating lots of objects just to track node and index while compiling const next = { index: 0, node: null as ChildNode | null, }; -class CompilationContext { - public factories: ViewBehaviorFactory[] = []; - public targetIds = new Set(); - public descriptors: PropertyDescriptorMap = {}; - public directives: ReadonlyArray; +/** + * The result of compiling a template and its directives. + * @public + */ +export interface HTMLTemplateCompilationResult { + /** + * A cloneable DocumentFragment representing the compiled HTML. + */ + readonly fragment: DocumentFragment; + + /** + * The behaviors that should be applied to the template's HTML. + */ + readonly factories: ReadonlyArray; + + /** + * Creates a behavior target lookup object. + * @param host - The host element. + * @param root - The root element. + * @returns A lookup object for behavior targets. + */ + createTargets(root: Node, host?: Node): ViewBehaviorTargets; +} + +class CompilationContext implements HTMLTemplateCompilationResult { + private proto: any = null; + private targetIds = new Set(); + private descriptors: PropertyDescriptorMap = {}; + public readonly factories: ViewBehaviorFactory[] = []; + + constructor( + public readonly fragment: DocumentFragment, + public readonly directives: ReadonlyArray + ) {} public addFactory( factory: ViewBehaviorFactory, @@ -81,7 +62,7 @@ class CompilationContext { ): void { if (!this.targetIds.has(targetId)) { this.targetIds.add(targetId); - addTargetDescriptor(this.descriptors, parentId, targetId, targetIndex); + this.addTargetDescriptor(parentId, targetId, targetIndex); } factory.targetId = targetId; @@ -98,27 +79,57 @@ class CompilationContext { this.addFactory(directive, parentId, targetId, targetIndex); } - public close(fragment: DocumentFragment): HTMLTemplateCompilationResult { - const result = new HTMLTemplateCompilationResult( - fragment, - this.factories, - this.targetIds, - this.descriptors - ); + public freeze(): HTMLTemplateCompilationResult { + this.proto = Object.create(null, this.descriptors); + return this; + } + + public createTargets(root: Node, host?: Node): ViewBehaviorTargets { + const targets = Object.create(this.proto); + targets.r = root; + targets.h = host ?? root; - this.factories = []; - this.targetIds = new Set(); - this.descriptors = {}; - sharedContext = this; + for (const id of this.targetIds) { + targets[id]; // trigger locator + } - return result; + return targets; } - public static open(directives: ReadonlyArray): CompilationContext { - const context = sharedContext ?? new CompilationContext(); - context.directives = directives; - sharedContext = null; - return context; + private addTargetDescriptor(parentId: string, targetId: string, targetIndex: number) { + const descriptors = this.descriptors; + + if ( + targetId === "r" || // root + targetId === "h" || // host + descriptors[targetId] + ) { + return; + } + + if (!descriptors[parentId]) { + const index = parentId.lastIndexOf("."); + const grandparentId = parentId.substr(0, index); + const childIndex = parseInt(parentId.substr(index + 1)); + this.addTargetDescriptor(grandparentId, parentId, childIndex); + } + + let descriptor = descriptorCache[targetId]; + + if (!descriptor) { + const field = `_${targetId}`; + + descriptorCache[targetId] = descriptor = { + get() { + return ( + this[field] ?? + (this[field] = this[parentId].childNodes[targetIndex]) + ); + }, + }; + } + + descriptors[targetId] = descriptor; } } @@ -316,49 +327,6 @@ function compileNode( return next; } -/** - * The result of compiling a template and its directives. - * @public - */ -export class HTMLTemplateCompilationResult { - private proto: any; - - /** - * - * @param fragment - A cloneable DocumentFragment representing the compiled HTML. - * @param viewBehaviorFactories - The behaviors that should be applied to the template's HTML. - * @param hostBehaviorFactories - The behaviors that should be applied to the host element that - * the template is rendered into. - * @param targetIds - The structural ids used by the behavior factories. - */ - public constructor( - public readonly fragment: DocumentFragment, - public readonly factories: ViewBehaviorFactory[], - private targetIds: Set, - descriptors: PropertyDescriptorMap - ) { - this.proto = Object.create(null, descriptors); - } - - /** - * Creates a behavior target lookup object. - * @param host - The host element. - * @param root - The root element. - * @returns A lookup object for behavior targets. - */ - public createTargets(root: Node, host?: Node): ViewBehaviorTargets { - const targets = Object.create(this.proto); - targets.r = root; - targets.h = host ?? root; - - for (const id of this.targetIds) { - targets[id]; // trigger locator - } - - return targets; - } -} - /** * Compiles a template and associated directives into a raw compilation * result which include a cloneable DocumentFragment and factories capable @@ -377,7 +345,7 @@ export function compileTemplate( ): HTMLTemplateCompilationResult { // https://bugs.chromium.org/p/chromium/issues/detail?id=1111864 const fragment = document.adoptNode(template.content); - const context = CompilationContext.open(directives); + const context = new CompilationContext(fragment, directives); compileAttributes(context, "", template, /* host */ "h", 0, true); if ( @@ -396,5 +364,5 @@ export function compileTemplate( compileChildren(context, fragment, /* root */ "r"); next.node = null; // prevent leaks - return context.close(fragment); + return context.freeze(); } diff --git a/packages/web-components/fast-element/src/templating/view.ts b/packages/web-components/fast-element/src/templating/view.ts index a4c85dec7ae..3f255367e51 100644 --- a/packages/web-components/fast-element/src/templating/view.ts +++ b/packages/web-components/fast-element/src/templating/view.ts @@ -136,7 +136,7 @@ export class HTMLView */ public constructor( private fragment: DocumentFragment, - private factories: ViewBehaviorFactory[], + private factories: ReadonlyArray, private targets: ViewBehaviorTargets ) { this.firstChild = fragment.firstChild!; From b7b17f3f4f2c4c36dad87c234208a361db46589e Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Tue, 19 Oct 2021 17:45:22 -0400 Subject: [PATCH 029/135] feat: wip updating binding system to support modes and configs --- .../fast-element/docs/api-report.md | 96 +-- .../src/templating/binding.spec.ts | 56 +- .../fast-element/src/templating/binding.ts | 655 +++++++++++------- .../src/templating/compiler.spec.ts | 4 +- .../fast-element/src/templating/compiler.ts | 7 +- .../src/templating/template.spec.ts | 4 +- .../fast-element/src/templating/template.ts | 4 +- 7 files changed, 482 insertions(+), 344 deletions(-) diff --git a/packages/web-components/fast-element/docs/api-report.md b/packages/web-components/fast-element/docs/api-report.md index ca19c6f02c6..8ed280b7e09 100644 --- a/packages/web-components/fast-element/docs/api-report.md +++ b/packages/web-components/fast-element/docs/api-report.md @@ -53,44 +53,43 @@ export interface Behavior { unbind(source: TSource, context: ExecutionContext): void; } +// @public (undocumented) +export function bind(binding: Binding, config?: BindingConfig | DefaultBindingOptions): CaptureType; + // @public export type Binding = (source: TSource, context: ExecutionContext) => TReturn; -// @public -export class BindingBehavior implements Behavior { - constructor(target: any, binding: Binding, isBindingVolatile: boolean, bind: typeof normalBind, unbind: typeof normalUnbind, updateTarget: typeof updatePropertyTarget, targetName?: string); - // Warning: (ae-forgotten-export) The symbol "normalBind" needs to be exported by the entry point index.d.ts - bind: typeof normalBind; - // @internal (undocumented) - binding: Binding; - // @internal (undocumented) - bindingObserver: BindingObserver | null; - // @internal (undocumented) - classVersions: Record; - // @internal (undocumented) - context: ExecutionContext | null; - // @internal (undocumented) - handleChange(): void; - // @internal (undocumented) - handleEvent(event: Event): void; - // @internal (undocumented) - isBindingVolatile: boolean; - // @internal (undocumented) - source: unknown; - // @internal (undocumented) - target: any; - // @internal (undocumented) - targetName?: string; - // Warning: (ae-forgotten-export) The symbol "normalUnbind" needs to be exported by the entry point index.d.ts - unbind: typeof normalUnbind; - // Warning: (ae-forgotten-export) The symbol "updatePropertyTarget" needs to be exported by the entry point index.d.ts - // - // @internal (undocumented) - updateTarget: typeof updatePropertyTarget; - // @internal (undocumented) - value: any; - // @internal (undocumented) - version: number; +// @public (undocumented) +export type BindingBehaviorFactory = { + readonly directive: HTMLBindingDirective; + createBehavior(targets: ViewBehaviorTargets): ViewBehavior; +}; + +// @public (undocumented) +export interface BindingConfig { + // (undocumented) + mode: BindingMode; + // (undocumented) + options: any; +} + +// @public (undocumented) +export type BindingFactory = new (directive: HTMLBindingDirective) => BindingBehaviorFactory; + +// @public (undocumented) +export interface BindingMode { + // (undocumented) + attribute?: BindingFactory; + // (undocumented) + booleanAttribute?: BindingFactory; + // (undocumented) + content?: BindingFactory; + // (undocumented) + event?: BindingFactory; + // (undocumented) + property?: BindingFactory; + // (undocumented) + tokenList?: BindingFactory; } // @public @@ -184,6 +183,11 @@ export function customElement(nameOrDef: string | PartialFASTElementDefinition): // @public export type DecoratorAttributeConfiguration = Omit; +// @public (undocumented) +export type DefaultBindingOptions = { + capture?: boolean; +}; + // @public export const defaultExecutionContext: ExecutionContext; @@ -308,16 +312,24 @@ export type Global = typeof globalThis & { // @public export function html(strings: TemplateStringsArray, ...values: TemplateValue[]): ViewTemplate; -// @public +// @public (undocumented) export class HTMLBindingDirective extends TargetedHTMLDirective { - constructor(binding: Binding); + constructor(binding: Binding, mode: BindingMode, options: any); // (undocumented) binding: Binding; - createBehavior(targets: ViewBehaviorTargets): BindingBehavior; + // (undocumented) + cleanedTargetName?: string; + // (undocumented) + createBehavior(targets: ViewBehaviorTargets): ViewBehavior; + // (undocumented) + mode: BindingMode; + // (undocumented) + options: any; + // (undocumented) targetAtContent(): void; get targetName(): string | undefined; set targetName(value: string | undefined); - } +} // @public export abstract class HTMLDirective implements ViewBehaviorFactory { @@ -395,6 +407,9 @@ export interface ObservationRecord { propertySource: any; } +// @public (undocumented) +export const oneTime: BindingConfig & ((options?: DefaultBindingOptions) => BindingConfig); + // @public export interface PartialFASTElementDefinition { readonly attributes?: (AttributeConfiguration | string)[]; @@ -555,6 +570,9 @@ export type TrustedTypesPolicy = { createHTML(html: string): string; }; +// @public (undocumented) +export const updateView: BindingConfig & ((options?: DefaultBindingOptions) => BindingConfig); + // @public export interface ValueConverter { fromView(value: any): any; diff --git a/packages/web-components/fast-element/src/templating/binding.spec.ts b/packages/web-components/fast-element/src/templating/binding.spec.ts index 5d381d67777..f6e0c1bf8c0 100644 --- a/packages/web-components/fast-element/src/templating/binding.spec.ts +++ b/packages/web-components/fast-element/src/templating/binding.spec.ts @@ -1,5 +1,5 @@ import { expect } from "chai"; -import { HTMLBindingDirective } from "./binding"; +import { bind, HTMLBindingDirective } from "./binding"; import { observable, defaultExecutionContext } from "../observation/observable"; import { DOM } from "../dom"; import { html, ViewTemplate } from "./template"; @@ -27,7 +27,7 @@ describe("The HTML binding directive", () => { } function contentBinding(propertyName: keyof Model = "value") { - const directive = new HTMLBindingDirective(x => x[propertyName]); + const directive = bind(x => x[propertyName]) as HTMLBindingDirective; directive.targetAtContent(); directive.targetId = 'r'; @@ -39,24 +39,24 @@ describe("The HTML binding directive", () => { parentNode.appendChild(node); - return { directive, behavior, node, parentNode }; + return { directive, behavior, node, parentNode, targets }; } context("when binding text content", () => { it("initially sets the text of a node", () => { - const { behavior, node } = contentBinding(); + const { behavior, node, targets } = contentBinding(); const model = new Model("This is a test"); - behavior.bind(model, defaultExecutionContext); + behavior.bind(model, defaultExecutionContext, targets); expect(node.textContent).to.equal(model.value); }); it("updates the text of a node when the expression changes", async () => { - const { behavior, node } = contentBinding(); + const { behavior, node, targets } = contentBinding(); const model = new Model("This is a test"); - behavior.bind(model, defaultExecutionContext); + behavior.bind(model, defaultExecutionContext, targets); expect(node.textContent).to.equal(model.value); @@ -70,21 +70,21 @@ describe("The HTML binding directive", () => { context("when binding template content", () => { it("initially inserts a view based on the template", () => { - const { behavior, parentNode } = contentBinding(); + const { behavior, parentNode, targets } = contentBinding(); const template = html`This is a template. ${x => x.knownValue}`; const model = new Model(template); - behavior.bind(model, defaultExecutionContext); + behavior.bind(model, defaultExecutionContext, targets); expect(toHTML(parentNode)).to.equal(`This is a template. value`); }); it("removes an inserted view when the value changes to plain text", async () => { - const { behavior, parentNode } = contentBinding(); + const { behavior, parentNode, targets } = contentBinding(); const template = html`This is a template. ${x => x.knownValue}`; const model = new Model(template); - behavior.bind(model, defaultExecutionContext); + behavior.bind(model, defaultExecutionContext, targets); expect(toHTML(parentNode)).to.equal(`This is a template. value`); @@ -96,11 +96,11 @@ describe("The HTML binding directive", () => { }); it("removes an inserted view when the value changes to null", async () => { - const { behavior, parentNode } = contentBinding(); + const { behavior, parentNode, targets } = contentBinding(); const template = html`This is a template. ${x => x.knownValue}`; const model = new Model(template); - behavior.bind(model, defaultExecutionContext); + behavior.bind(model, defaultExecutionContext, targets); expect(toHTML(parentNode)).to.equal(`This is a template. value`); @@ -112,11 +112,11 @@ describe("The HTML binding directive", () => { }); it("removes an inserted view when the value changes to undefined", async () => { - const { behavior, parentNode } = contentBinding(); + const { behavior, parentNode, targets } = contentBinding(); const template = html`This is a template. ${x => x.knownValue}`; const model = new Model(template); - behavior.bind(model, defaultExecutionContext); + behavior.bind(model, defaultExecutionContext, targets); expect(toHTML(parentNode)).to.equal(`This is a template. value`); @@ -128,11 +128,11 @@ describe("The HTML binding directive", () => { }); it("updates an inserted view when the value changes to a new template", async () => { - const { behavior, parentNode } = contentBinding(); + const { behavior, parentNode, targets } = contentBinding(); const template = html`This is a template. ${x => x.knownValue}`; const model = new Model(template); - behavior.bind(model, defaultExecutionContext); + behavior.bind(model, defaultExecutionContext, targets); expect(toHTML(parentNode)).to.equal(`This is a template. value`); @@ -145,11 +145,11 @@ describe("The HTML binding directive", () => { }); it("reuses a previous view when the value changes back from a string", async () => { - const { behavior, parentNode, node } = contentBinding(); + const { behavior, parentNode, node, targets } = contentBinding(); const template = html`This is a template. ${x => x.knownValue}`; const model = new Model(template); - behavior.bind(model, defaultExecutionContext); + behavior.bind(model, defaultExecutionContext, targets); const view = (node as any).$fastView as SyntheticView; const capturedTemplate = (node as any).$fastTemplate as ViewTemplate; @@ -177,11 +177,11 @@ describe("The HTML binding directive", () => { }); it("doesn't compose an already composed view", async () => { - const { behavior, parentNode } = contentBinding("computedValue"); + const { behavior, parentNode, targets } = contentBinding("computedValue"); const template = html`This is a template. ${x => x.knownValue}`; const model = new Model(template); - behavior.bind(model, defaultExecutionContext); + behavior.bind(model, defaultExecutionContext, targets); expect(toHTML(parentNode)).to.equal(`This is a template. value`); @@ -213,37 +213,37 @@ describe("The HTML binding directive", () => { context("when unbinding template content", () => { it("unbinds a composed view", () => { - const { behavior, node, parentNode } = contentBinding(); + const { behavior, node, parentNode, targets } = contentBinding(); const template = html`This is a template. ${x => x.knownValue}`; const model = new Model(template); - behavior.bind(model, defaultExecutionContext); + behavior.bind(model, defaultExecutionContext, targets); const newView = (node as any).$fastView as SyntheticView; expect(newView.source).to.equal(model); expect(toHTML(parentNode)).to.equal(`This is a template. value`); - behavior.unbind(); + behavior.unbind(model, defaultExecutionContext, targets); expect(newView.source).to.equal(null); }); it("rebinds a previously unbound composed view", () => { - const { behavior, node, parentNode } = contentBinding(); + const { behavior, node, parentNode, targets } = contentBinding(); const template = html`This is a template. ${x => x.knownValue}`; const model = new Model(template); - behavior.bind(model, defaultExecutionContext); + behavior.bind(model, defaultExecutionContext, targets); const view = (node as any).$fastView as SyntheticView; expect(view.source).to.equal(model); expect(toHTML(parentNode)).to.equal(`This is a template. value`); - behavior.unbind(); + behavior.unbind(model, defaultExecutionContext, targets); expect(view.source).to.equal(null); - behavior.bind(model, defaultExecutionContext); + behavior.bind(model, defaultExecutionContext, targets); const newView = (node as any).$fastView as SyntheticView; expect(newView.source).to.equal(model); diff --git a/packages/web-components/fast-element/src/templating/binding.ts b/packages/web-components/fast-element/src/templating/binding.ts index fa619a40105..ef18d8a6044 100644 --- a/packages/web-components/fast-element/src/templating/binding.ts +++ b/packages/web-components/fast-element/src/templating/binding.ts @@ -1,5 +1,5 @@ import { DOM } from "../dom"; -import type { Behavior } from "../observation/behavior"; +import type { Constructable } from "../interfaces"; import { Binding, BindingObserver, @@ -7,200 +7,428 @@ import { Observable, setCurrentEvent, } from "../observation/observable"; -import { ViewBehaviorTargets, TargetedHTMLDirective } from "./html-directive"; +import { + TargetedHTMLDirective, + ViewBehavior, + ViewBehaviorTargets, +} from "./html-directive"; +import type { CaptureType } from "./template"; import type { SyntheticView } from "./view"; -function normalBind( - this: BindingBehavior, - source: unknown, - context: ExecutionContext -): void { - this.source = source; - this.context = context; - - if (this.bindingObserver === null) { - this.bindingObserver = Observable.binding( - this.binding, - this, - this.isBindingVolatile - ); - } +export type BindingBehaviorFactory = { + readonly directive: HTMLBindingDirective; + createBehavior(targets: ViewBehaviorTargets): ViewBehavior; +}; - this.handleChange(); +export type BindingFactory = new ( + directive: HTMLBindingDirective +) => BindingBehaviorFactory; + +export interface BindingMode { + attribute?: BindingFactory; + booleanAttribute?: BindingFactory; + property?: BindingFactory; + content?: BindingFactory; + tokenList?: BindingFactory; + event?: BindingFactory; } -function triggerBind( - this: BindingBehavior, - source: unknown, - context: ExecutionContext -): void { - this.source = source; - this.context = context; - this.target.addEventListener(this.targetName!, this); +export interface BindingConfig { + mode: BindingMode; + options: any; } -function normalUnbind(this: BindingBehavior): void { - this.bindingObserver!.disconnect(); - this.source = null; - this.context = null; - this.value = null; +interface ViewBinding extends BindingBehaviorFactory, ViewBehavior { + updateTarget(target: Node, value: any, source: any, context: ExecutionContext): void; } -type ComposableView = SyntheticView & { - isComposed?: boolean; - needsBindOnly?: boolean; -}; +abstract class ViewSetBinding implements ViewBinding { + constructor(public readonly directive: HTMLBindingDirective) {} -function contentUnbind(this: BindingBehavior): void { - normalUnbind.call(this); + bind(source: any, context: ExecutionContext, targets: ViewBehaviorTargets): void { + const target = targets[this.directive.targetId]; + this.updateTarget( + target, + this.directive.binding(source, context), + source, + context + ); + } - const view = this.target.$fastView as ComposableView; + unbind(): void {} - if (view !== void 0 && view.isComposed) { - view.unbind(); - view.needsBindOnly = true; + createBehavior(): ViewBehavior { + return this; } + + abstract updateTarget( + target: Node, + value: any, + source: any, + context: ExecutionContext + ): void; } -function triggerUnbind(this: BindingBehavior): void { - this.target.removeEventListener(this.targetName!, this); - this.source = null; - this.context = null; +abstract class ViewUpdateBinding implements ViewBinding { + private isBindingVolatile: boolean; + + constructor(public readonly directive: HTMLBindingDirective) { + this.isBindingVolatile = Observable.isVolatileBinding(directive.binding); + } + + bind(source: any, context: ExecutionContext, targets: ViewBehaviorTargets): void { + const target = targets[this.directive.targetId]; + const observer: BindingObserver = + target[this.directive.targetId] ?? + (target[this.directive.targetId] = Observable.binding( + this.directive.binding, + this, + this.isBindingVolatile + )); + + (observer as any).target = target; + (observer as any).source = source; + (observer as any).context = context; + + this.updateTarget(target, observer.observe(source, context), source, context); + } + + unbind(source: any, context: ExecutionContext, targets: ViewBehaviorTargets): void { + const target = targets[this.directive.targetId]; + const observer = target[this.directive.targetId]; + observer.disconnect(); + observer.target = null; + observer.source = null; + observer.context = null; + } + + /** @internal */ + public handleChange(observer: BindingObserver): void { + const target = (observer as any).target; + const source = (observer as any).source; + const context = (observer as any).context; + this.updateTarget(target, observer.observe(source, context!), source, context); + } + + abstract updateTarget( + target: Node, + value: any, + source: any, + context: ExecutionContext + ): void; + + createBehavior(): ViewBehavior { + return this; + } } -function updateAttributeTarget(this: BindingBehavior, value: unknown): void { - DOM.setAttribute(this.target, this.targetName!, value); +function createPropertyBindingFactory< + T extends Constructable +>(BindingBase: T): BindingFactory { + return class extends BindingBase { + updateTarget(target: Node, value: any): void { + target[this.directive.cleanedTargetName!] = value; + } + }; } -function updateBooleanAttributeTarget(this: BindingBehavior, value: unknown): void { - DOM.setBooleanAttribute(this.target, this.targetName!, value as boolean); +function createAttributeBindingFactory< + T extends Constructable +>(BindingBase: T): BindingFactory { + return class extends BindingBase { + updateTarget(target: Node, value: any): void { + DOM.setAttribute( + target as HTMLElement, + this.directive.cleanedTargetName!, + value + ); + } + }; } -function updateContentTarget(this: BindingBehavior, value: any): void { - // If there's no actual value, then this equates to the - // empty string for the purposes of content bindings. - if (value === null || value === undefined) { - value = ""; - } +function createBooleanAttributeBindingFactory< + T extends Constructable +>(BindingBase: T): BindingFactory { + return class extends BindingBase { + updateTarget(target: Node, value: any): void { + DOM.setBooleanAttribute( + target as HTMLElement, + this.directive.cleanedTargetName!, + value as boolean + ); + } + }; +} - // If the value has a "create" method, then it's a template-like. - if (value.create) { - this.target.textContent = ""; - let view = this.target.$fastView as ComposableView; - - // If there's no previous view that we might be able to - // reuse then create a new view from the template. - if (view === void 0) { - view = value.create() as SyntheticView; - } else { - // If there is a previous view, but it wasn't created - // from the same template as the new value, then we - // need to remove the old view if it's still in the DOM - // and create a new view from the template. - if (this.target.$fastTemplate !== value) { - if (view.isComposed) { - view.remove(); - view.unbind(); +function createTokenListBindingFactory>( + BindingBase: T +): BindingFactory { + class TokenListBinding extends BindingBase { + classVersions = Object.create(null); + version = 0; + + updateTarget(target: Element, value: any): void { + const classVersions = this.classVersions; + const tokenList = target[this.directive.cleanedTargetName!] as DOMTokenList; + let version = this.version; + + // Add the classes, tracking the version at which they were added. + if (value !== null && value !== undefined && value.length) { + const names = value.split(/\s+/); + + for (let i = 0, ii = names.length; i < ii; ++i) { + const currentName = names[i]; + + if (currentName === "") { + continue; + } + + classVersions[currentName] = version; + tokenList.add(currentName); } + } - view = value.create() as SyntheticView; + this.classVersions = classVersions; + this.version = version + 1; + + // If this is the first call to add classes, there's no need to remove old ones. + if (version === 0) { + return; + } + + // Remove classes from the previous version. + version -= 1; + + for (const name in classVersions) { + if (classVersions[name] === version) { + tokenList.remove(name); + } } } + } - // It's possible that the value is the same as the previous template - // and that there's actually no need to compose it. - if (!view.isComposed) { - view.isComposed = true; - view.bind(this.source, this.context!); - view.insertBefore(this.target); - this.target.$fastView = view; - this.target.$fastTemplate = value; - } else if (view.needsBindOnly) { - view.needsBindOnly = false; - view.bind(this.source, this.context!); + return class implements BindingBehaviorFactory { + constructor(public directive: HTMLBindingDirective) {} + createBehavior() { + return new TokenListBinding(this.directive); } - } else { - const view = this.target.$fastView as ComposableView; + }; +} - // If there is a view and it's currently composed into - // the DOM, then we need to remove it. - if (view !== void 0 && view.isComposed) { - view.isComposed = false; - view.remove(); +type ComposableView = SyntheticView & { + isComposed?: boolean; + needsBindOnly?: boolean; +}; - if (view.needsBindOnly) { - view.needsBindOnly = false; - } else { +type ContentTarget = Node & { + $fastView?: ComposableView; + $fastTemplate?: { create(): SyntheticView }; +}; + +function createContentBindingFactory< + T extends Constructable +>(BindingBase: T): BindingFactory { + return class extends BindingBase { + unbind(source: any, context: ExecutionContext, targets: ViewBehaviorTargets) { + super.unbind(source, context, targets); + + const target = targets[this.directive.targetId] as ContentTarget; + const view = target.$fastView as ComposableView; + + if (view !== void 0 && view.isComposed) { view.unbind(); + view.needsBindOnly = true; } } - this.target.textContent = value; - } -} + updateTarget( + target: ContentTarget, + value: any, + source: any, + context: ExecutionContext + ): void { + // If there's no actual value, then this equates to the + // empty string for the purposes of content bindings. + if (value === null || value === undefined) { + value = ""; + } -function updatePropertyTarget(this: BindingBehavior, value: unknown): void { - this.target[this.targetName!] = value; -} + // If the value has a "create" method, then it's a template-like. + if (value.create) { + target.textContent = ""; + let view = target.$fastView as ComposableView; + + // If there's no previous view that we might be able to + // reuse then create a new view from the template. + if (view === void 0) { + view = value.create() as SyntheticView; + } else { + // If there is a previous view, but it wasn't created + // from the same template as the new value, then we + // need to remove the old view if it's still in the DOM + // and create a new view from the template. + if (target.$fastTemplate !== value) { + if (view.isComposed) { + view.remove(); + view.unbind(); + } + + view = value.create() as SyntheticView; + } + } -function updateClassListTarget(this: BindingBehavior, value: string): void { - const classVersions = this.classVersions || Object.create(null); - const target = this.target; - let version = this.version || 0; + // It's possible that the value is the same as the previous template + // and that there's actually no need to compose it. + if (!view.isComposed) { + view.isComposed = true; + view.bind(source, context!); + view.insertBefore(target); + target.$fastView = view; + target.$fastTemplate = value; + } else if (view.needsBindOnly) { + view.needsBindOnly = false; + view.bind(source, context!); + } + } else { + const view = target.$fastView as ComposableView; - // Add the classes, tracking the version at which they were added. - if (value !== null && value !== undefined && value.length) { - const names = value.split(/\s+/); + // If there is a view and it's currently composed into + // the DOM, then we need to remove it. + if (view !== void 0 && view.isComposed) { + view.isComposed = false; + view.remove(); - for (let i = 0, ii = names.length; i < ii; ++i) { - const currentName = names[i]; + if (view.needsBindOnly) { + view.needsBindOnly = false; + } else { + view.unbind(); + } + } - if (currentName === "") { - continue; + target.textContent = value; } - - classVersions[currentName] = version; - target.classList.add(currentName); } - } + }; +} - this.classVersions = classVersions; - this.version = version + 1; +class EventListener implements BindingBehaviorFactory { + constructor(public directive: HTMLBindingDirective) {} - // If this is the first call to add classes, there's no need to remove old ones. - if (version === 0) { - return; + bind(source: any, context: ExecutionContext, targets: ViewBehaviorTargets): void { + const target = targets[this.directive.targetId] as any; + target.$fastSource = source; + target.$fastContext = context; + target.addEventListener( + this.directive.cleanedTargetName!, + this, + this.directive.options + ); } - // Remove classes from the previous version. - version -= 1; + unbind(source: any, context: ExecutionContext, targets: ViewBehaviorTargets): void { + const target = targets[this.directive.targetId] as any; + target.$fastSource = null; + target.$fastContext = null; + target.removeEventListener( + this.directive.cleanedTargetName!, + this, + this.directive.options + ); + } + + handleEvent(event: Event): void { + const target = event.currentTarget as any; + const source = target.$fastSource; + const context = target.$fastContext; + + setCurrentEvent(event); + const result = this.directive.binding(source, context); + setCurrentEvent(null); - for (const name in classVersions) { - if (classVersions[name] === version) { - target.classList.remove(name); + if (result !== true) { + event.preventDefault(); } } + + createBehavior(targets: ViewBehaviorTargets): ViewBehavior { + return this; + } +} + +class OneTimeEventListener extends EventListener { + handleEvent(event: Event) { + super.handleEvent(event); + const target = event.currentTarget as any; + target.$fastSource = null; + target.$fastContext = null; + target.removeEventListener( + this.directive.cleanedTargetName!, + this, + this.directive.options + ); + } } -/** - * A directive that configures data binding to element content and attributes. - * @public - */ +export type DefaultBindingOptions = { + capture?: boolean; +}; + +const defaultBindingOptions: DefaultBindingOptions = { + capture: false, +}; + +export const updateView: BindingConfig & + ((options?: DefaultBindingOptions) => BindingConfig) = ( + options: DefaultBindingOptions +): BindingConfig => { + return { + mode: updateView.mode, + options: Object.assign({}, defaultBindingOptions, options), + }; +}; + +updateView.options = defaultBindingOptions; +updateView.mode = Object.freeze({ + attribute: createAttributeBindingFactory(ViewUpdateBinding as any), + booleanAttribute: createBooleanAttributeBindingFactory(ViewUpdateBinding as any), + property: createPropertyBindingFactory(ViewUpdateBinding as any), + content: createContentBindingFactory(ViewUpdateBinding as any), + tokenList: createTokenListBindingFactory(ViewUpdateBinding as any), + event: EventListener, +}); + +export const oneTime: BindingConfig & + ((options?: DefaultBindingOptions) => BindingConfig) = ( + options: DefaultBindingOptions +): BindingConfig => { + return { + mode: oneTime.mode, + options: Object.assign({}, defaultBindingOptions, options), + }; +}; + +oneTime.options = defaultBindingOptions; +oneTime.mode = Object.freeze({ + attribute: createAttributeBindingFactory(ViewSetBinding as any), + booleanAttribute: createBooleanAttributeBindingFactory(ViewSetBinding as any), + property: createPropertyBindingFactory(ViewSetBinding as any), + content: createContentBindingFactory(ViewSetBinding as any), + tokenList: createTokenListBindingFactory(ViewSetBinding as any), + event: OneTimeEventListener, +}); + export class HTMLBindingDirective extends TargetedHTMLDirective { - private cleanedTargetName?: string; private originalTargetName?: string; - private bind: typeof normalBind = normalBind; - private unbind: typeof normalUnbind = normalUnbind; - private updateTarget: typeof updateAttributeTarget = updateAttributeTarget; - private isBindingVolatile: boolean; + private factory!: BindingBehaviorFactory; - /** - * Creates an instance of BindingDirective. - * @param binding - A binding that returns the data used to update the DOM. - */ - public constructor(public binding: Binding) { + public cleanedTargetName?: string; + + public constructor( + public binding: Binding, + public mode: BindingMode, + public options: any + ) { super(); - this.isBindingVolatile = Observable.isVolatileBinding(this.binding); } /** @@ -226,163 +454,54 @@ export class HTMLBindingDirective extends TargetedHTMLDirective { const binding = this.binding; /* eslint-disable-next-line */ this.binding = (s, c) => DOM.createHTML(binding(s, c)); - this.updateTarget = updatePropertyTarget; + this.factory = new this.mode.property!(this); break; case "classList": - this.updateTarget = updateClassListTarget; + this.factory = new this.mode.tokenList!(this); break; default: - this.updateTarget = updatePropertyTarget; + this.factory = new this.mode.property!(this); break; } break; case "?": this.cleanedTargetName = value.substr(1); - this.updateTarget = updateBooleanAttributeTarget; + this.factory = new this.mode.booleanAttribute!(this); break; case "@": this.cleanedTargetName = value.substr(1); - this.bind = triggerBind; - this.unbind = triggerUnbind; + this.factory = new this.mode.event!(this); break; default: this.cleanedTargetName = value; if (value === "class") { this.cleanedTargetName = "className"; - this.updateTarget = updatePropertyTarget; + this.factory = new this.mode.property!(this); + } else { + this.factory = new this.mode.attribute!(this); } break; } } - /** - * Makes this binding target the content of an element rather than - * a particular attribute or property. - */ public targetAtContent(): void { - this.updateTarget = updateContentTarget; - this.unbind = contentUnbind; + this.factory = new this.mode.content!(this); } - /** - * Creates the runtime BindingBehavior instance based on the configuration - * information stored in the BindingDirective. - * @param target - The target node that the binding behavior should attach to. - */ - createBehavior(targets: ViewBehaviorTargets): BindingBehavior { - /* eslint-disable-next-line @typescript-eslint/no-use-before-define */ - return new BindingBehavior( - targets[this.targetId], - this.binding, - this.isBindingVolatile, - this.bind, - this.unbind, - this.updateTarget, - this.cleanedTargetName - ); + createBehavior(targets: ViewBehaviorTargets): ViewBehavior { + return this.factory.createBehavior(targets); } } -/** - * A behavior that updates content and attributes based on a configured - * BindingDirective. - * @public - */ -export class BindingBehavior implements Behavior { - /** @internal */ - public value: any = null; - - /** @internal */ - public source: unknown = null; - - /** @internal */ - public context: ExecutionContext | null = null; - - /** @internal */ - public bindingObserver: BindingObserver | null = null; - - /** @internal */ - public classVersions: Record; - - /** @internal */ - public version: number; - - /** @internal */ - public target: any; - - /** @internal */ - public binding: Binding; - - /** @internal */ - public isBindingVolatile: boolean; - - /** @internal */ - public updateTarget: typeof updatePropertyTarget; - - /** @internal */ - public targetName?: string; - - /** - * Bind this behavior to the source. - * @param source - The source to bind to. - * @param context - The execution context that the binding is operating within. - */ - public bind: typeof normalBind; - - /** - * Unbinds this behavior from the source. - * @param source - The source to unbind from. - */ - public unbind: typeof normalUnbind; - - /** - * Creates an instance of BindingBehavior. - * @param target - The target of the data updates. - * @param binding - The binding that returns the latest value for an update. - * @param isBindingVolatile - Indicates whether the binding has volatile dependencies. - * @param bind - The operation to perform during binding. - * @param unbind - The operation to perform during unbinding. - * @param updateTarget - The operation to perform when updating. - * @param targetName - The name of the target attribute or property to update. - */ - public constructor( - target: any, - binding: Binding, - isBindingVolatile: boolean, - bind: typeof normalBind, - unbind: typeof normalUnbind, - updateTarget: typeof updatePropertyTarget, - targetName?: string - ) { - this.target = target; - this.binding = binding; - this.isBindingVolatile = isBindingVolatile; - this.bind = bind; - this.unbind = unbind; - this.updateTarget = updateTarget; - this.targetName = targetName; - } - - /** @internal */ - public handleChange(): void { - const newValue = this.bindingObserver!.observe(this.source, this.context!); - - if (this.value !== newValue) { - this.value = newValue; - this.updateTarget(newValue); - } +export function bind( + binding: Binding, + config: BindingConfig | DefaultBindingOptions = updateView +): CaptureType { + if (!("mode" in config)) { + config = updateView(config); } - /** @internal */ - public handleEvent(event: Event): void { - setCurrentEvent(event); - const result = this.binding(this.source, this.context!); - setCurrentEvent(null); - - if (result !== true) { - event.preventDefault(); - } - } + return new HTMLBindingDirective(binding, config.mode, config.options); } diff --git a/packages/web-components/fast-element/src/templating/compiler.spec.ts b/packages/web-components/fast-element/src/templating/compiler.spec.ts index 4e80842363f..cfeca6b29be 100644 --- a/packages/web-components/fast-element/src/templating/compiler.spec.ts +++ b/packages/web-components/fast-element/src/templating/compiler.spec.ts @@ -5,7 +5,7 @@ import { defaultExecutionContext } from "../observation/observable"; import { css } from "../styles/css"; import type { StyleTarget } from "../styles/element-styles"; import { toHTML, uniqueElementName } from "../__test__/helpers"; -import { HTMLBindingDirective } from "./binding"; +import { bind, HTMLBindingDirective } from "./binding"; import { compileTemplate } from "./compiler"; import type { HTMLDirective } from "./html-directive"; import { html } from "./template"; @@ -22,7 +22,7 @@ describe("The template compiler", () => { } function binding(result = "result") { - return new HTMLBindingDirective(() => result); + return bind(() => result) as HTMLBindingDirective; } const scope = {}; diff --git a/packages/web-components/fast-element/src/templating/compiler.ts b/packages/web-components/fast-element/src/templating/compiler.ts index 31dc22cc3a1..6b21959740b 100644 --- a/packages/web-components/fast-element/src/templating/compiler.ts +++ b/packages/web-components/fast-element/src/templating/compiler.ts @@ -1,8 +1,9 @@ import type { ViewBehaviorTargets } from "./html-directive"; import { _interpolationEnd, _interpolationStart, DOM } from "../dom"; import type { Binding, ExecutionContext } from "../observation/observable"; -import { HTMLBindingDirective } from "./binding"; +import { bind, HTMLBindingDirective } from "./binding"; import type { HTMLDirective, ViewBehaviorFactory } from "./html-directive"; +import { oneTime } from ".."; type InlineDirective = HTMLDirective & { targetName?: string; @@ -161,7 +162,7 @@ function createAggregateBinding( return output; }; - const directive = new HTMLBindingDirective(binding); + const directive = bind(binding) as HTMLBindingDirective; directive.targetName = targetName; return directive; } @@ -219,7 +220,7 @@ function compileAttributes( if (parseResult === null) { if (includeBasicValues) { - result = new HTMLBindingDirective(() => attrValue); + result = bind(() => attrValue, oneTime) as HTMLBindingDirective; result.targetName = attr.name; } } else { diff --git a/packages/web-components/fast-element/src/templating/template.spec.ts b/packages/web-components/fast-element/src/templating/template.spec.ts index c81a1d13530..6cfa93e2abf 100644 --- a/packages/web-components/fast-element/src/templating/template.spec.ts +++ b/packages/web-components/fast-element/src/templating/template.spec.ts @@ -3,7 +3,7 @@ import { html, ViewTemplate } from "./template"; import { DOM } from "../dom"; import { HTMLBindingDirective } from "./binding"; import { HTMLDirective, TargetedHTMLDirective } from "./html-directive"; -import type { ViewBehaviorTargets } from ".."; +import { bind, ViewBehaviorTargets } from ".."; describe(`The html tag template helper`, () => { it(`transforms a string into a ViewTemplate.`, () => { @@ -217,7 +217,7 @@ describe(`The html tag template helper`, () => { }); it(`captures a case-sensitive property name when used with a binding`, () => { - const template = html` x.value)}>`; + const template = html` x.value)}>`; const placeholder = DOM.createInterpolationPlaceholder(0); expect(template.html).to.equal( diff --git a/packages/web-components/fast-element/src/templating/template.ts b/packages/web-components/fast-element/src/templating/template.ts index 3157a0a7dc7..350cadfe9be 100644 --- a/packages/web-components/fast-element/src/templating/template.ts +++ b/packages/web-components/fast-element/src/templating/template.ts @@ -4,7 +4,7 @@ import { compileTemplate } from "./compiler"; import type { HTMLTemplateCompilationResult } from "./compiler"; import { ElementView, HTMLView, SyntheticView } from "./view"; import { HTMLDirective, TargetedHTMLDirective } from "./html-directive"; -import { HTMLBindingDirective } from "./binding"; +import { bind } from "./binding"; /** * A template capable of creating views specifically for rendering custom elements. @@ -182,7 +182,7 @@ export function html( } if (typeof value === "function") { - value = new HTMLBindingDirective(value as Binding); + value = bind(value as Binding); } if (value instanceof TargetedHTMLDirective) { From abf54c7fcda77695b9b6252d34fd18dd5f8c1254 Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Wed, 20 Oct 2021 17:59:00 -0400 Subject: [PATCH 030/135] fix: correct handle change signature for content binding --- packages/web-components/fast-element/src/templating/binding.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web-components/fast-element/src/templating/binding.ts b/packages/web-components/fast-element/src/templating/binding.ts index ef18d8a6044..8332e78cfd7 100644 --- a/packages/web-components/fast-element/src/templating/binding.ts +++ b/packages/web-components/fast-element/src/templating/binding.ts @@ -103,7 +103,7 @@ abstract class ViewUpdateBinding implements ViewBinding { } /** @internal */ - public handleChange(observer: BindingObserver): void { + public handleChange(binding: Binding, observer: BindingObserver): void { const target = (observer as any).target; const source = (observer as any).source; const context = (observer as any).context; From e4a1548ac64e2d451302e3e9c45f389664570ac0 Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Thu, 21 Oct 2021 21:49:26 -0400 Subject: [PATCH 031/135] feat: enable interpolating non-strings as one time bindings in templates --- .../fast-element/docs/api-report.md | 2 +- .../src/templating/template.spec.ts | 54 +++++++++++-------- .../fast-element/src/templating/template.ts | 37 ++++++------- 3 files changed, 51 insertions(+), 42 deletions(-) diff --git a/packages/web-components/fast-element/docs/api-report.md b/packages/web-components/fast-element/docs/api-report.md index 8ed280b7e09..4fb56954272 100644 --- a/packages/web-components/fast-element/docs/api-report.md +++ b/packages/web-components/fast-element/docs/api-report.md @@ -558,7 +558,7 @@ export abstract class TargetedHTMLDirective extends HTMLDirective { } // @public -export type TemplateValue = Binding | string | number | HTMLDirective | CaptureType; +export type TemplateValue = Binding | HTMLDirective | CaptureType; // @public export type TrustedTypes = { diff --git a/packages/web-components/fast-element/src/templating/template.spec.ts b/packages/web-components/fast-element/src/templating/template.spec.ts index 6cfa93e2abf..6db413bc341 100644 --- a/packages/web-components/fast-element/src/templating/template.spec.ts +++ b/packages/web-components/fast-element/src/templating/template.spec.ts @@ -51,19 +51,19 @@ describe(`The html tag template helper`, () => { type: "number", location: "at the beginning", template: html`${numberValue} end`, - result: `${numberValue} end`, + result: `${DOM.createInterpolationPlaceholder(0)} end`, }, { type: "number", location: "in the middle", template: html`beginning ${numberValue} end`, - result: `beginning ${numberValue} end`, + result: `beginning ${DOM.createInterpolationPlaceholder(0)} end`, }, { type: "number", location: "at the end", template: html`beginning ${numberValue}`, - result: `beginning ${numberValue}`, + result: `beginning ${DOM.createInterpolationPlaceholder(0)}`, }, // expression interpolation { @@ -136,58 +136,70 @@ describe(`The html tag template helper`, () => { type: "mixed, back-to-back string, number, expression, and directive", location: "at the beginning", template: html`${stringValue}${numberValue}${x => x.value}${new TestDirective()} end`, - result: `${stringValue}${numberValue}${DOM.createInterpolationPlaceholder( + result: `${stringValue}${DOM.createInterpolationPlaceholder( 0 - )}${DOM.createBlockPlaceholder(1)} end`, - expectDirectives: [HTMLBindingDirective, TestDirective], + )}${DOM.createInterpolationPlaceholder( + 1 + )}${DOM.createBlockPlaceholder(2)} end`, + expectDirectives: [HTMLBindingDirective, HTMLBindingDirective, TestDirective], }, { type: "mixed, back-to-back string, number, expression, and directive", location: "in the middle", template: html`beginning ${stringValue}${numberValue}${x => x.value}${new TestDirective()} end`, - result: `beginning ${stringValue}${numberValue}${DOM.createInterpolationPlaceholder( + result: `beginning ${stringValue}${DOM.createInterpolationPlaceholder( 0 - )}${DOM.createBlockPlaceholder(1)} end`, - expectDirectives: [HTMLBindingDirective, TestDirective], + )}${DOM.createInterpolationPlaceholder( + 1 + )}${DOM.createBlockPlaceholder(2)} end`, + expectDirectives: [HTMLBindingDirective, HTMLBindingDirective, TestDirective], }, { type: "mixed, back-to-back string, number, expression, and directive", location: "at the end", template: html`beginning ${stringValue}${numberValue}${x => x.value}${new TestDirective()}`, - result: `beginning ${stringValue}${numberValue}${DOM.createInterpolationPlaceholder( + result: `beginning ${stringValue}${DOM.createInterpolationPlaceholder( 0 - )}${DOM.createBlockPlaceholder(1)}`, - expectDirectives: [HTMLBindingDirective, TestDirective], + )}${DOM.createInterpolationPlaceholder( + 1 + )}${DOM.createBlockPlaceholder(2)}`, + expectDirectives: [HTMLBindingDirective, HTMLBindingDirective, TestDirective], }, { type: "mixed, separated string, number, expression, and directive", location: "at the beginning", template: html`${stringValue}separator${numberValue}separator${x => x.value}separator${new TestDirective()} end`, - result: `${stringValue}separator${numberValue}separator${DOM.createInterpolationPlaceholder( + result: `${stringValue}separator${DOM.createInterpolationPlaceholder( 0 - )}separator${DOM.createBlockPlaceholder(1)} end`, - expectDirectives: [HTMLBindingDirective, TestDirective], + )}separator${DOM.createInterpolationPlaceholder( + 1 + )}separator${DOM.createBlockPlaceholder(2)} end`, + expectDirectives: [HTMLBindingDirective, HTMLBindingDirective, TestDirective], }, { type: "mixed, separated string, number, expression, and directive", location: "in the middle", template: html`beginning ${stringValue}separator${numberValue}separator${x => x.value}separator${new TestDirective()} end`, - result: `beginning ${stringValue}separator${numberValue}separator${DOM.createInterpolationPlaceholder( + result: `beginning ${stringValue}separator${DOM.createInterpolationPlaceholder( 0 - )}separator${DOM.createBlockPlaceholder(1)} end`, - expectDirectives: [HTMLBindingDirective, TestDirective], + )}separator${DOM.createInterpolationPlaceholder( + 1 + )}separator${DOM.createBlockPlaceholder(2)} end`, + expectDirectives: [HTMLBindingDirective, HTMLBindingDirective, TestDirective], }, { type: "mixed, separated string, number, expression, and directive", location: "at the end", template: html`beginning ${stringValue}separator${numberValue}separator${x => x.value}separator${new TestDirective()}`, - result: `beginning ${stringValue}separator${numberValue}separator${DOM.createInterpolationPlaceholder( + result: `beginning ${stringValue}separator${DOM.createInterpolationPlaceholder( 0 - )}separator${DOM.createBlockPlaceholder(1)}`, - expectDirectives: [HTMLBindingDirective, TestDirective], + )}separator${DOM.createInterpolationPlaceholder( + 1 + )}separator${DOM.createBlockPlaceholder(2)}`, + expectDirectives: [HTMLBindingDirective, HTMLBindingDirective, TestDirective], }, ]; diff --git a/packages/web-components/fast-element/src/templating/template.ts b/packages/web-components/fast-element/src/templating/template.ts index 350cadfe9be..0d706e7bab7 100644 --- a/packages/web-components/fast-element/src/templating/template.ts +++ b/packages/web-components/fast-element/src/templating/template.ts @@ -4,7 +4,7 @@ import { compileTemplate } from "./compiler"; import type { HTMLTemplateCompilationResult } from "./compiler"; import { ElementView, HTMLView, SyntheticView } from "./view"; import { HTMLDirective, TargetedHTMLDirective } from "./html-directive"; -import { bind } from "./binding"; +import { bind, oneTime } from "./binding"; /** * A template capable of creating views specifically for rendering custom elements. @@ -149,8 +149,6 @@ export interface CaptureType {} */ export type TemplateValue = | Binding - | string - | number | HTMLDirective | CaptureType; @@ -172,34 +170,33 @@ export function html( for (let i = 0, ii = strings.length - 1; i < ii; ++i) { const currentString = strings[i]; - let value = values[i]; + let currentValue = values[i]; + const valueType = typeof currentValue; html += currentString; - if (value instanceof ViewTemplate) { - const template = value; - value = (): ViewTemplate => template; + if (valueType === "function") { + currentValue = bind(currentValue as Binding); + } else if (valueType !== "string" && !(currentValue instanceof HTMLDirective)) { + const capturedValue = currentValue; + currentValue = bind(() => capturedValue, oneTime); } - if (typeof value === "function") { - value = bind(value as Binding); - } - - if (value instanceof TargetedHTMLDirective) { - const match = lastAttributeNameRegex.exec(currentString); - if (match !== null) { - value.targetName = match[2]; + if (currentValue instanceof HTMLDirective) { + if (currentValue instanceof TargetedHTMLDirective) { + const match = lastAttributeNameRegex.exec(currentString); + if (match !== null) { + currentValue.targetName = match[2]; + } } - } - if (value instanceof HTMLDirective) { // Since not all values are directives, we can't use i // as the index for the placeholder. Instead, we need to // use directives.length to get the next index. - html += value.createPlaceholder(directives.length); - directives.push(value); + html += currentValue.createPlaceholder(directives.length); + directives.push(currentValue); } else { - html += value; + html += currentValue; } } From 68d575605e011f88104bd4a9a27b0623bcdc2be1 Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Thu, 21 Oct 2021 21:59:47 -0400 Subject: [PATCH 032/135] refactor: remove duplication in one time event --- .../fast-element/src/templating/binding.ts | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/web-components/fast-element/src/templating/binding.ts b/packages/web-components/fast-element/src/templating/binding.ts index 8332e78cfd7..0ceb3b4ae3f 100644 --- a/packages/web-components/fast-element/src/templating/binding.ts +++ b/packages/web-components/fast-element/src/templating/binding.ts @@ -311,11 +311,16 @@ function createContentBindingFactory< }; } +type FASTEventSource = Node & { + $fastSource: any; + $fastContext: ExecutionContext | null; +}; + class EventListener implements BindingBehaviorFactory { constructor(public directive: HTMLBindingDirective) {} bind(source: any, context: ExecutionContext, targets: ViewBehaviorTargets): void { - const target = targets[this.directive.targetId] as any; + const target = targets[this.directive.targetId] as FASTEventSource; target.$fastSource = source; target.$fastContext = context; target.addEventListener( @@ -326,7 +331,10 @@ class EventListener implements BindingBehaviorFactory { } unbind(source: any, context: ExecutionContext, targets: ViewBehaviorTargets): void { - const target = targets[this.directive.targetId] as any; + this.removeEventListener(targets[this.directive.targetId] as FASTEventSource); + } + + protected removeEventListener(target: FASTEventSource) { target.$fastSource = null; target.$fastContext = null; target.removeEventListener( @@ -358,14 +366,7 @@ class EventListener implements BindingBehaviorFactory { class OneTimeEventListener extends EventListener { handleEvent(event: Event) { super.handleEvent(event); - const target = event.currentTarget as any; - target.$fastSource = null; - target.$fastContext = null; - target.removeEventListener( - this.directive.cleanedTargetName!, - this, - this.directive.options - ); + this.removeEventListener(event.currentTarget as FASTEventSource); } } From 84661eeb6c127a321f48ccb0da41dd9ce929676f Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Thu, 21 Oct 2021 23:31:22 -0400 Subject: [PATCH 033/135] refactor: reduce some duplication when creating binding configs --- .../fast-element/docs/api-report.md | 4 +- .../fast-element/src/templating/binding.ts | 61 ++++++++----------- 2 files changed, 27 insertions(+), 38 deletions(-) diff --git a/packages/web-components/fast-element/docs/api-report.md b/packages/web-components/fast-element/docs/api-report.md index 4fb56954272..c79024f6700 100644 --- a/packages/web-components/fast-element/docs/api-report.md +++ b/packages/web-components/fast-element/docs/api-report.md @@ -408,7 +408,7 @@ export interface ObservationRecord { } // @public (undocumented) -export const oneTime: BindingConfig & ((options?: DefaultBindingOptions) => BindingConfig); +export const oneTime: BindingConfig & ((options?: DefaultBindingOptions | undefined) => BindingConfig); // @public export interface PartialFASTElementDefinition { @@ -571,7 +571,7 @@ export type TrustedTypesPolicy = { }; // @public (undocumented) -export const updateView: BindingConfig & ((options?: DefaultBindingOptions) => BindingConfig); +export const updateView: BindingConfig & ((options?: DefaultBindingOptions | undefined) => BindingConfig); // @public export interface ValueConverter { diff --git a/packages/web-components/fast-element/src/templating/binding.ts b/packages/web-components/fast-element/src/templating/binding.ts index 0ceb3b4ae3f..0dca3ec2e86 100644 --- a/packages/web-components/fast-element/src/templating/binding.ts +++ b/packages/web-components/fast-element/src/templating/binding.ts @@ -378,45 +378,34 @@ const defaultBindingOptions: DefaultBindingOptions = { capture: false, }; -export const updateView: BindingConfig & - ((options?: DefaultBindingOptions) => BindingConfig) = ( - options: DefaultBindingOptions -): BindingConfig => { - return { - mode: updateView.mode, - options: Object.assign({}, defaultBindingOptions, options), +function createBindingConfig>( + BindingBase: T, + EventListener: Constructable +) { + const config: BindingConfig & ((options?: DefaultBindingOptions) => BindingConfig) = ( + options: DefaultBindingOptions + ): BindingConfig => { + return { + mode: config.mode, + options: Object.assign({}, defaultBindingOptions, options), + }; }; -}; -updateView.options = defaultBindingOptions; -updateView.mode = Object.freeze({ - attribute: createAttributeBindingFactory(ViewUpdateBinding as any), - booleanAttribute: createBooleanAttributeBindingFactory(ViewUpdateBinding as any), - property: createPropertyBindingFactory(ViewUpdateBinding as any), - content: createContentBindingFactory(ViewUpdateBinding as any), - tokenList: createTokenListBindingFactory(ViewUpdateBinding as any), - event: EventListener, -}); - -export const oneTime: BindingConfig & - ((options?: DefaultBindingOptions) => BindingConfig) = ( - options: DefaultBindingOptions -): BindingConfig => { - return { - mode: oneTime.mode, - options: Object.assign({}, defaultBindingOptions, options), - }; -}; + config.options = defaultBindingOptions; + config.mode = Object.freeze({ + attribute: createAttributeBindingFactory(BindingBase), + booleanAttribute: createBooleanAttributeBindingFactory(BindingBase), + property: createPropertyBindingFactory(BindingBase), + content: createContentBindingFactory(BindingBase), + tokenList: createTokenListBindingFactory(BindingBase), + event: EventListener, + }); + + return config; +} -oneTime.options = defaultBindingOptions; -oneTime.mode = Object.freeze({ - attribute: createAttributeBindingFactory(ViewSetBinding as any), - booleanAttribute: createBooleanAttributeBindingFactory(ViewSetBinding as any), - property: createPropertyBindingFactory(ViewSetBinding as any), - content: createContentBindingFactory(ViewSetBinding as any), - tokenList: createTokenListBindingFactory(ViewSetBinding as any), - event: OneTimeEventListener, -}); +export const updateView = createBindingConfig(ViewUpdateBinding as any, EventListener); +export const oneTime = createBindingConfig(ViewSetBinding as any, OneTimeEventListener); export class HTMLBindingDirective extends TargetedHTMLDirective { private originalTargetName?: string; From 9a96946a9b986182f2a6dd69fcb4bdcb33baf352 Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Sat, 23 Oct 2021 13:50:43 -0400 Subject: [PATCH 034/135] refactor: reduce duplication and simplify new binding system --- .../fast-element/docs/api-report.md | 29 +- .../fast-element/src/templating/binding.ts | 413 ++++++++---------- 2 files changed, 205 insertions(+), 237 deletions(-) diff --git a/packages/web-components/fast-element/docs/api-report.md b/packages/web-components/fast-element/docs/api-report.md index c79024f6700..a054b9827ba 100644 --- a/packages/web-components/fast-element/docs/api-report.md +++ b/packages/web-components/fast-element/docs/api-report.md @@ -61,7 +61,6 @@ export type Binding = (source: TSou // @public (undocumented) export type BindingBehaviorFactory = { - readonly directive: HTMLBindingDirective; createBehavior(targets: ViewBehaviorTargets): ViewBehavior; }; @@ -73,23 +72,20 @@ export interface BindingConfig { options: any; } -// @public (undocumented) -export type BindingFactory = new (directive: HTMLBindingDirective) => BindingBehaviorFactory; - // @public (undocumented) export interface BindingMode { // (undocumented) - attribute?: BindingFactory; + attribute?: BindingType; // (undocumented) - booleanAttribute?: BindingFactory; + booleanAttribute?: BindingType; // (undocumented) - content?: BindingFactory; + content?: BindingType; // (undocumented) - event?: BindingFactory; + event?: BindingType; // (undocumented) - property?: BindingFactory; + property?: BindingType; // (undocumented) - tokenList?: BindingFactory; + tokenList?: BindingType; } // @public @@ -99,6 +95,9 @@ export interface BindingObserver ex records(): IterableIterator; } +// @public (undocumented) +export type BindingType = (directive: HTMLBindingDirective) => BindingBehaviorFactory; + // @public export const booleanConverter: ValueConverter; @@ -318,14 +317,14 @@ export class HTMLBindingDirective extends TargetedHTMLDirective { // (undocumented) binding: Binding; // (undocumented) - cleanedTargetName?: string; - // (undocumented) createBehavior(targets: ViewBehaviorTargets): ViewBehavior; // (undocumented) mode: BindingMode; // (undocumented) options: any; // (undocumented) + targetAspect?: string; + // (undocumented) targetAtContent(): void; get targetName(): string | undefined; set targetName(value: string | undefined); @@ -407,6 +406,9 @@ export interface ObservationRecord { propertySource: any; } +// @public (undocumented) +export const onChange: BindingConfig & ((options?: DefaultBindingOptions | undefined) => BindingConfig); + // @public (undocumented) export const oneTime: BindingConfig & ((options?: DefaultBindingOptions | undefined) => BindingConfig); @@ -570,9 +572,6 @@ export type TrustedTypesPolicy = { createHTML(html: string): string; }; -// @public (undocumented) -export const updateView: BindingConfig & ((options?: DefaultBindingOptions | undefined) => BindingConfig); - // @public export interface ValueConverter { fromView(value: any): any; diff --git a/packages/web-components/fast-element/src/templating/binding.ts b/packages/web-components/fast-element/src/templating/binding.ts index 0dca3ec2e86..a94e383edcc 100644 --- a/packages/web-components/fast-element/src/templating/binding.ts +++ b/packages/web-components/fast-element/src/templating/binding.ts @@ -16,21 +16,18 @@ import type { CaptureType } from "./template"; import type { SyntheticView } from "./view"; export type BindingBehaviorFactory = { - readonly directive: HTMLBindingDirective; createBehavior(targets: ViewBehaviorTargets): ViewBehavior; }; -export type BindingFactory = new ( - directive: HTMLBindingDirective -) => BindingBehaviorFactory; +export type BindingType = (directive: HTMLBindingDirective) => BindingBehaviorFactory; export interface BindingMode { - attribute?: BindingFactory; - booleanAttribute?: BindingFactory; - property?: BindingFactory; - content?: BindingFactory; - tokenList?: BindingFactory; - event?: BindingFactory; + attribute?: BindingType; + booleanAttribute?: BindingType; + property?: BindingType; + content?: BindingType; + tokenList?: BindingType; + event?: BindingType; } export interface BindingConfig { @@ -38,13 +35,40 @@ export interface BindingConfig { options: any; } -interface ViewBinding extends BindingBehaviorFactory, ViewBehavior { - updateTarget(target: Node, value: any, source: any, context: ExecutionContext): void; +interface UpdateTargetThis { + directive: HTMLBindingDirective; } -abstract class ViewSetBinding implements ViewBinding { +type UpdateTarget = ( + this: UpdateTargetThis, + target, + value, + source: any, + context: ExecutionContext +) => void; + +class BindingBase { constructor(public readonly directive: HTMLBindingDirective) {} + bind(source: any, context: ExecutionContext, targets: ViewBehaviorTargets): void {} + unbind(source: any, context: ExecutionContext, targets: ViewBehaviorTargets): void {} + + createBehavior(): ViewBehavior { + return this; + } +} + +class TargetUpdateBinding extends BindingBase { + constructor(directive: HTMLBindingDirective, protected updateTarget: UpdateTarget) { + super(directive); + } + + static createType(updateTarget: UpdateTarget) { + return directive => new this(directive, updateTarget); + } +} + +class OneTimeBinding extends TargetUpdateBinding { bind(source: any, context: ExecutionContext, targets: ViewBehaviorTargets): void { const target = targets[this.directive.targetId]; this.updateTarget( @@ -54,25 +78,13 @@ abstract class ViewSetBinding implements ViewBinding { context ); } - - unbind(): void {} - - createBehavior(): ViewBehavior { - return this; - } - - abstract updateTarget( - target: Node, - value: any, - source: any, - context: ExecutionContext - ): void; } -abstract class ViewUpdateBinding implements ViewBinding { +class OnChangeBinding extends TargetUpdateBinding { private isBindingVolatile: boolean; - constructor(public readonly directive: HTMLBindingDirective) { + constructor(directive: HTMLBindingDirective, updateTarget: UpdateTarget) { + super(directive, updateTarget); this.isBindingVolatile = Observable.isVolatileBinding(directive.binding); } @@ -109,109 +121,76 @@ abstract class ViewUpdateBinding implements ViewBinding { const context = (observer as any).context; this.updateTarget(target, observer.observe(source, context!), source, context); } - - abstract updateTarget( - target: Node, - value: any, - source: any, - context: ExecutionContext - ): void; - - createBehavior(): ViewBehavior { - return this; - } } -function createPropertyBindingFactory< - T extends Constructable ->(BindingBase: T): BindingFactory { - return class extends BindingBase { - updateTarget(target: Node, value: any): void { - target[this.directive.cleanedTargetName!] = value; - } - }; +function setPropertyTarget(this: UpdateTargetThis, target, value) { + target[this.directive.targetAspect!] = value; } -function createAttributeBindingFactory< - T extends Constructable ->(BindingBase: T): BindingFactory { - return class extends BindingBase { - updateTarget(target: Node, value: any): void { - DOM.setAttribute( - target as HTMLElement, - this.directive.cleanedTargetName!, - value - ); - } - }; +function setAttributeTarget(this: UpdateTargetThis, target, value) { + DOM.setAttribute(target as HTMLElement, this.directive.targetAspect!, value); } -function createBooleanAttributeBindingFactory< - T extends Constructable ->(BindingBase: T): BindingFactory { - return class extends BindingBase { - updateTarget(target: Node, value: any): void { - DOM.setBooleanAttribute( - target as HTMLElement, - this.directive.cleanedTargetName!, - value as boolean - ); - } - }; +function setBooleanAttributeTarget(this: UpdateTargetThis, target, value) { + DOM.setBooleanAttribute( + target as HTMLElement, + this.directive.targetAspect!, + value as boolean + ); } -function createTokenListBindingFactory>( - BindingBase: T -): BindingFactory { - class TokenListBinding extends BindingBase { - classVersions = Object.create(null); - version = 0; - - updateTarget(target: Element, value: any): void { - const classVersions = this.classVersions; - const tokenList = target[this.directive.cleanedTargetName!] as DOMTokenList; - let version = this.version; +interface UpdateTokenListThis extends UpdateTargetThis { + classVersions: any; + version: number; +} - // Add the classes, tracking the version at which they were added. - if (value !== null && value !== undefined && value.length) { - const names = value.split(/\s+/); +function updateTokenListTarget( + this: UpdateTokenListThis, + target: Element, + value: any +): void { + const classVersions = this.classVersions; + const tokenList = target[this.directive.targetAspect!] as DOMTokenList; + let version = this.version; - for (let i = 0, ii = names.length; i < ii; ++i) { - const currentName = names[i]; + // Add the classes, tracking the version at which they were added. + if (value !== null && value !== undefined && value.length) { + const names = value.split(/\s+/); - if (currentName === "") { - continue; - } + for (let i = 0, ii = names.length; i < ii; ++i) { + const currentName = names[i]; - classVersions[currentName] = version; - tokenList.add(currentName); - } + if (currentName === "") { + continue; } - this.classVersions = classVersions; - this.version = version + 1; + classVersions[currentName] = version; + tokenList.add(currentName); + } + } - // If this is the first call to add classes, there's no need to remove old ones. - if (version === 0) { - return; - } + this.classVersions = classVersions; + this.version = version + 1; - // Remove classes from the previous version. - version -= 1; + // If this is the first call to add classes, there's no need to remove old ones. + if (version === 0) { + return; + } - for (const name in classVersions) { - if (classVersions[name] === version) { - tokenList.remove(name); - } - } + // Remove classes from the previous version. + version -= 1; + + for (const name in classVersions) { + if (classVersions[name] === version) { + tokenList.remove(name); } } +} - return class implements BindingBehaviorFactory { - constructor(public directive: HTMLBindingDirective) {} - createBehavior() { - return new TokenListBinding(this.directive); - } +function createTokenListBinding(BaseType: typeof TargetUpdateBinding) { + return class TokenListBinding extends BaseType implements UpdateTokenListThis { + classVersions = Object.create(null); + version = 0; }; } @@ -225,87 +204,85 @@ type ContentTarget = Node & { $fastTemplate?: { create(): SyntheticView }; }; -function createContentBindingFactory< - T extends Constructable ->(BindingBase: T): BindingFactory { - return class extends BindingBase { - unbind(source: any, context: ExecutionContext, targets: ViewBehaviorTargets) { - super.unbind(source, context, targets); +function updateContentTarget( + target: ContentTarget, + value: any, + source: any, + context: ExecutionContext +): void { + // If there's no actual value, then this equates to the + // empty string for the purposes of content bindings. + if (value === null || value === undefined) { + value = ""; + } - const target = targets[this.directive.targetId] as ContentTarget; - const view = target.$fastView as ComposableView; + // If the value has a "create" method, then it's a template-like. + if (value.create) { + target.textContent = ""; + let view = target.$fastView as ComposableView; + + // If there's no previous view that we might be able to + // reuse then create a new view from the template. + if (view === void 0) { + view = value.create() as SyntheticView; + } else { + // If there is a previous view, but it wasn't created + // from the same template as the new value, then we + // need to remove the old view if it's still in the DOM + // and create a new view from the template. + if (target.$fastTemplate !== value) { + if (view.isComposed) { + view.remove(); + view.unbind(); + } - if (view !== void 0 && view.isComposed) { - view.unbind(); - view.needsBindOnly = true; + view = value.create() as SyntheticView; } } - updateTarget( - target: ContentTarget, - value: any, - source: any, - context: ExecutionContext - ): void { - // If there's no actual value, then this equates to the - // empty string for the purposes of content bindings. - if (value === null || value === undefined) { - value = ""; - } - - // If the value has a "create" method, then it's a template-like. - if (value.create) { - target.textContent = ""; - let view = target.$fastView as ComposableView; + // It's possible that the value is the same as the previous template + // and that there's actually no need to compose it. + if (!view.isComposed) { + view.isComposed = true; + view.bind(source, context!); + view.insertBefore(target); + target.$fastView = view; + target.$fastTemplate = value; + } else if (view.needsBindOnly) { + view.needsBindOnly = false; + view.bind(source, context!); + } + } else { + const view = target.$fastView as ComposableView; - // If there's no previous view that we might be able to - // reuse then create a new view from the template. - if (view === void 0) { - view = value.create() as SyntheticView; - } else { - // If there is a previous view, but it wasn't created - // from the same template as the new value, then we - // need to remove the old view if it's still in the DOM - // and create a new view from the template. - if (target.$fastTemplate !== value) { - if (view.isComposed) { - view.remove(); - view.unbind(); - } - - view = value.create() as SyntheticView; - } - } + // If there is a view and it's currently composed into + // the DOM, then we need to remove it. + if (view !== void 0 && view.isComposed) { + view.isComposed = false; + view.remove(); - // It's possible that the value is the same as the previous template - // and that there's actually no need to compose it. - if (!view.isComposed) { - view.isComposed = true; - view.bind(source, context!); - view.insertBefore(target); - target.$fastView = view; - target.$fastTemplate = value; - } else if (view.needsBindOnly) { - view.needsBindOnly = false; - view.bind(source, context!); - } + if (view.needsBindOnly) { + view.needsBindOnly = false; } else { - const view = target.$fastView as ComposableView; + view.unbind(); + } + } - // If there is a view and it's currently composed into - // the DOM, then we need to remove it. - if (view !== void 0 && view.isComposed) { - view.isComposed = false; - view.remove(); + target.textContent = value; + } +} - if (view.needsBindOnly) { - view.needsBindOnly = false; - } else { - view.unbind(); - } - } +function createContentBinding(BaseType: typeof TargetUpdateBinding) { + return class extends BaseType { + unbind(source: any, context: ExecutionContext, targets: ViewBehaviorTargets) { + super.unbind(source, context, targets); + + const target = targets[this.directive.targetId] as ContentTarget; + const view = target.$fastView as ComposableView; - target.textContent = value; + if (view !== void 0 && view.isComposed) { + view.unbind(); + view.needsBindOnly = true; } } }; @@ -316,15 +293,13 @@ type FASTEventSource = Node & { $fastContext: ExecutionContext | null; }; -class EventListener implements BindingBehaviorFactory { - constructor(public directive: HTMLBindingDirective) {} - +class EventListener extends BindingBase { bind(source: any, context: ExecutionContext, targets: ViewBehaviorTargets): void { const target = targets[this.directive.targetId] as FASTEventSource; target.$fastSource = source; target.$fastContext = context; target.addEventListener( - this.directive.cleanedTargetName!, + this.directive.targetAspect!, this, this.directive.options ); @@ -338,7 +313,7 @@ class EventListener implements BindingBehaviorFactory { target.$fastSource = null; target.$fastContext = null; target.removeEventListener( - this.directive.cleanedTargetName!, + this.directive.targetAspect!, this, this.directive.options ); @@ -357,10 +332,6 @@ class EventListener implements BindingBehaviorFactory { event.preventDefault(); } } - - createBehavior(targets: ViewBehaviorTargets): ViewBehavior { - return this; - } } class OneTimeEventListener extends EventListener { @@ -378,8 +349,8 @@ const defaultBindingOptions: DefaultBindingOptions = { capture: false, }; -function createBindingConfig>( - BindingBase: T, +function createBindingConfig( + BaseType: typeof TargetUpdateBinding, EventListener: Constructable ) { const config: BindingConfig & ((options?: DefaultBindingOptions) => BindingConfig) = ( @@ -393,25 +364,25 @@ function createBindingConfig new EventListener(directive), }); return config; } -export const updateView = createBindingConfig(ViewUpdateBinding as any, EventListener); -export const oneTime = createBindingConfig(ViewSetBinding as any, OneTimeEventListener); +export const onChange = createBindingConfig(OnChangeBinding, EventListener); +export const oneTime = createBindingConfig(OneTimeBinding, OneTimeEventListener); export class HTMLBindingDirective extends TargetedHTMLDirective { - private originalTargetName?: string; + private originalTargetAspect?: string; private factory!: BindingBehaviorFactory; - public cleanedTargetName?: string; + public targetAspect?: string; public constructor( public binding: Binding, @@ -426,11 +397,11 @@ export class HTMLBindingDirective extends TargetedHTMLDirective { * binding is targeting. */ public get targetName(): string | undefined { - return this.originalTargetName; + return this.originalTargetAspect; } public set targetName(value: string | undefined) { - this.originalTargetName = value; + this.originalTargetAspect = value; if (value === void 0) { return; @@ -438,46 +409,44 @@ export class HTMLBindingDirective extends TargetedHTMLDirective { switch (value[0]) { case ":": - this.cleanedTargetName = value.substr(1); - switch (this.cleanedTargetName) { + this.targetAspect = value.substr(1); + switch (this.targetAspect) { case "innerHTML": const binding = this.binding; /* eslint-disable-next-line */ this.binding = (s, c) => DOM.createHTML(binding(s, c)); - this.factory = new this.mode.property!(this); + this.factory = this.mode.property!(this); break; case "classList": - this.factory = new this.mode.tokenList!(this); + this.factory = this.mode.tokenList!(this); break; default: - this.factory = new this.mode.property!(this); + this.factory = this.mode.property!(this); break; } break; case "?": - this.cleanedTargetName = value.substr(1); - this.factory = new this.mode.booleanAttribute!(this); + this.targetAspect = value.substr(1); + this.factory = this.mode.booleanAttribute!(this); break; case "@": - this.cleanedTargetName = value.substr(1); - this.factory = new this.mode.event!(this); + this.targetAspect = value.substr(1); + this.factory = this.mode.event!(this); break; default: - this.cleanedTargetName = value; - if (value === "class") { - this.cleanedTargetName = "className"; - this.factory = new this.mode.property!(this); + this.targetAspect = "className"; + this.factory = this.mode.property!(this); } else { - this.factory = new this.mode.attribute!(this); + this.targetAspect = value; + this.factory = this.mode.attribute!(this); } - break; } } public targetAtContent(): void { - this.factory = new this.mode.content!(this); + this.factory = this.mode.content!(this); } createBehavior(targets: ViewBehaviorTargets): ViewBehavior { @@ -487,10 +456,10 @@ export class HTMLBindingDirective extends TargetedHTMLDirective { export function bind( binding: Binding, - config: BindingConfig | DefaultBindingOptions = updateView + config: BindingConfig | DefaultBindingOptions = onChange ): CaptureType { if (!("mode" in config)) { - config = updateView(config); + config = onChange(config); } return new HTMLBindingDirective(binding, config.mode, config.options); From f6cf90b8c776ef2662d4b4f7e3c7f686e88a6680 Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Sat, 23 Oct 2021 15:09:21 -0400 Subject: [PATCH 035/135] refactor: improve compiler/directive protocol --- .../fast-element/docs/api-report.md | 31 ++++++---- .../src/templating/binding.spec.ts | 1 - .../fast-element/src/templating/binding.ts | 62 +++++++------------ .../fast-element/src/templating/compiler.ts | 45 ++++---------- .../src/templating/html-directive.ts | 18 +++--- .../src/templating/template.spec.ts | 22 ++++--- .../fast-element/src/templating/template.ts | 6 +- 7 files changed, 81 insertions(+), 104 deletions(-) diff --git a/packages/web-components/fast-element/docs/api-report.md b/packages/web-components/fast-element/docs/api-report.md index a054b9827ba..ad0736e38f6 100644 --- a/packages/web-components/fast-element/docs/api-report.md +++ b/packages/web-components/fast-element/docs/api-report.md @@ -14,6 +14,13 @@ export interface Accessor { setValue(source: any, value: any): void; } +// @public +export abstract class AspectedHTMLDirective extends HTMLDirective { + createPlaceholder: (index: number) => string; + // (undocumented) + abstract setAspect(value: string): void; +} + // @public export function attr(config?: DecoratorAttributeConfiguration): (target: {}, property: string) => void; @@ -312,9 +319,11 @@ export type Global = typeof globalThis & { export function html(strings: TemplateStringsArray, ...values: TemplateValue[]): ViewTemplate; // @public (undocumented) -export class HTMLBindingDirective extends TargetedHTMLDirective { +export class HTMLBindingDirective extends InlinableHTMLDirective { constructor(binding: Binding, mode: BindingMode, options: any); // (undocumented) + readonly aspect?: string; + // (undocumented) binding: Binding; // (undocumented) createBehavior(targets: ViewBehaviorTargets): ViewBehavior; @@ -323,11 +332,9 @@ export class HTMLBindingDirective extends TargetedHTMLDirective { // (undocumented) options: any; // (undocumented) - targetAspect?: string; + readonly rawAspect?: string; // (undocumented) - targetAtContent(): void; - get targetName(): string | undefined; - set targetName(value: string | undefined); + setAspect(value: string): void; } // @public @@ -360,6 +367,14 @@ export class HTMLView implemen unbind(): void; } +// @public (undocumented) +export abstract class InlinableHTMLDirective extends AspectedHTMLDirective { + // (undocumented) + abstract binding: Binding; + // (undocumented) + abstract rawAspect?: string; +} + // Warning: (ae-internal-missing-underscore) The name "Mutable" should be prefixed with an underscore because the declaration is marked as @internal // // @internal @@ -553,12 +568,6 @@ export interface SyntheticViewTemplate; } -// @public -export abstract class TargetedHTMLDirective extends HTMLDirective { - createPlaceholder: (index: number) => string; - abstract targetName: string | undefined; -} - // @public export type TemplateValue = Binding | HTMLDirective | CaptureType; diff --git a/packages/web-components/fast-element/src/templating/binding.spec.ts b/packages/web-components/fast-element/src/templating/binding.spec.ts index f6e0c1bf8c0..d0eb2b18dde 100644 --- a/packages/web-components/fast-element/src/templating/binding.spec.ts +++ b/packages/web-components/fast-element/src/templating/binding.spec.ts @@ -28,7 +28,6 @@ describe("The HTML binding directive", () => { function contentBinding(propertyName: keyof Model = "value") { const directive = bind(x => x[propertyName]) as HTMLBindingDirective; - directive.targetAtContent(); directive.targetId = 'r'; const node = document.createTextNode(" "); diff --git a/packages/web-components/fast-element/src/templating/binding.ts b/packages/web-components/fast-element/src/templating/binding.ts index a94e383edcc..09e2fa2bae7 100644 --- a/packages/web-components/fast-element/src/templating/binding.ts +++ b/packages/web-components/fast-element/src/templating/binding.ts @@ -1,5 +1,5 @@ import { DOM } from "../dom"; -import type { Constructable } from "../interfaces"; +import type { Constructable, Mutable } from "../interfaces"; import { Binding, BindingObserver, @@ -8,7 +8,7 @@ import { setCurrentEvent, } from "../observation/observable"; import { - TargetedHTMLDirective, + InlinableHTMLDirective, ViewBehavior, ViewBehaviorTargets, } from "./html-directive"; @@ -124,17 +124,17 @@ class OnChangeBinding extends TargetUpdateBinding { } function setPropertyTarget(this: UpdateTargetThis, target, value) { - target[this.directive.targetAspect!] = value; + target[this.directive.aspect!] = value; } function setAttributeTarget(this: UpdateTargetThis, target, value) { - DOM.setAttribute(target as HTMLElement, this.directive.targetAspect!, value); + DOM.setAttribute(target as HTMLElement, this.directive.aspect!, value); } function setBooleanAttributeTarget(this: UpdateTargetThis, target, value) { DOM.setBooleanAttribute( target as HTMLElement, - this.directive.targetAspect!, + this.directive.aspect!, value as boolean ); } @@ -150,7 +150,7 @@ function updateTokenListTarget( value: any ): void { const classVersions = this.classVersions; - const tokenList = target[this.directive.targetAspect!] as DOMTokenList; + const tokenList = target[this.directive.aspect!] as DOMTokenList; let version = this.version; // Add the classes, tracking the version at which they were added. @@ -298,11 +298,7 @@ class EventListener extends BindingBase { const target = targets[this.directive.targetId] as FASTEventSource; target.$fastSource = source; target.$fastContext = context; - target.addEventListener( - this.directive.targetAspect!, - this, - this.directive.options - ); + target.addEventListener(this.directive.aspect!, this, this.directive.options); } unbind(source: any, context: ExecutionContext, targets: ViewBehaviorTargets): void { @@ -312,11 +308,7 @@ class EventListener extends BindingBase { protected removeEventListener(target: FASTEventSource) { target.$fastSource = null; target.$fastContext = null; - target.removeEventListener( - this.directive.targetAspect!, - this, - this.directive.options - ); + target.removeEventListener(this.directive.aspect!, this, this.directive.options); } handleEvent(event: Event): void { @@ -378,11 +370,11 @@ function createBindingConfig( export const onChange = createBindingConfig(OnChangeBinding, EventListener); export const oneTime = createBindingConfig(OneTimeBinding, OneTimeEventListener); -export class HTMLBindingDirective extends TargetedHTMLDirective { - private originalTargetAspect?: string; +export class HTMLBindingDirective extends InlinableHTMLDirective { private factory!: BindingBehaviorFactory; - public targetAspect?: string; + public readonly rawAspect?: string; + public readonly aspect?: string; public constructor( public binding: Binding, @@ -392,25 +384,17 @@ export class HTMLBindingDirective extends TargetedHTMLDirective { super(); } - /** - * Gets/sets the name of the attribute or property that this - * binding is targeting. - */ - public get targetName(): string | undefined { - return this.originalTargetAspect; - } - - public set targetName(value: string | undefined) { - this.originalTargetAspect = value; + public setAspect(value: string) { + (this as Mutable).rawAspect = value; - if (value === void 0) { + if (!value) { return; } switch (value[0]) { case ":": - this.targetAspect = value.substr(1); - switch (this.targetAspect) { + (this as Mutable).aspect = value.substr(1); + switch (this.aspect) { case "innerHTML": const binding = this.binding; /* eslint-disable-next-line */ @@ -426,31 +410,27 @@ export class HTMLBindingDirective extends TargetedHTMLDirective { } break; case "?": - this.targetAspect = value.substr(1); + (this as Mutable).aspect = value.substr(1); this.factory = this.mode.booleanAttribute!(this); break; case "@": - this.targetAspect = value.substr(1); + (this as Mutable).aspect = value.substr(1); this.factory = this.mode.event!(this); break; default: if (value === "class") { - this.targetAspect = "className"; + (this as Mutable).aspect = "className"; this.factory = this.mode.property!(this); } else { - this.targetAspect = value; + (this as Mutable).aspect = value; this.factory = this.mode.attribute!(this); } break; } } - public targetAtContent(): void { - this.factory = this.mode.content!(this); - } - createBehavior(targets: ViewBehaviorTargets): ViewBehavior { - return this.factory.createBehavior(targets); + return (this.factory ?? this.mode.content!(this)).createBehavior(targets); } } diff --git a/packages/web-components/fast-element/src/templating/compiler.ts b/packages/web-components/fast-element/src/templating/compiler.ts index 6b21959740b..a07dc81e750 100644 --- a/packages/web-components/fast-element/src/templating/compiler.ts +++ b/packages/web-components/fast-element/src/templating/compiler.ts @@ -1,15 +1,8 @@ -import type { ViewBehaviorTargets } from "./html-directive"; +import type { InlinableHTMLDirective, ViewBehaviorTargets } from "./html-directive"; import { _interpolationEnd, _interpolationStart, DOM } from "../dom"; -import type { Binding, ExecutionContext } from "../observation/observable"; -import { bind, HTMLBindingDirective } from "./binding"; +import type { ExecutionContext } from "../observation/observable"; +import { bind, HTMLBindingDirective, oneTime } from "./binding"; import type { HTMLDirective, ViewBehaviorFactory } from "./html-directive"; -import { oneTime } from ".."; - -type InlineDirective = HTMLDirective & { - targetName?: string; - binding: Binding; - targetAtContent(): void; -}; const targetIdFrom = (parentId: string, nodeIndex: number) => `${parentId}.${nodeIndex}`; const descriptorCache: PropertyDescriptorMap = {}; @@ -70,16 +63,6 @@ class CompilationContext implements HTMLTemplateCompilationResult { this.factories.push(factory); } - public captureContentBinding( - directive: HTMLBindingDirective, - parentId: string, - targetId: string, - targetIndex: number - ): void { - directive.targetAtContent(); - this.addFactory(directive, parentId, targetId, targetIndex); - } - public freeze(): HTMLTemplateCompilationResult { this.proto = Object.create(null, this.descriptors); return this; @@ -134,21 +117,19 @@ class CompilationContext implements HTMLTemplateCompilationResult { } } -function createAggregateBinding( - parts: (string | InlineDirective)[] -): HTMLBindingDirective { +function createAggregateBinding(parts: (string | HTMLDirective)[]): HTMLDirective { if (parts.length === 1) { - return (parts[0] as any) as HTMLBindingDirective; + return parts[0] as HTMLDirective; } - let targetName: string | undefined; + let aspect: string | undefined; const partCount = parts.length; - const finalParts = parts.map((x: string | InlineDirective) => { + const finalParts = parts.map((x: string | InlinableHTMLDirective) => { if (typeof x === "string") { return (): string => x; } - targetName = x.targetName || targetName; + aspect = x.rawAspect || aspect; return x.binding; }); @@ -163,7 +144,7 @@ function createAggregateBinding( }; const directive = bind(binding) as HTMLBindingDirective; - directive.targetName = targetName; + directive.setAspect(aspect!); return directive; } @@ -172,7 +153,7 @@ const interpolationEndLength = _interpolationEnd.length; function parseContent( context: CompilationContext, value: string -): (string | InlineDirective)[] | null { +): (string | HTMLDirective)[] | null { const valueParts = value.split(_interpolationStart); if (valueParts.length === 1) { @@ -216,12 +197,12 @@ function compileAttributes( const attr = attributes[i]; const attrValue = attr.value; const parseResult = parseContent(context, attrValue); - let result: HTMLBindingDirective | null = null; + let result: HTMLDirective | null = null; if (parseResult === null) { if (includeBasicValues) { result = bind(() => attrValue, oneTime) as HTMLBindingDirective; - result.targetName = attr.name; + (result as HTMLBindingDirective).setAspect(attr.name); } } else { result = createAggregateBinding(parseResult); @@ -269,7 +250,7 @@ function compileContent( currentNode.textContent = currentPart; } else { currentNode.textContent = " "; - context.captureContentBinding( + context.addFactory( currentPart as HTMLBindingDirective, parentId, nodeId, 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 954a5094166..dc175fc3b14 100644 --- a/packages/web-components/fast-element/src/templating/html-directive.ts +++ b/packages/web-components/fast-element/src/templating/html-directive.ts @@ -1,6 +1,6 @@ import { DOM } from "../dom"; import type { Behavior } from "../observation/behavior"; -import type { ExecutionContext } from "../observation/observable"; +import type { Binding, ExecutionContext } from "../observation/observable"; /** * The target nodes available to a behavior. @@ -82,15 +82,12 @@ export abstract class HTMLDirective implements ViewBehaviorFactory { } /** - * A {@link HTMLDirective} that targets a named attribute or property on a node. + * A {@link HTMLDirective} that targets a particular aspect + * (attribute, property, event, etc.) of a node. * @public */ -export abstract class TargetedHTMLDirective extends HTMLDirective { - /** - * Gets/sets the name of the attribute or property that this - * directive is targeting on the associated node. - */ - public abstract targetName: string | undefined; +export abstract class AspectedHTMLDirective extends HTMLDirective { + abstract setAspect(value: string): void; /** * Creates a placeholder string based on the directive's index within the template. @@ -100,6 +97,11 @@ export abstract class TargetedHTMLDirective extends HTMLDirective { DOM.createInterpolationPlaceholder; } +export abstract class InlinableHTMLDirective extends AspectedHTMLDirective { + abstract binding: Binding; + abstract rawAspect?: string; +} + /** @internal */ export abstract class StatelessAttachedAttributeDirective extends HTMLDirective implements ViewBehavior { diff --git a/packages/web-components/fast-element/src/templating/template.spec.ts b/packages/web-components/fast-element/src/templating/template.spec.ts index 6db413bc341..f436b932716 100644 --- a/packages/web-components/fast-element/src/templating/template.spec.ts +++ b/packages/web-components/fast-element/src/templating/template.spec.ts @@ -2,8 +2,8 @@ import { expect } from "chai"; import { html, ViewTemplate } from "./template"; import { DOM } from "../dom"; import { HTMLBindingDirective } from "./binding"; -import { HTMLDirective, TargetedHTMLDirective } from "./html-directive"; -import { bind, ViewBehaviorTargets } from ".."; +import { HTMLDirective, AspectedHTMLDirective } from "./html-directive"; +import { bind, Binding, InlinableHTMLDirective, ViewBehaviorTargets } from ".."; describe(`The html tag template helper`, () => { it(`transforms a string into a ViewTemplate.`, () => { @@ -223,7 +223,7 @@ describe(`The html tag template helper`, () => { expect(template.html).to.equal( `` ); - expect((template.directives[0] as HTMLBindingDirective).targetName).to.equal( + expect((template.directives[0] as HTMLBindingDirective).rawAspect).to.equal( ":someAttribute" ); }); @@ -235,14 +235,20 @@ describe(`The html tag template helper`, () => { expect(template.html).to.equal( `` ); - expect((template.directives[0] as TargetedHTMLDirective).targetName).to.equal( + expect((template.directives[0] as HTMLBindingDirective).rawAspect).to.equal( ":someAttribute" ); }); - it(`captures a case-sensitive property name when used with a named target directive`, () => { - class TestDirective extends TargetedHTMLDirective { - targetName: string | undefined; + it(`captures a case-sensitive property name when used with an inline directive`, () => { + class TestDirective extends InlinableHTMLDirective { + binding: Binding; + rawAspect: string; + + setAspect(value) { + this.rawAspect = value; + } + createBehavior(targets: ViewBehaviorTargets) { return { bind() {}, unbind() {} }; } @@ -254,7 +260,7 @@ describe(`The html tag template helper`, () => { expect(template.html).to.equal( `` ); - expect((template.directives[0] as TargetedHTMLDirective).targetName).to.equal( + expect((template.directives[0] as TestDirective).rawAspect).to.equal( ":someAttribute" ); }); diff --git a/packages/web-components/fast-element/src/templating/template.ts b/packages/web-components/fast-element/src/templating/template.ts index 0d706e7bab7..fab81e91a91 100644 --- a/packages/web-components/fast-element/src/templating/template.ts +++ b/packages/web-components/fast-element/src/templating/template.ts @@ -3,7 +3,7 @@ import { Binding, defaultExecutionContext } from "../observation/observable"; import { compileTemplate } from "./compiler"; import type { HTMLTemplateCompilationResult } from "./compiler"; import { ElementView, HTMLView, SyntheticView } from "./view"; -import { HTMLDirective, TargetedHTMLDirective } from "./html-directive"; +import { HTMLDirective, AspectedHTMLDirective } from "./html-directive"; import { bind, oneTime } from "./binding"; /** @@ -183,10 +183,10 @@ export function html( } if (currentValue instanceof HTMLDirective) { - if (currentValue instanceof TargetedHTMLDirective) { + if (currentValue instanceof AspectedHTMLDirective) { const match = lastAttributeNameRegex.exec(currentString); if (match !== null) { - currentValue.targetName = match[2]; + currentValue.setAspect(match[2]); } } From 1568401bf04f602a7b441746a74121a1a9d9d674 Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Sat, 23 Oct 2021 16:01:07 -0400 Subject: [PATCH 036/135] refactor: rename Notifier#source to Notifier#subject --- .../fast-element/docs/api-report.md | 12 ++--- .../docs/fast-element-2-changes.md | 3 +- .../src/observation/array-observer.ts | 12 ++--- .../fast-element/src/observation/notifier.ts | 50 +++++++++---------- 4 files changed, 39 insertions(+), 38 deletions(-) diff --git a/packages/web-components/fast-element/docs/api-report.md b/packages/web-components/fast-element/docs/api-report.md index ad0736e38f6..43e56f3a029 100644 --- a/packages/web-components/fast-element/docs/api-report.md +++ b/packages/web-components/fast-element/docs/api-report.md @@ -391,7 +391,7 @@ export interface NodeBehaviorOptions { // @public export interface Notifier { notify(args: any): void; - readonly source: any; + readonly subject: any; subscribe(subscriber: Subscriber, propertyToWatch?: any): void; unsubscribe(subscriber: Subscriber, propertyToUnwatch?: any): void; } @@ -439,9 +439,9 @@ export interface PartialFASTElementDefinition { // @public export class PropertyChangeNotifier implements Notifier { - constructor(source: any); + constructor(subject: any); notify(propertyName: string): void; - readonly source: any; + readonly subject: any; subscribe(subscriber: Subscriber, propertyToWatch?: string): void; unsubscribe(subscriber: Subscriber, propertyToUnwatch?: string): void; } @@ -535,15 +535,15 @@ export interface StyleTarget { // @public export interface Subscriber { - handleChange(source: any, args: any): void; + handleChange(subject: any, args: any): void; } // @public export class SubscriberSet implements Notifier { - constructor(source: any, initialSubscriber?: Subscriber); + constructor(subject: any, initialSubscriber?: Subscriber); has(subscriber: Subscriber): boolean; notify(args: any): void; - readonly source: any; + readonly subject: any; subscribe(subscriber: Subscriber): void; unsubscribe(subscriber: Subscriber): void; } diff --git a/packages/web-components/fast-element/docs/fast-element-2-changes.md b/packages/web-components/fast-element/docs/fast-element-2-changes.md index ef65794c438..c78b27acbdb 100644 --- a/packages/web-components/fast-element/docs/fast-element-2-changes.md +++ b/packages/web-components/fast-element/docs/fast-element-2-changes.md @@ -11,4 +11,5 @@ * `Behavior` and `ViewBehavior` - `Behavior` now requires an `ExecutionContext` for `unbind`. Behaviors can be used for elements or views. `ViewBehavior` has been introduced for use exclusively with views, and provides some optimization opportunities. * `RefBehavior` has been replaced with `RefDirective`. The directive also implements `ViewBehavior` allowing a single directive instance to be shared across all template instances that use the ref. * Removed `SlottedBehavior` and `ChildrenBehavior` have been replaced with `SlottedDirective` and `ChildrenDirective`. These directives allow a single directive instance to be shared across all template instances that use the ref. -* Removed `AttachedBehaviorHTMLDirective` and `AttachedBehaviorType` since they are no longer used in the new directive/behavior architecture for ref, slotted, and children. \ No newline at end of file +* Removed `AttachedBehaviorHTMLDirective` and `AttachedBehaviorType` since they are no longer used in the new directive/behavior architecture for ref, slotted, and children. +* Renamed `Notifier#source` to `Notifier#subject` to align with other observable terminology and prevent name clashes with `BindingObserver` properties. \ No newline at end of file diff --git a/packages/web-components/fast-element/src/observation/array-observer.ts b/packages/web-components/fast-element/src/observation/array-observer.ts index eea12acf01d..33af938c546 100644 --- a/packages/web-components/fast-element/src/observation/array-observer.ts +++ b/packages/web-components/fast-element/src/observation/array-observer.ts @@ -30,9 +30,9 @@ class ArrayObserver extends SubscriberSet { private needsQueue: boolean = true; call: () => void = this.flush; - constructor(source: any[]) { - super(source); - (source as any).$fastController = this; + constructor(subject: any[]) { + super(subject); + (subject as any).$fastController = this; } public subscribe(subscriber: Subscriber): void { @@ -70,12 +70,12 @@ class ArrayObserver extends SubscriberSet { const finalSplices = oldCollection === void 0 ? splices!.length > 1 - ? projectArraySplices(this.source, splices!) + ? projectArraySplices(this.subject, splices!) : splices : calcSplices( - this.source, + this.subject, 0, - this.source.length, + this.subject.length, oldCollection, 0, oldCollection.length diff --git a/packages/web-components/fast-element/src/observation/notifier.ts b/packages/web-components/fast-element/src/observation/notifier.ts index 62912188caa..c885ff918ce 100644 --- a/packages/web-components/fast-element/src/observation/notifier.ts +++ b/packages/web-components/fast-element/src/observation/notifier.ts @@ -4,22 +4,22 @@ */ export interface Subscriber { /** - * Called when a source this instance has subscribed to changes. - * @param source - The source of the change. + * Called when a subject this instance has subscribed to changes. + * @param subject - The subject of the change. * @param args - The event args detailing the change that occurred. */ - handleChange(source: any, args: any): void; + handleChange(subject: any, args: any): void; } /** - * Provides change notification for a source object. + * Provides change notifications for an observed subject. * @public */ export interface Notifier { /** - * The source object that this notifier provides change notification for. + * The object that subscribers will receive notifications for. */ - readonly source: any; + readonly subject: any; /** * Notifies all subscribers, based on the args. @@ -52,7 +52,7 @@ export interface Notifier { /** * An implementation of {@link Notifier} that efficiently keeps track of * subscribers interested in a specific change notification on an - * observable source. + * observable subject. * * @remarks * This set is optimized for the most common scenario of 1 or 2 subscribers. @@ -66,17 +66,17 @@ export class SubscriberSet implements Notifier { private spillover: Subscriber[] | undefined = void 0; /** - * The source that this subscriber set is reporting changes for. + * The object that subscribers will receive notifications for. */ - public readonly source: any; + public readonly subject: any; /** - * Creates an instance of SubscriberSet for the specified source. - * @param source - The object source that subscribers will receive notifications from. + * Creates an instance of SubscriberSet for the specified subject. + * @param subject - The subject that subscribers will receive notifications from. * @param initialSubscriber - An initial subscriber to changes. */ - public constructor(source: any, initialSubscriber?: Subscriber) { - this.source = source; + public constructor(subject: any, initialSubscriber?: Subscriber) { + this.subject = subject; this.sub1 = initialSubscriber; } @@ -178,19 +178,19 @@ export class SubscriberSet implements Notifier { */ export class PropertyChangeNotifier implements Notifier { private subscribers: Record = {}; - private sourceSubscribers: SubscriberSet | null = null; + private subjectSubscribers: SubscriberSet | null = null; /** - * The source that property changes are being notified for. + * The subject that property changes are being notified for. */ - public readonly source: any; + public readonly subject: any; /** - * Creates an instance of PropertyChangeNotifier for the specified source. - * @param source - The object source that subscribers will receive notifications from. + * Creates an instance of PropertyChangeNotifier for the specified subject. + * @param subject - The object that subscribers will receive notifications for. */ - public constructor(source: any) { - this.source = source; + public constructor(subject: any) { + this.subject = subject; } /** @@ -199,7 +199,7 @@ export class PropertyChangeNotifier implements Notifier { */ public notify(propertyName: string): void { this.subscribers[propertyName]?.notify(propertyName); - this.sourceSubscribers?.notify(propertyName); + this.subjectSubscribers?.notify(propertyName); } /** @@ -213,11 +213,11 @@ export class PropertyChangeNotifier implements Notifier { if (propertyToWatch) { subscribers = this.subscribers[propertyToWatch] ?? - (this.subscribers[propertyToWatch] = new SubscriberSet(this.source)); + (this.subscribers[propertyToWatch] = new SubscriberSet(this.subject)); } else { subscribers = - this.sourceSubscribers ?? - (this.sourceSubscribers = new SubscriberSet(this.source)); + this.subjectSubscribers ?? + (this.subjectSubscribers = new SubscriberSet(this.subject)); } subscribers.subscribe(subscriber); @@ -232,7 +232,7 @@ export class PropertyChangeNotifier implements Notifier { if (propertyToUnwatch) { this.subscribers[propertyToUnwatch]?.unsubscribe(subscriber); } else { - this.sourceSubscribers?.unsubscribe(subscriber); + this.subjectSubscribers?.unsubscribe(subscriber); } } } From bddaad5b15814370f56bd296994d3f8df4b9d3a4 Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Sat, 23 Oct 2021 23:05:52 -0400 Subject: [PATCH 037/135] refactor: reduce duplication --- .../fast-element/docs/api-report.md | 1 + .../fast-element/src/templating/binding.ts | 75 ++++++++++--------- .../src/templating/html-directive.ts | 13 +++- 3 files changed, 52 insertions(+), 37 deletions(-) diff --git a/packages/web-components/fast-element/docs/api-report.md b/packages/web-components/fast-element/docs/api-report.md index 43e56f3a029..9b506b25477 100644 --- a/packages/web-components/fast-element/docs/api-report.md +++ b/packages/web-components/fast-element/docs/api-report.md @@ -342,6 +342,7 @@ export abstract class HTMLDirective implements ViewBehaviorFactory { abstract createBehavior(targets: ViewBehaviorTargets): Behavior | ViewBehavior; abstract createPlaceholder(index: number): string; targetId: string; + uniqueId: string; } // @public diff --git a/packages/web-components/fast-element/src/templating/binding.ts b/packages/web-components/fast-element/src/templating/binding.ts index 09e2fa2bae7..47e78e1ae4b 100644 --- a/packages/web-components/fast-element/src/templating/binding.ts +++ b/packages/web-components/fast-element/src/templating/binding.ts @@ -42,6 +42,7 @@ interface UpdateTargetThis { type UpdateTarget = ( this: UpdateTargetThis, target, + aspect: string, value, source: any, context: ExecutionContext @@ -70,10 +71,12 @@ class TargetUpdateBinding extends BindingBase { class OneTimeBinding extends TargetUpdateBinding { bind(source: any, context: ExecutionContext, targets: ViewBehaviorTargets): void { - const target = targets[this.directive.targetId]; + const directive = this.directive; + const target = targets[directive.targetId]; this.updateTarget( target, - this.directive.binding(source, context), + directive.aspect!, + directive.binding(source, context), source, context ); @@ -89,11 +92,12 @@ class OnChangeBinding extends TargetUpdateBinding { } bind(source: any, context: ExecutionContext, targets: ViewBehaviorTargets): void { - const target = targets[this.directive.targetId]; + const directive = this.directive; + const target = targets[directive.targetId]; const observer: BindingObserver = - target[this.directive.targetId] ?? - (target[this.directive.targetId] = Observable.binding( - this.directive.binding, + target[directive.uniqueId] ?? + (target[directive.uniqueId] = Observable.binding( + directive.binding, this, this.isBindingVolatile )); @@ -102,12 +106,18 @@ class OnChangeBinding extends TargetUpdateBinding { (observer as any).source = source; (observer as any).context = context; - this.updateTarget(target, observer.observe(source, context), source, context); + this.updateTarget( + target, + directive.aspect!, + observer.observe(source, context), + source, + context + ); } unbind(source: any, context: ExecutionContext, targets: ViewBehaviorTargets): void { const target = targets[this.directive.targetId]; - const observer = target[this.directive.targetId]; + const observer = target[this.directive.uniqueId]; observer.disconnect(); observer.target = null; observer.source = null; @@ -119,26 +129,16 @@ class OnChangeBinding extends TargetUpdateBinding { const target = (observer as any).target; const source = (observer as any).source; const context = (observer as any).context; - this.updateTarget(target, observer.observe(source, context!), source, context); + this.updateTarget( + target, + this.directive.aspect!, + observer.observe(source, context!), + source, + context + ); } } -function setPropertyTarget(this: UpdateTargetThis, target, value) { - target[this.directive.aspect!] = value; -} - -function setAttributeTarget(this: UpdateTargetThis, target, value) { - DOM.setAttribute(target as HTMLElement, this.directive.aspect!, value); -} - -function setBooleanAttributeTarget(this: UpdateTargetThis, target, value) { - DOM.setBooleanAttribute( - target as HTMLElement, - this.directive.aspect!, - value as boolean - ); -} - interface UpdateTokenListThis extends UpdateTargetThis { classVersions: any; version: number; @@ -147,10 +147,11 @@ interface UpdateTokenListThis extends UpdateTargetThis { function updateTokenListTarget( this: UpdateTokenListThis, target: Element, + aspect: string, value: any ): void { const classVersions = this.classVersions; - const tokenList = target[this.directive.aspect!] as DOMTokenList; + const tokenList = target[aspect] as DOMTokenList; let version = this.version; // Add the classes, tracking the version at which they were added. @@ -206,6 +207,7 @@ type ContentTarget = Node & { function updateContentTarget( target: ContentTarget, + aspect: string, value: any, source: any, context: ExecutionContext @@ -295,10 +297,11 @@ type FASTEventSource = Node & { class EventListener extends BindingBase { bind(source: any, context: ExecutionContext, targets: ViewBehaviorTargets): void { - const target = targets[this.directive.targetId] as FASTEventSource; + const directive = this.directive; + const target = targets[directive.targetId] as FASTEventSource; target.$fastSource = source; target.$fastContext = context; - target.addEventListener(this.directive.aspect!, this, this.directive.options); + target.addEventListener(directive.aspect!, this, directive.options); } unbind(source: any, context: ExecutionContext, targets: ViewBehaviorTargets): void { @@ -342,8 +345,8 @@ const defaultBindingOptions: DefaultBindingOptions = { }; function createBindingConfig( - BaseType: typeof TargetUpdateBinding, - EventListener: Constructable + Base: typeof TargetUpdateBinding, + Listener: Constructable ) { const config: BindingConfig & ((options?: DefaultBindingOptions) => BindingConfig) = ( options: DefaultBindingOptions @@ -356,12 +359,12 @@ function createBindingConfig( config.options = defaultBindingOptions; config.mode = Object.freeze({ - attribute: BaseType.createType(setAttributeTarget), - booleanAttribute: BaseType.createType(setBooleanAttributeTarget), - property: BaseType.createType(setPropertyTarget), - content: createContentBinding(BaseType).createType(updateContentTarget), - tokenList: createTokenListBinding(BaseType).createType(updateTokenListTarget), - event: directive => new EventListener(directive), + attribute: Base.createType(DOM.setAttribute), + booleanAttribute: Base.createType(DOM.setBooleanAttribute), + property: Base.createType((target, aspect, value) => (target[aspect] = value)), + content: createContentBinding(Base).createType(updateContentTarget), + tokenList: createTokenListBinding(Base).createType(updateTokenListTarget), + event: directive => new Listener(directive), }); return config; 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 dc175fc3b14..d321e5dad8a 100644 --- a/packages/web-components/fast-element/src/templating/html-directive.ts +++ b/packages/web-components/fast-element/src/templating/html-directive.ts @@ -58,16 +58,27 @@ export interface ViewBehaviorFactory { createBehavior(targets: ViewBehaviorTargets): Behavior | ViewBehavior; } +let directiveId = 0; +function nextId() { + return `fast-${++directiveId}`; +} + /** * Instructs the template engine to apply behavior to a node. * @public */ export abstract class HTMLDirective implements ViewBehaviorFactory { /** - * The structural id of the DOM node to which the created behavior will apply. + * The structural id of the directive based on the DOM node + * that it applies to. */ public targetId: string = "h"; + /** + * The unique id of the directive instance. + */ + public uniqueId: string = nextId(); + /** * Creates a placeholder string based on the directive's index within the template. * @param index - The index of the directive within the template. From db654ab09fa9f521bac2f6563bcc9e604860b343 Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Sat, 23 Oct 2021 23:16:23 -0400 Subject: [PATCH 038/135] fix: state issue with token list bindings --- .../fast-element/src/templating/binding.ts | 38 +++++++++---------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/packages/web-components/fast-element/src/templating/binding.ts b/packages/web-components/fast-element/src/templating/binding.ts index 47e78e1ae4b..27309364b74 100644 --- a/packages/web-components/fast-element/src/templating/binding.ts +++ b/packages/web-components/fast-element/src/templating/binding.ts @@ -139,20 +139,24 @@ class OnChangeBinding extends TargetUpdateBinding { } } -interface UpdateTokenListThis extends UpdateTargetThis { - classVersions: any; - version: number; +interface TokenListState { + v: {}; + c: number; } function updateTokenListTarget( - this: UpdateTokenListThis, + this: UpdateTargetThis, target: Element, aspect: string, value: any ): void { - const classVersions = this.classVersions; + const directive = this.directive; + const state: TokenListState = + target[directive.uniqueId] ?? + (target[directive.uniqueId] = { c: 0, v: Object.create(null) }); + const versions = state.v; + let currentVersion = state.c; const tokenList = target[aspect] as DOMTokenList; - let version = this.version; // Add the classes, tracking the version at which they were added. if (value !== null && value !== undefined && value.length) { @@ -165,36 +169,28 @@ function updateTokenListTarget( continue; } - classVersions[currentName] = version; + versions[currentName] = currentVersion; tokenList.add(currentName); } } - this.classVersions = classVersions; - this.version = version + 1; + state.v = currentVersion + 1; // If this is the first call to add classes, there's no need to remove old ones. - if (version === 0) { + if (currentVersion === 0) { return; } // Remove classes from the previous version. - version -= 1; + currentVersion -= 1; - for (const name in classVersions) { - if (classVersions[name] === version) { + for (const name in versions) { + if (versions[name] === currentVersion) { tokenList.remove(name); } } } -function createTokenListBinding(BaseType: typeof TargetUpdateBinding) { - return class TokenListBinding extends BaseType implements UpdateTokenListThis { - classVersions = Object.create(null); - version = 0; - }; -} - type ComposableView = SyntheticView & { isComposed?: boolean; needsBindOnly?: boolean; @@ -363,7 +359,7 @@ function createBindingConfig( booleanAttribute: Base.createType(DOM.setBooleanAttribute), property: Base.createType((target, aspect, value) => (target[aspect] = value)), content: createContentBinding(Base).createType(updateContentTarget), - tokenList: createTokenListBinding(Base).createType(updateTokenListTarget), + tokenList: Base.createType(updateTokenListTarget), event: directive => new Listener(directive), }); From b25775bb94a85378fc6773f840da5395909689ce Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Sun, 24 Oct 2021 14:11:42 -0400 Subject: [PATCH 039/135] refactor: clean up types and remove internals from public export --- .../fast-element/docs/api-report.md | 21 +------ .../web-components/fast-element/src/index.ts | 61 +++++++++++-------- .../fast-element/src/templating/binding.ts | 3 + .../fast-element/src/templating/compiler.ts | 24 ++++---- 4 files changed, 52 insertions(+), 57 deletions(-) diff --git a/packages/web-components/fast-element/docs/api-report.md b/packages/web-components/fast-element/docs/api-report.md index 9b506b25477..70cfec1bb69 100644 --- a/packages/web-components/fast-element/docs/api-report.md +++ b/packages/web-components/fast-element/docs/api-report.md @@ -102,6 +102,8 @@ export interface BindingObserver ex records(): IterableIterator; } +// Warning: (ae-forgotten-export) The symbol "HTMLBindingDirective" needs to be exported by the entry point index.d.ts +// // @public (undocumented) export type BindingType = (directive: HTMLBindingDirective) => BindingBehaviorFactory; @@ -318,25 +320,6 @@ export type Global = typeof globalThis & { // @public export function html(strings: TemplateStringsArray, ...values: TemplateValue[]): ViewTemplate; -// @public (undocumented) -export class HTMLBindingDirective extends InlinableHTMLDirective { - constructor(binding: Binding, mode: BindingMode, options: any); - // (undocumented) - readonly aspect?: string; - // (undocumented) - binding: Binding; - // (undocumented) - createBehavior(targets: ViewBehaviorTargets): ViewBehavior; - // (undocumented) - mode: BindingMode; - // (undocumented) - options: any; - // (undocumented) - readonly rawAspect?: string; - // (undocumented) - setAspect(value: string): void; -} - // @public export abstract class HTMLDirective implements ViewBehaviorFactory { abstract createBehavior(targets: ViewBehaviorTargets): Behavior | ViewBehavior; diff --git a/packages/web-components/fast-element/src/index.ts b/packages/web-components/fast-element/src/index.ts index c0e9a072962..dfaf540b099 100644 --- a/packages/web-components/fast-element/src/index.ts +++ b/packages/web-components/fast-element/src/index.ts @@ -1,38 +1,47 @@ -export * from "./platform.js"; -export * from "./templating/template.js"; -export * from "./components/fast-element.js"; +export * from "./platform"; +export * from "./templating/template"; +export * from "./components/fast-element"; export { FASTElementDefinition, PartialFASTElementDefinition, -} from "./components/fast-definitions.js"; -export * from "./components/attributes.js"; -export * from "./components/controller.js"; -export type { Callable, Constructable, Mutable } from "./interfaces.js"; -export * from "./templating/compiler.js"; +} from "./components/fast-definitions"; +export * from "./components/attributes"; +export * from "./components/controller"; +export type { Callable, Constructable, Mutable } from "./interfaces"; +export * from "./templating/compiler"; export { ElementStyles, ElementStyleFactory, ComposableStyles, StyleTarget, -} from "./styles/element-styles.js"; -export { css, cssPartial } from "./styles/css.js"; -export { CSSDirective } from "./styles/css-directive.js"; -export * from "./templating/view.js"; -export * from "./observation/observable.js"; -export * from "./observation/notifier.js"; -export { Splice } from "./observation/array-change-records.js"; -export { enableArrayObservation } from "./observation/array-observer.js"; -export { DOM } from "./dom.js"; -export type { Behavior } from "./observation/behavior.js"; -export * from "./templating/binding.js"; -export * from "./templating/html-directive.js"; -export * from "./templating/ref.js"; -export * from "./templating/when.js"; -export * from "./templating/repeat.js"; -export * from "./templating/slotted.js"; -export * from "./templating/children.js"; +} from "./styles/element-styles"; +export { css, cssPartial } from "./styles/css"; +export { CSSDirective } from "./styles/css-directive"; +export * from "./templating/view"; +export * from "./observation/observable"; +export * from "./observation/notifier"; +export { Splice } from "./observation/array-change-records"; +export { enableArrayObservation } from "./observation/array-observer"; +export { DOM } from "./dom"; +export type { Behavior } from "./observation/behavior"; +export { + bind, + oneTime, + onChange, + BindingConfig, + BindingMode, + BindingType, + BindingBehaviorFactory, + DefaultBindingOptions, +} from "./templating/binding"; +export * from "./templating/html-directive"; +export * from "./templating/ref"; +export * from "./templating/when"; +export * from "./templating/repeat"; +export * from "./templating/slotted"; +export * from "./templating/children"; export { elements, ElementsFilter, NodeBehaviorOptions, -} from "./templating/node-observation.js"; +} from "./templating/node-observation"; diff --git a/packages/web-components/fast-element/src/templating/binding.ts b/packages/web-components/fast-element/src/templating/binding.ts index 27309364b74..72cfffb71aa 100644 --- a/packages/web-components/fast-element/src/templating/binding.ts +++ b/packages/web-components/fast-element/src/templating/binding.ts @@ -369,6 +369,9 @@ function createBindingConfig( export const onChange = createBindingConfig(OnChangeBinding, EventListener); export const oneTime = createBindingConfig(OneTimeBinding, OneTimeEventListener); +/** + * @internal + */ export class HTMLBindingDirective extends InlinableHTMLDirective { private factory!: BindingBehaviorFactory; diff --git a/packages/web-components/fast-element/src/templating/compiler.ts b/packages/web-components/fast-element/src/templating/compiler.ts index a07dc81e750..e2fc9263df1 100644 --- a/packages/web-components/fast-element/src/templating/compiler.ts +++ b/packages/web-components/fast-element/src/templating/compiler.ts @@ -1,8 +1,13 @@ -import type { InlinableHTMLDirective, ViewBehaviorTargets } from "./html-directive"; +import type { + AspectedHTMLDirective, + HTMLDirective, + InlinableHTMLDirective, + ViewBehaviorFactory, + ViewBehaviorTargets, +} from "./html-directive"; import { _interpolationEnd, _interpolationStart, DOM } from "../dom"; import type { ExecutionContext } from "../observation/observable"; -import { bind, HTMLBindingDirective, oneTime } from "./binding"; -import type { HTMLDirective, ViewBehaviorFactory } from "./html-directive"; +import { bind, oneTime } from "./binding"; const targetIdFrom = (parentId: string, nodeIndex: number) => `${parentId}.${nodeIndex}`; const descriptorCache: PropertyDescriptorMap = {}; @@ -143,7 +148,7 @@ function createAggregateBinding(parts: (string | HTMLDirective)[]): HTMLDirectiv return output; }; - const directive = bind(binding) as HTMLBindingDirective; + const directive = bind(binding) as AspectedHTMLDirective; directive.setAspect(aspect!); return directive; } @@ -201,8 +206,8 @@ function compileAttributes( if (parseResult === null) { if (includeBasicValues) { - result = bind(() => attrValue, oneTime) as HTMLBindingDirective; - (result as HTMLBindingDirective).setAspect(attr.name); + result = bind(() => attrValue, oneTime) as AspectedHTMLDirective; + (result as AspectedHTMLDirective).setAspect(attr.name); } } else { result = createAggregateBinding(parseResult); @@ -250,12 +255,7 @@ function compileContent( currentNode.textContent = currentPart; } else { currentNode.textContent = " "; - context.addFactory( - currentPart as HTMLBindingDirective, - parentId, - nodeId, - nodeIndex - ); + context.addFactory(currentPart, parentId, nodeId, nodeIndex); } lastNode = currentNode; From 35f1cc22f0a8e2789b13efed7449f91f0ec14ecc Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Sun, 24 Oct 2021 20:32:14 -0400 Subject: [PATCH 040/135] refactor: centralize common type checks --- .../fast-element/src/components/attributes.ts | 9 +++++---- .../fast-element/src/components/fast-definitions.ts | 12 ++++++------ .../web-components/fast-element/src/interfaces.ts | 11 +++++++++++ .../fast-element/src/observation/observable.ts | 5 +++-- .../web-components/fast-element/src/styles/css.ts | 11 ++++++----- .../fast-element/src/templating/children.ts | 3 ++- .../fast-element/src/templating/compiler.ts | 5 +++-- .../fast-element/src/templating/repeat.ts | 8 ++++---- .../fast-element/src/templating/slotted.ts | 3 ++- .../fast-element/src/templating/template.ts | 9 ++++----- .../fast-element/src/templating/when.ts | 12 ++++++------ 11 files changed, 52 insertions(+), 36 deletions(-) diff --git a/packages/web-components/fast-element/src/components/attributes.ts b/packages/web-components/fast-element/src/components/attributes.ts index 21b8fffa39f..b9e0fb24510 100644 --- a/packages/web-components/fast-element/src/components/attributes.ts +++ b/packages/web-components/fast-element/src/components/attributes.ts @@ -1,6 +1,7 @@ -import { Accessor, Observable } from "../observation/observable.js"; -import { DOM } from "../dom.js"; -import type { Notifier } from "../observation/notifier.js"; +import { Accessor, Observable } from "../observation/observable"; +import { DOM } from "../dom"; +import type { Notifier } from "../observation/notifier"; +import { isString } from "../interfaces"; /** * Represents objects that can convert values to and from @@ -271,7 +272,7 @@ export class AttributeDefinition implements Accessor { for (let j = 0, jj = list.length; j < jj; ++j) { const config = list[j]; - if (typeof config === "string") { + if (isString(config)) { attributes.push(new AttributeDefinition(Owner, config)); } else { attributes.push( diff --git a/packages/web-components/fast-element/src/components/fast-definitions.ts b/packages/web-components/fast-element/src/components/fast-definitions.ts index 4de8fc4f80e..9c698fbfb87 100644 --- a/packages/web-components/fast-element/src/components/fast-definitions.ts +++ b/packages/web-components/fast-element/src/components/fast-definitions.ts @@ -1,8 +1,8 @@ -import { FAST, KernelServiceId } from "../platform.js"; -import { Observable } from "../observation/observable.js"; -import { ComposableStyles, ElementStyles } from "../styles/element-styles.js"; -import type { ElementViewTemplate } from "../templating/template.js"; -import { AttributeConfiguration, AttributeDefinition } from "./attributes.js"; +import { isString, Mutable } from "../interfaces"; +import { Observable } from "../observation/observable"; +import { ComposableStyles, ElementStyles } from "../styles/element-styles"; +import type { ElementViewTemplate } from "../templating/template"; +import { AttributeConfiguration, AttributeDefinition } from "./attributes"; const defaultShadowOptions: ShadowRootInit = { mode: "open" }; const defaultElementOptions: ElementDefinitionOptions = {}; @@ -129,7 +129,7 @@ export class FASTElementDefinition { type: TType, nameOrConfig: PartialFASTElementDefinition | string = (type as any).definition ) { - if (typeof nameOrConfig === "string") { + if (isString(nameOrConfig)) { nameOrConfig = { name: nameOrConfig }; } diff --git a/packages/web-components/fast-element/src/interfaces.ts b/packages/web-components/fast-element/src/interfaces.ts index 4e842abc9aa..4a7991af745 100644 --- a/packages/web-components/fast-element/src/interfaces.ts +++ b/packages/web-components/fast-element/src/interfaces.ts @@ -20,3 +20,14 @@ export type Constructable = { export type Mutable = { -readonly [P in keyof T]: T[P]; }; + +/** + * @internal + */ +export const isFunction = (object: any): object is Function => + typeof object === "function"; + +/** + * @internal + */ +export const isString = (object: any): object is string => typeof object === "string"; diff --git a/packages/web-components/fast-element/src/observation/observable.ts b/packages/web-components/fast-element/src/observation/observable.ts index bea3a8016bf..c243f78262b 100644 --- a/packages/web-components/fast-element/src/observation/observable.ts +++ b/packages/web-components/fast-element/src/observation/observable.ts @@ -1,6 +1,7 @@ import { DOM } from "../dom"; import { PropertyChangeNotifier, SubscriberSet } from "./notifier"; import type { Notifier, Subscriber } from "./notifier"; +import { isFunction, isString } from "../interfaces"; const volatileRegex = /(:|&&|\|\||if)/; const notifierLookup = new WeakMap(); @@ -60,7 +61,7 @@ class DefaultObservableAccessor implements Accessor { const callback = source[this.callback]; - if (typeof callback === "function") { + if (isFunction(callback)) { callback.call(source, oldValue, newValue); } @@ -136,7 +137,7 @@ export const Observable = Object.freeze({ * or a custom accessor that specifies the property name and accessor implementation. */ defineProperty(target: {}, nameOrAccessor: string | Accessor): void { - if (typeof nameOrAccessor === "string") { + if (isString(nameOrAccessor)) { nameOrAccessor = new DefaultObservableAccessor(nameOrAccessor); } diff --git a/packages/web-components/fast-element/src/styles/css.ts b/packages/web-components/fast-element/src/styles/css.ts index da413894150..dc94edfb349 100644 --- a/packages/web-components/fast-element/src/styles/css.ts +++ b/packages/web-components/fast-element/src/styles/css.ts @@ -1,7 +1,8 @@ -import type { FASTElement } from "../components/fast-element.js"; -import type { Behavior } from "../observation/behavior.js"; -import { CSSDirective } from "./css-directive.js"; -import { ComposableStyles, ElementStyles } from "./element-styles.js"; +import type { FASTElement } from "../components/fast-element"; +import { isString } from "../interfaces"; +import type { Behavior } from "../observation/behavior"; +import { CSSDirective } from "./css-directive"; +import { ComposableStyles, ElementStyles } from "./element-styles"; function collectStyles( strings: TemplateStringsArray, @@ -79,7 +80,7 @@ class CSSPartial extends CSSDirective implements Behavior { accumulated: Exclude[], current: ComposableStyles ) => { - if (typeof current === "string") { + if (isString(current)) { this.css += current; } else { accumulated.push(current); diff --git a/packages/web-components/fast-element/src/templating/children.ts b/packages/web-components/fast-element/src/templating/children.ts index 437f8097cfa..0d9b24722ee 100644 --- a/packages/web-components/fast-element/src/templating/children.ts +++ b/packages/web-components/fast-element/src/templating/children.ts @@ -1,3 +1,4 @@ +import { isString } from "../interfaces"; import { NodeObservationDirective, NodeBehaviorOptions } from "./node-observation"; import type { CaptureType } from "./template"; @@ -104,7 +105,7 @@ export class ChildrenDirective extends NodeObservationDirective< export function children( propertyOrOptions: (keyof T & string) | ChildListDirectiveOptions ): CaptureType { - if (typeof propertyOrOptions === "string") { + if (isString(propertyOrOptions)) { propertyOrOptions = { property: propertyOrOptions, }; diff --git a/packages/web-components/fast-element/src/templating/compiler.ts b/packages/web-components/fast-element/src/templating/compiler.ts index e2fc9263df1..8fe9274aa2f 100644 --- a/packages/web-components/fast-element/src/templating/compiler.ts +++ b/packages/web-components/fast-element/src/templating/compiler.ts @@ -8,6 +8,7 @@ import type { import { _interpolationEnd, _interpolationStart, DOM } from "../dom"; import type { ExecutionContext } from "../observation/observable"; import { bind, oneTime } from "./binding"; +import { isString } from "../interfaces"; const targetIdFrom = (parentId: string, nodeIndex: number) => `${parentId}.${nodeIndex}`; const descriptorCache: PropertyDescriptorMap = {}; @@ -130,7 +131,7 @@ function createAggregateBinding(parts: (string | HTMLDirective)[]): HTMLDirectiv let aspect: string | undefined; const partCount = parts.length; const finalParts = parts.map((x: string | InlinableHTMLDirective) => { - if (typeof x === "string") { + if (isString(x)) { return (): string => x; } @@ -251,7 +252,7 @@ function compileContent( ); } - if (typeof currentPart === "string") { + if (isString(currentPart)) { currentNode.textContent = currentPart; } else { currentNode.textContent = " "; diff --git a/packages/web-components/fast-element/src/templating/repeat.ts b/packages/web-components/fast-element/src/templating/repeat.ts index ce9193fe98c..c1748ce96f2 100644 --- a/packages/web-components/fast-element/src/templating/repeat.ts +++ b/packages/web-components/fast-element/src/templating/repeat.ts @@ -13,6 +13,7 @@ import { emptyArray } from "../platform"; import { ViewBehaviorTargets, HTMLDirective } from "./html-directive"; import { HTMLView, SyntheticView } from "./view"; import type { CaptureType, SyntheticViewTemplate } from "./template"; +import { isFunction } from "../interfaces"; /** * Options for configuring repeat behavior. @@ -356,10 +357,9 @@ export function repeat( | Binding, options: RepeatOptions = defaultRepeatOptions ): CaptureType { - const templateBinding = - typeof templateOrTemplateBinding === "function" - ? templateOrTemplateBinding - : (): SyntheticViewTemplate => templateOrTemplateBinding; + const templateBinding = isFunction(templateOrTemplateBinding) + ? templateOrTemplateBinding + : (): SyntheticViewTemplate => templateOrTemplateBinding; return new RepeatDirective(itemsBinding, templateBinding, options); } diff --git a/packages/web-components/fast-element/src/templating/slotted.ts b/packages/web-components/fast-element/src/templating/slotted.ts index a7feff6c71a..c65e4491c5d 100644 --- a/packages/web-components/fast-element/src/templating/slotted.ts +++ b/packages/web-components/fast-element/src/templating/slotted.ts @@ -1,3 +1,4 @@ +import { isString } from "../interfaces"; import { NodeObservationDirective, NodeBehaviorOptions } from "./node-observation"; import type { CaptureType } from "./template"; @@ -55,7 +56,7 @@ export class SlottedDirective extends NodeObservationDirective( propertyOrOptions: (keyof T & string) | SlottedDirectiveOptions ): CaptureType { - if (typeof propertyOrOptions === "string") { + if (isString(propertyOrOptions)) { propertyOrOptions = { property: propertyOrOptions }; } diff --git a/packages/web-components/fast-element/src/templating/template.ts b/packages/web-components/fast-element/src/templating/template.ts index fab81e91a91..97ab51fefad 100644 --- a/packages/web-components/fast-element/src/templating/template.ts +++ b/packages/web-components/fast-element/src/templating/template.ts @@ -5,6 +5,7 @@ import type { HTMLTemplateCompilationResult } from "./compiler"; import { ElementView, HTMLView, SyntheticView } from "./view"; import { HTMLDirective, AspectedHTMLDirective } from "./html-directive"; import { bind, oneTime } from "./binding"; +import { isFunction, isString } from "../interfaces"; /** * A template capable of creating views specifically for rendering custom elements. @@ -85,7 +86,7 @@ export class ViewTemplate let template: HTMLTemplateElement; const html = this.html; - if (typeof html === "string") { + if (isString(html)) { template = document.createElement("template"); template.innerHTML = DOM.createHTML(html); @@ -171,13 +172,11 @@ export function html( for (let i = 0, ii = strings.length - 1; i < ii; ++i) { const currentString = strings[i]; let currentValue = values[i]; - const valueType = typeof currentValue; - html += currentString; - if (valueType === "function") { + if (isFunction(currentValue)) { currentValue = bind(currentValue as Binding); - } else if (valueType !== "string" && !(currentValue instanceof HTMLDirective)) { + } else if (!isString(currentValue) && !(currentValue instanceof HTMLDirective)) { const capturedValue = currentValue; currentValue = bind(() => capturedValue, oneTime); } diff --git a/packages/web-components/fast-element/src/templating/when.ts b/packages/web-components/fast-element/src/templating/when.ts index dd22bfec978..b2a5cdca59a 100644 --- a/packages/web-components/fast-element/src/templating/when.ts +++ b/packages/web-components/fast-element/src/templating/when.ts @@ -1,5 +1,6 @@ -import type { Binding, ExecutionContext } from "../observation/observable.js"; -import type { CaptureType, SyntheticViewTemplate } from "./template.js"; +import { isFunction } from "../interfaces"; +import type { Binding, ExecutionContext } from "../observation/observable"; +import type { CaptureType, SyntheticViewTemplate } from "./template"; /** * A directive that enables basic conditional rendering in a template. @@ -14,10 +15,9 @@ export function when( | SyntheticViewTemplate | Binding ): CaptureType { - const getTemplate = - typeof templateOrTemplateBinding === "function" - ? templateOrTemplateBinding - : (): SyntheticViewTemplate => templateOrTemplateBinding; + const getTemplate = isFunction(templateOrTemplateBinding) + ? templateOrTemplateBinding + : (): SyntheticViewTemplate => templateOrTemplateBinding; return (source: TSource, context: ExecutionContext): SyntheticViewTemplate | null => binding(source, context) ? getTemplate(source, context) : null; From 2afec8ef1fc6c21c5c3308edff428de244f77c0b Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Sun, 24 Oct 2021 20:41:14 -0400 Subject: [PATCH 041/135] refactor: centralize id generation --- packages/web-components/fast-element/src/dom.ts | 7 +++++-- packages/web-components/fast-element/src/platform.ts | 2 +- .../fast-element/src/templating/html-directive.ts | 7 +------ 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/web-components/fast-element/src/dom.ts b/packages/web-components/fast-element/src/dom.ts index a47d2e20d30..a478147b26a 100644 --- a/packages/web-components/fast-element/src/dom.ts +++ b/packages/web-components/fast-element/src/dom.ts @@ -30,7 +30,10 @@ function tryRunTask(task: Callable): void { } const marker = `fast-${Math.random().toString(36).substring(2, 8)}`; -let attrId = 0; +let id = 0; + +/** @internal */ +export const nextId = () => `${marker}-${++id}`; /** @internal */ export const _interpolationStart = `${marker}{`; @@ -112,7 +115,7 @@ export const DOM = Object.freeze({ * Used internally by attribute directives such as `ref`, `slotted`, and `children`. */ createCustomAttributePlaceholder(index: number): string { - return `${marker}-${++attrId}="${this.createInterpolationPlaceholder(index)}"`; + return `${nextId()}="${this.createInterpolationPlaceholder(index)}"`; }, /** diff --git a/packages/web-components/fast-element/src/platform.ts b/packages/web-components/fast-element/src/platform.ts index 4b108898d72..83fbc6a45dc 100644 --- a/packages/web-components/fast-element/src/platform.ts +++ b/packages/web-components/fast-element/src/platform.ts @@ -100,7 +100,7 @@ export const $global: Global = (function () { })(); // API-only Polyfill for trustedTypes -if ($global.trustedTypes === void 0) { +if (!$global.trustedTypes) { $global.trustedTypes = { createPolicy: (n: string, r: TrustedTypesPolicy) => r }; } 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 d321e5dad8a..11e7cd7c827 100644 --- a/packages/web-components/fast-element/src/templating/html-directive.ts +++ b/packages/web-components/fast-element/src/templating/html-directive.ts @@ -1,4 +1,4 @@ -import { DOM } from "../dom"; +import { DOM, nextId } from "../dom"; import type { Behavior } from "../observation/behavior"; import type { Binding, ExecutionContext } from "../observation/observable"; @@ -58,11 +58,6 @@ export interface ViewBehaviorFactory { createBehavior(targets: ViewBehaviorTargets): Behavior | ViewBehavior; } -let directiveId = 0; -function nextId() { - return `fast-${++directiveId}`; -} - /** * Instructs the template engine to apply behavior to a node. * @public From 975d5db28e4383315b24363d5eab58d9d466ffdf Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Sun, 24 Oct 2021 20:48:22 -0400 Subject: [PATCH 042/135] refactor: only export intended types for directives --- .../fast-element/docs/api-report.md | 15 +-------------- packages/web-components/fast-element/src/index.ts | 9 ++++++++- 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/packages/web-components/fast-element/docs/api-report.md b/packages/web-components/fast-element/docs/api-report.md index 70cfec1bb69..a4309fed780 100644 --- a/packages/web-components/fast-element/docs/api-report.md +++ b/packages/web-components/fast-element/docs/api-report.md @@ -433,7 +433,7 @@ export class PropertyChangeNotifier implements Notifier { // @public export function ref(propertyName: keyof T & string): CaptureType; -// Warning: (ae-incompatible-release-tags) The symbol "RefDirective" is marked as @public, but its signature references "StatelessAttachedAttributeDirective" which is marked as @internal +// Warning: (ae-forgotten-export) The symbol "StatelessAttachedAttributeDirective" needs to be exported by the entry point index.d.ts // // @public export class RefDirective extends StatelessAttachedAttributeDirective { @@ -494,19 +494,6 @@ export interface Splice { removed: any[]; } -// Warning: (ae-internal-missing-underscore) The name "StatelessAttachedAttributeDirective" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal (undocumented) -export abstract class StatelessAttachedAttributeDirective extends HTMLDirective implements ViewBehavior { - constructor(options: T); - abstract bind(source: any, context: ExecutionContext, targets: ViewBehaviorTargets): void; - createBehavior(targets: ViewBehaviorTargets): ViewBehavior; - createPlaceholder(index: number): string; - // (undocumented) - protected options: T; - abstract unbind(source: any, context: ExecutionContext, targets: ViewBehaviorTargets): void; -} - // @public export interface StyleTarget { adoptedStyleSheets?: CSSStyleSheet[]; diff --git a/packages/web-components/fast-element/src/index.ts b/packages/web-components/fast-element/src/index.ts index dfaf540b099..e7ea35b8a32 100644 --- a/packages/web-components/fast-element/src/index.ts +++ b/packages/web-components/fast-element/src/index.ts @@ -34,7 +34,14 @@ export { BindingBehaviorFactory, DefaultBindingOptions, } from "./templating/binding"; -export * from "./templating/html-directive"; +export { + ViewBehaviorTargets, + ViewBehavior, + ViewBehaviorFactory, + HTMLDirective, + AspectedHTMLDirective, + InlinableHTMLDirective, +} from "./templating/html-directive"; export * from "./templating/ref"; export * from "./templating/when"; export * from "./templating/repeat"; From c7cf771cf584b3ee65a128182ef6c58950cfabcc Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Sun, 24 Oct 2021 23:14:47 -0400 Subject: [PATCH 043/135] refactor: internal code reduction --- .../web-components/fast-element/docs/api-report.md | 10 +++++----- .../fast-element/src/templating/children.ts | 8 +++----- .../fast-element/src/templating/html-directive.ts | 6 +++--- .../fast-element/src/templating/node-observation.ts | 5 ++--- .../web-components/fast-element/src/templating/ref.ts | 5 ++--- 5 files changed, 15 insertions(+), 19 deletions(-) diff --git a/packages/web-components/fast-element/docs/api-report.md b/packages/web-components/fast-element/docs/api-report.md index a4309fed780..c29fba09785 100644 --- a/packages/web-components/fast-element/docs/api-report.md +++ b/packages/web-components/fast-element/docs/api-report.md @@ -217,7 +217,7 @@ export const DOM: Readonly<{ }>; // @public -export function elements(selector?: string): ElementsFilter; +export const elements: (selector?: string | undefined) => ElementsFilter; // @public export type ElementsFilter = (value: Node, index: number, array: Node[]) => boolean; @@ -325,7 +325,7 @@ export abstract class HTMLDirective implements ViewBehaviorFactory { abstract createBehavior(targets: ViewBehaviorTargets): Behavior | ViewBehavior; abstract createPlaceholder(index: number): string; targetId: string; - uniqueId: string; + readonly uniqueId: string; } // @public @@ -354,9 +354,9 @@ export class HTMLView implemen // @public (undocumented) export abstract class InlinableHTMLDirective extends AspectedHTMLDirective { // (undocumented) - abstract binding: Binding; + abstract readonly binding: Binding; // (undocumented) - abstract rawAspect?: string; + abstract readonly rawAspect?: string; } // Warning: (ae-internal-missing-underscore) The name "Mutable" should be prefixed with an underscore because the declaration is marked as @internal @@ -431,7 +431,7 @@ export class PropertyChangeNotifier implements Notifier { } // @public -export function ref(propertyName: keyof T & string): CaptureType; +export const ref: (propertyName: keyof T & string) => CaptureType; // Warning: (ae-forgotten-export) The symbol "StatelessAttachedAttributeDirective" needs to be exported by the entry point index.d.ts // diff --git a/packages/web-components/fast-element/src/templating/children.ts b/packages/web-components/fast-element/src/templating/children.ts index 0d9b24722ee..85419d018eb 100644 --- a/packages/web-components/fast-element/src/templating/children.ts +++ b/packages/web-components/fast-element/src/templating/children.ts @@ -58,10 +58,9 @@ export class ChildrenDirective extends NodeObservationDirective< * @param target - The target to observe. */ observe(target: any) { - const observerId = `${this.targetId}-observer`; const observer = - target[observerId] ?? - (target[observerId] = new MutationObserver(this.handleEvent)); + target[this.uniqueId] ?? + (target[this.uniqueId] = new MutationObserver(this.handleEvent)); observer.$fastTarget = target; observer.observe(target, this.options); } @@ -71,8 +70,7 @@ export class ChildrenDirective extends NodeObservationDirective< * @param target - The target to unobserve. */ disconnect(target: any) { - const observerId = `${this.targetId}-observer`; - const observer = target[observerId]; + const observer = target[this.uniqueId]; observer.$fastTarget = null; observer.disconnect(); } 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 11e7cd7c827..d7cc532c76c 100644 --- a/packages/web-components/fast-element/src/templating/html-directive.ts +++ b/packages/web-components/fast-element/src/templating/html-directive.ts @@ -72,7 +72,7 @@ export abstract class HTMLDirective implements ViewBehaviorFactory { /** * The unique id of the directive instance. */ - public uniqueId: string = nextId(); + public readonly uniqueId: string = nextId(); /** * Creates a placeholder string based on the directive's index within the template. @@ -104,8 +104,8 @@ export abstract class AspectedHTMLDirective extends HTMLDirective { } export abstract class InlinableHTMLDirective extends AspectedHTMLDirective { - abstract binding: Binding; - abstract rawAspect?: string; + abstract readonly binding: Binding; + abstract readonly rawAspect?: string; } /** @internal */ diff --git a/packages/web-components/fast-element/src/templating/node-observation.ts b/packages/web-components/fast-element/src/templating/node-observation.ts index 76e222dbdeb..489b3e6569c 100644 --- a/packages/web-components/fast-element/src/templating/node-observation.ts +++ b/packages/web-components/fast-element/src/templating/node-observation.ts @@ -39,11 +39,10 @@ const selectElements = value => value.nodeType === 1; * @param selector - An optional selector to restrict the filter to. * @public */ -export function elements(selector?: string): ElementsFilter { - return selector +export const elements = (selector?: string): ElementsFilter => + selector ? value => value.nodeType === 1 && (value as HTMLElement).matches(selector) : selectElements; -} /** * A base class for node observation. diff --git a/packages/web-components/fast-element/src/templating/ref.ts b/packages/web-components/fast-element/src/templating/ref.ts index e0c176b7795..fb4d47a795e 100644 --- a/packages/web-components/fast-element/src/templating/ref.ts +++ b/packages/web-components/fast-element/src/templating/ref.ts @@ -37,6 +37,5 @@ export class RefDirective extends StatelessAttachedAttributeDirective { * @param propertyName - The name of the property to assign the reference to. * @public */ -export function ref(propertyName: keyof T & string): CaptureType { - return new RefDirective(propertyName); -} +export const ref = (propertyName: keyof T & string): CaptureType => + new RefDirective(propertyName); From dadcc40e5b50d8f4d4f9e5b79c7a4efe566c01e2 Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Mon, 25 Oct 2021 23:10:56 -0400 Subject: [PATCH 044/135] refactor: consolidate more unique id logic --- .../fast-element/src/styles/element-styles.ts | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/packages/web-components/fast-element/src/styles/element-styles.ts b/packages/web-components/fast-element/src/styles/element-styles.ts index 95f22f5703f..57dc196b334 100644 --- a/packages/web-components/fast-element/src/styles/element-styles.ts +++ b/packages/web-components/fast-element/src/styles/element-styles.ts @@ -1,5 +1,5 @@ -import type { Behavior } from "../observation/behavior.js"; -import { DOM } from "../dom.js"; +import type { Behavior } from "../observation/behavior"; +import { DOM, nextId } from "../dom"; /** * A node that can be targeted by styles. @@ -195,12 +195,6 @@ export class AdoptedStyleSheetsStyles extends ElementStyles { } } -let styleClassId = 0; - -function getNextStyleClass(): string { - return `fast-style-class-${++styleClassId}`; -} - /** * @internal */ @@ -211,7 +205,7 @@ export class StyleElementStyles extends ElementStyles { public constructor(styles: ComposableStyles[]) { super(styles, reduceBehaviors(styles)); this.styleSheets = reduceStyles(styles) as string[]; - this.styleClass = getNextStyleClass(); + this.styleClass = nextId(); } public addStylesTo(target: StyleTarget): void { From 97b4b9a0448f2a89d7fa74e065ab200de7c7b2e6 Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Tue, 26 Oct 2021 09:51:25 -0400 Subject: [PATCH 045/135] chore: fix module import path --- .../web-components/fast-element/src/templating/view.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/web-components/fast-element/src/templating/view.ts b/packages/web-components/fast-element/src/templating/view.ts index 3f255367e51..36eca6e8cf0 100644 --- a/packages/web-components/fast-element/src/templating/view.ts +++ b/packages/web-components/fast-element/src/templating/view.ts @@ -1,7 +1,10 @@ -import type { ViewBehavior } from ".."; import type { Behavior } from "../observation/behavior"; import type { ExecutionContext } from "../observation/observable"; -import type { ViewBehaviorTargets, ViewBehaviorFactory } from "./html-directive"; +import type { + ViewBehaviorTargets, + ViewBehaviorFactory, + ViewBehavior, +} from "./html-directive"; /** * Represents a collection of DOM nodes which can be bound to a data source. From d7cbe1153d42964076377c43d42710b54b2d4887 Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Thu, 28 Oct 2021 23:56:06 -0400 Subject: [PATCH 046/135] feat: signal bindings and refactoring of binding type APIs --- .../fast-element/docs/api-report.md | 20 +- .../fast-element/src/templating/binding.ts | 176 +++++++++++++----- 2 files changed, 141 insertions(+), 55 deletions(-) diff --git a/packages/web-components/fast-element/docs/api-report.md b/packages/web-components/fast-element/docs/api-report.md index c29fba09785..34174aa7810 100644 --- a/packages/web-components/fast-element/docs/api-report.md +++ b/packages/web-components/fast-element/docs/api-report.md @@ -61,7 +61,7 @@ export interface Behavior { } // @public (undocumented) -export function bind(binding: Binding, config?: BindingConfig | DefaultBindingOptions): CaptureType; +export function bind(binding: Binding, config?: BindingConfig | DefaultBindingOptions): CaptureType; // @public export type Binding = (source: TSource, context: ExecutionContext) => TReturn; @@ -72,7 +72,7 @@ export type BindingBehaviorFactory = { }; // @public (undocumented) -export interface BindingConfig { +export interface BindingConfig { // (undocumented) mode: BindingMode; // (undocumented) @@ -82,17 +82,17 @@ export interface BindingConfig { // @public (undocumented) export interface BindingMode { // (undocumented) - attribute?: BindingType; + attribute: BindingType; // (undocumented) - booleanAttribute?: BindingType; + booleanAttribute: BindingType; // (undocumented) - content?: BindingType; + content: BindingType; // (undocumented) - event?: BindingType; + event: BindingType; // (undocumented) - property?: BindingType; + property: BindingType; // (undocumented) - tokenList?: BindingType; + tokenList: BindingType; } // @public @@ -406,10 +406,10 @@ export interface ObservationRecord { } // @public (undocumented) -export const onChange: BindingConfig & ((options?: DefaultBindingOptions | undefined) => BindingConfig); +export const onChange: BindingConfig & ((options?: DefaultBindingOptions | undefined) => BindingConfig); // @public (undocumented) -export const oneTime: BindingConfig & ((options?: DefaultBindingOptions | undefined) => BindingConfig); +export const oneTime: BindingConfig & ((options?: DefaultBindingOptions | undefined) => BindingConfig); // @public export interface PartialFASTElementDefinition { diff --git a/packages/web-components/fast-element/src/templating/binding.ts b/packages/web-components/fast-element/src/templating/binding.ts index 72cfffb71aa..a0a443fe8f7 100644 --- a/packages/web-components/fast-element/src/templating/binding.ts +++ b/packages/web-components/fast-element/src/templating/binding.ts @@ -1,5 +1,6 @@ import { DOM } from "../dom"; -import type { Constructable, Mutable } from "../interfaces"; +import { isString, Mutable } from "../interfaces"; +import { PropertyChangeNotifier } from "../observation/notifier"; import { Binding, BindingObserver, @@ -20,17 +21,20 @@ export type BindingBehaviorFactory = { }; export type BindingType = (directive: HTMLBindingDirective) => BindingBehaviorFactory; +export const notSupportedBindingType: BindingType = () => { + throw new Error(); +}; export interface BindingMode { - attribute?: BindingType; - booleanAttribute?: BindingType; - property?: BindingType; - content?: BindingType; - tokenList?: BindingType; - event?: BindingType; + attribute: BindingType; + booleanAttribute: BindingType; + property: BindingType; + content: BindingType; + tokenList: BindingType; + event: BindingType; } -export interface BindingConfig { +export interface BindingConfig { mode: BindingMode; options: any; } @@ -64,7 +68,39 @@ class TargetUpdateBinding extends BindingBase { super(directive); } - static createType(updateTarget: UpdateTarget) { + static createBindingConfig(defaultOptions: T, eventType?: BindingType) { + const config: BindingConfig & + ((options?: typeof defaultOptions) => BindingConfig) = ( + options: typeof defaultOptions + ): BindingConfig => { + return { + mode: config.mode, + options: Object.assign({}, defaultOptions, options), + }; + }; + + config.options = defaultOptions; + config.mode = this.createBindingMode(eventType); + + return config; + } + + static createBindingMode( + eventType: BindingType = notSupportedBindingType + ): BindingMode { + return Object.freeze({ + attribute: this.createType(DOM.setAttribute), + booleanAttribute: this.createType(DOM.setBooleanAttribute), + property: this.createType( + (target, aspect, value) => (target[aspect] = value) + ), + content: createContentBinding(this).createType(updateContentTarget), + tokenList: this.createType(updateTokenListTarget), + event: eventType, + }); + } + + private static createType(updateTarget: UpdateTarget) { return directive => new this(directive, updateTarget); } } @@ -83,6 +119,69 @@ class OneTimeBinding extends TargetUpdateBinding { } } +const signals = new PropertyChangeNotifier({}); + +export function sendSignal(signal: string) { + signals.notify(signal); +} + +class OnSignalBinding extends TargetUpdateBinding { + bind( + source: any, + context: ExecutionContext, + targets: ViewBehaviorTargets + ): void { + const handler = this.getOrCreateHandler(source, context, targets); + handler.handleChange(); + signals.subscribe(handler, this.getSignal(source, context)); + } + + unbind( + source: any, + context: ExecutionContext, + targets: ViewBehaviorTargets + ): void { + const directive = this.directive; + const target = targets[directive.targetId]; + signals.unsubscribe(target[directive.uniqueId], this.getSignal(source, context)); + } + + private getSignal(source: any, context: ExecutionContext) { + const options = this.directive.options; + return isString(options) ? options : options(source, context); + } + + private getOrCreateHandler( + source: any, + context: ExecutionContext, + targets: ViewBehaviorTargets + ) { + const directive = this.directive; + const target = targets[directive.targetId]; + let handler = target[directive.uniqueId]; + + if (!handler) { + handler = { + target, + directive, + handleChange() { + this.directive.updateTarget( + this.target, + this.directive.aspect, + this.directive.binding(this.source, this.context), + this.source, + this.context + ); + }, + }; + } + + handler.source = source; + handler.context = context; + return handler; + } +} + class OnChangeBinding extends TargetUpdateBinding { private isBindingVolatile: boolean; @@ -340,34 +439,21 @@ const defaultBindingOptions: DefaultBindingOptions = { capture: false, }; -function createBindingConfig( - Base: typeof TargetUpdateBinding, - Listener: Constructable -) { - const config: BindingConfig & ((options?: DefaultBindingOptions) => BindingConfig) = ( - options: DefaultBindingOptions - ): BindingConfig => { - return { - mode: config.mode, - options: Object.assign({}, defaultBindingOptions, options), - }; - }; +export const onChange = OnChangeBinding.createBindingConfig( + defaultBindingOptions, + directive => new EventListener(directive) +); - config.options = defaultBindingOptions; - config.mode = Object.freeze({ - attribute: Base.createType(DOM.setAttribute), - booleanAttribute: Base.createType(DOM.setBooleanAttribute), - property: Base.createType((target, aspect, value) => (target[aspect] = value)), - content: createContentBinding(Base).createType(updateContentTarget), - tokenList: Base.createType(updateTokenListTarget), - event: directive => new Listener(directive), - }); - - return config; -} +export const oneTime = OneTimeBinding.createBindingConfig( + defaultBindingOptions, + directive => new OneTimeEventListener(directive) +); -export const onChange = createBindingConfig(OnChangeBinding, EventListener); -export const oneTime = createBindingConfig(OneTimeBinding, OneTimeEventListener); +const signalMode: BindingMode = OnSignalBinding.createBindingMode(); + +export const signal = (options: string | Binding): BindingConfig => { + return { mode: signalMode, options }; +}; /** * @internal @@ -401,44 +487,44 @@ export class HTMLBindingDirective extends InlinableHTMLDirective { const binding = this.binding; /* eslint-disable-next-line */ this.binding = (s, c) => DOM.createHTML(binding(s, c)); - this.factory = this.mode.property!(this); + this.factory = this.mode.property(this); break; case "classList": - this.factory = this.mode.tokenList!(this); + this.factory = this.mode.tokenList(this); break; default: - this.factory = this.mode.property!(this); + this.factory = this.mode.property(this); break; } break; case "?": (this as Mutable).aspect = value.substr(1); - this.factory = this.mode.booleanAttribute!(this); + this.factory = this.mode.booleanAttribute(this); break; case "@": (this as Mutable).aspect = value.substr(1); - this.factory = this.mode.event!(this); + this.factory = this.mode.event(this); break; default: if (value === "class") { (this as Mutable).aspect = "className"; - this.factory = this.mode.property!(this); + this.factory = this.mode.property(this); } else { (this as Mutable).aspect = value; - this.factory = this.mode.attribute!(this); + this.factory = this.mode.attribute(this); } break; } } createBehavior(targets: ViewBehaviorTargets): ViewBehavior { - return (this.factory ?? this.mode.content!(this)).createBehavior(targets); + return (this.factory ?? this.mode.content(this)).createBehavior(targets); } } export function bind( - binding: Binding, - config: BindingConfig | DefaultBindingOptions = onChange + binding: Binding, + config: BindingConfig | DefaultBindingOptions = onChange ): CaptureType { if (!("mode" in config)) { config = onChange(config); From 7adbf894963592bae2eb4abd028e3f3bf77aa1a8 Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Tue, 2 Nov 2021 10:36:49 -0400 Subject: [PATCH 047/135] refactor(controller): minor internal improvements --- .../fast-element/src/components/controller.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/web-components/fast-element/src/components/controller.ts b/packages/web-components/fast-element/src/components/controller.ts index 150911fb744..1eb174f272d 100644 --- a/packages/web-components/fast-element/src/components/controller.ts +++ b/packages/web-components/fast-element/src/components/controller.ts @@ -16,7 +16,7 @@ const defaultEventOptions: CustomEventInit = { }; function getShadowRoot(element: HTMLElement): ShadowRoot | null { - return element.shadowRoot || shadowRoots.get(element) || null; + return element.shadowRoot ?? shadowRoots.get(element) ?? null; } /** @@ -210,7 +210,7 @@ export class Controller extends PropertyChangeNotifier { * @param behaviors - The behaviors to add. */ public addBehaviors(behaviors: ReadonlyArray>): void { - const targetBehaviors = this.behaviors || (this.behaviors = new Map()); + const targetBehaviors = this.behaviors ?? (this.behaviors = new Map()); const length = behaviors.length; const behaviorsToBind: Behavior[] = []; @@ -292,7 +292,7 @@ export class Controller extends PropertyChangeNotifier { const behaviors = this.behaviors; if (behaviors !== null) { - for (const [behavior] of behaviors) { + for (const behavior of behaviors.keys()) { behavior.bind(element, defaultExecutionContext); } } @@ -320,7 +320,7 @@ export class Controller extends PropertyChangeNotifier { if (behaviors !== null) { const element = this.element; - for (const [behavior] of behaviors) { + for (const behavior of behaviors.keys()) { behavior.unbind(element, defaultExecutionContext); } } @@ -391,7 +391,7 @@ export class Controller extends PropertyChangeNotifier { this._template = (this.element as any).resolveTemplate(); } else if (definition.template) { // 3. Default to the static definition. - this._template = definition.template || null; + this._template = definition.template ?? null; } } @@ -409,7 +409,7 @@ export class Controller extends PropertyChangeNotifier { this._styles = (this.element as any).resolveStyles(); } else if (definition.styles) { // 3. Default to the static definition. - this._styles = definition.styles || null; + this._styles = definition.styles ?? null; } } @@ -426,7 +426,7 @@ export class Controller extends PropertyChangeNotifier { // When getting the host to render to, we start by looking // up the shadow root. If there isn't one, then that means // we're doing a Light DOM render to the element's direct children. - const host = getShadowRoot(element) || element; + const host = getShadowRoot(element) ?? element; if (this.view !== null) { // If there's already a view, we need to unbind and remove through dispose. From 4a5152318a37bf6ff9d5a5474f918306e8bcbaee Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Tue, 2 Nov 2021 12:38:24 -0400 Subject: [PATCH 048/135] refactor(attributes): reduce some duplication --- .../fast-element/src/components/attributes.ts | 33 ++++++++----------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/packages/web-components/fast-element/src/components/attributes.ts b/packages/web-components/fast-element/src/components/attributes.ts index b9e0fb24510..e70ce543b02 100644 --- a/packages/web-components/fast-element/src/components/attributes.ts +++ b/packages/web-components/fast-element/src/components/attributes.ts @@ -64,20 +64,24 @@ export const booleanConverter: ValueConverter = { }, fromView(value: any): any { - if ( - value === null || + return value === null || value === void 0 || value === "false" || value === false || value === 0 - ) { - return false; - } - - return true; + ? false + : true; }, }; +function toNumber(value: any): any { + if (value === null || value === undefined) { + return null; + } + const number: number = value * 1; + return isNaN(number) ? null : number; +} + /** * A {@link ValueConverter} that converts to and from `number` values. * @remarks @@ -87,20 +91,11 @@ export const booleanConverter: ValueConverter = { */ export const nullableNumberConverter: ValueConverter = { toView(value: any): string | null { - if (value === null || value === undefined) { - return null; - } - const number: number = value * 1; - return isNaN(number) ? null : number.toString(); + const output = toNumber(value); + return output ? output.toString() : output; }, - fromView(value: any): any { - if (value === null || value === undefined) { - return null; - } - const number: number = value * 1; - return isNaN(number) ? null : number; - }, + fromView: toNumber, }; /** From 913f6214f75ec9b85e681e0d7e73a3e2ebd68f4e Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Tue, 2 Nov 2021 13:15:52 -0400 Subject: [PATCH 049/135] fix(binding): bug in location of update target --- .../fast-element/src/observation/array-change-records.ts | 2 +- packages/web-components/fast-element/src/templating/binding.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/web-components/fast-element/src/observation/array-change-records.ts b/packages/web-components/fast-element/src/observation/array-change-records.ts index aad738b58ac..33d416e966c 100644 --- a/packages/web-components/fast-element/src/observation/array-change-records.ts +++ b/packages/web-components/fast-element/src/observation/array-change-records.ts @@ -245,7 +245,7 @@ export function calcSplices( oldEnd -= suffixCount; if (currentEnd - currentStart === 0 && oldEnd - oldStart === 0) { - return emptyArray; + return emptyArray as any; } if (currentStart === currentEnd) { diff --git a/packages/web-components/fast-element/src/templating/binding.ts b/packages/web-components/fast-element/src/templating/binding.ts index a0a443fe8f7..bd55b687160 100644 --- a/packages/web-components/fast-element/src/templating/binding.ts +++ b/packages/web-components/fast-element/src/templating/binding.ts @@ -164,8 +164,9 @@ class OnSignalBinding extends TargetUpdateBinding { handler = { target, directive, + updateTarget: this.updateTarget, handleChange() { - this.directive.updateTarget( + this.updateTarget( this.target, this.directive.aspect, this.directive.binding(this.source, this.context), From d3d32bad2a4b3d34929cec2521c97757cbf6435e Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Thu, 4 Nov 2021 17:34:10 -0400 Subject: [PATCH 050/135] refactor: modernize the splice code --- .../fast-element/docs/api-report.md | 8 +- .../src/observation/array-change-records.ts | 169 ++++++++---------- .../src/observation/array-observer.ts | 34 +--- 3 files changed, 91 insertions(+), 120 deletions(-) diff --git a/packages/web-components/fast-element/docs/api-report.md b/packages/web-components/fast-element/docs/api-report.md index 34174aa7810..bfb4247549b 100644 --- a/packages/web-components/fast-element/docs/api-report.md +++ b/packages/web-components/fast-element/docs/api-report.md @@ -488,9 +488,15 @@ export interface SlottedDirectiveOptions extends NodeBehaviorOptions } // @public -export interface Splice { +export class Splice { + constructor( + index: number, + removed: any[], + addedCount: number); addedCount: number; index: number; + // (undocumented) + static normalize(previous: unknown[] | undefined, current: unknown[], changes: Splice[] | undefined): Splice[] | undefined; removed: any[]; } diff --git a/packages/web-components/fast-element/src/observation/array-change-records.ts b/packages/web-components/fast-element/src/observation/array-change-records.ts index 33d416e966c..695b0312bf9 100644 --- a/packages/web-components/fast-element/src/observation/array-change-records.ts +++ b/packages/web-components/fast-element/src/observation/array-change-records.ts @@ -1,40 +1,12 @@ import { emptyArray } from "../platform.js"; -/** - * Represents a set of splice-based changes against an Array. - * @public - */ -export interface Splice { - /** - * The index that the splice occurs at. - */ - index: number; - - /** - * The items that were removed. - */ - removed: any[]; - - /** - * The number of items that were added. - */ - addedCount: number; +const enum Edit { + leave = 0, + update = 1, + add = 2, + delete = 3, } -/** @internal */ -export function newSplice(index: number, removed: any[], addedCount: number): Splice { - return { - index: index, - removed: removed, - addedCount: addedCount, - }; -} - -const EDIT_LEAVE = 0; -const EDIT_UPDATE = 1; -const EDIT_ADD = 2; -const EDIT_DELETE = 3; - // Note: This function is *based* on the computation of the Levenshtein // "edit" distance. The one change is that "updates" are treated as two // edits - not one. With Array splices, an update is really a delete @@ -98,12 +70,12 @@ function spliceOperationsFromEditDistances(distances: number[][]): number[] { while (i > 0 || j > 0) { if (i === 0) { - edits.push(EDIT_ADD); + edits.push(Edit.add); j--; continue; } if (j === 0) { - edits.push(EDIT_DELETE); + edits.push(Edit.delete); i--; continue; } @@ -121,19 +93,19 @@ function spliceOperationsFromEditDistances(distances: number[][]): number[] { if (min === northWest) { if (northWest === current) { - edits.push(EDIT_LEAVE); + edits.push(Edit.leave); } else { - edits.push(EDIT_UPDATE); + edits.push(Edit.update); current = northWest; } i--; j--; } else if (min === west) { - edits.push(EDIT_DELETE); + edits.push(Edit.delete); i--; current = west; } else { - edits.push(EDIT_ADD); + edits.push(Edit.add); j--; current = north; } @@ -194,21 +166,6 @@ function intersect(start1: number, end1: number, start2: number, end2: number): } /** - * Splice Projection functions: - * - * 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 - * - * - * - * 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. - */ - -/** - * @internal * @remarks * Lacking individual splice mutation information, the minimal set of * splices can be synthesized given the previous state and final state of an @@ -219,14 +176,14 @@ function intersect(start1: number, end1: number, start2: number, end2: number): * l: The length of the current array * p: The length of the old array */ -export function calcSplices( - current: any[], +function calc( + current: unknown[], currentStart: number, currentEnd: number, - old: any[], + old: unknown[], oldStart: number, oldEnd: number -): ReadonlyArray | Splice[] { +): Splice[] { let prefixCount = 0; let suffixCount = 0; @@ -249,7 +206,7 @@ export function calcSplices( } if (currentStart === currentEnd) { - const splice = newSplice(currentStart, [], 0); + const splice = new Splice(currentStart, [], 0); while (oldStart < oldEnd) { splice.removed.push(old[oldStart++]); @@ -257,7 +214,7 @@ export function calcSplices( return [splice]; } else if (oldStart === oldEnd) { - return [newSplice(currentStart, [], currentEnd - currentStart)]; + return [new Splice(currentStart, [], currentEnd - currentStart)]; } const ops = spliceOperationsFromEditDistances( @@ -271,7 +228,7 @@ export function calcSplices( for (let i = 0; i < ops.length; ++i) { switch (ops[i]) { - case EDIT_LEAVE: + case Edit.leave: if (splice !== void 0) { splices.push(splice); splice = void 0; @@ -280,9 +237,9 @@ export function calcSplices( index++; oldIndex++; break; - case EDIT_UPDATE: + case Edit.update: if (splice === void 0) { - splice = newSplice(index, [], 0); + splice = new Splice(index, [], 0); } splice.addedCount++; @@ -291,17 +248,17 @@ export function calcSplices( splice.removed.push(old[oldIndex]); oldIndex++; break; - case EDIT_ADD: + case Edit.add: if (splice === void 0) { - splice = newSplice(index, [], 0); + splice = new Splice(index, [], 0); } splice.addedCount++; index++; break; - case EDIT_DELETE: + case Edit.delete: if (splice === void 0) { - splice = newSplice(index, [], 0); + splice = new Splice(index, [], 0); } splice.removed.push(old[oldIndex]); @@ -318,15 +275,7 @@ export function calcSplices( return splices; } -const $push = Array.prototype.push; - -function mergeSplice( - splices: Splice[], - index: number, - removed: any[], - addedCount: number -): void { - const splice = newSplice(index, removed, addedCount); +function merge(splice: Splice, splices: Splice[]): void { let inserted = false; let insertionOffset = 0; @@ -366,7 +315,7 @@ function mergeSplice( if (splice.index < current.index) { // some prefix of splice.removed is prepended to current.removed. const prepend = splice.removed.slice(0, current.index - splice.index); - $push.apply(prepend, currentRemoved); + prepend.push(...currentRemoved); currentRemoved = prepend; } @@ -378,7 +327,7 @@ function mergeSplice( const append = splice.removed.slice( current.index + current.addedCount - splice.index ); - $push.apply(currentRemoved, append); + currentRemoved.push(...append); } splice.removed = currentRemoved; @@ -389,7 +338,6 @@ function mergeSplice( } } else if (splice.index < current.index) { // Insert splice here. - inserted = true; splices.splice(i, 0, splice); @@ -406,22 +354,14 @@ function mergeSplice( } } -function createInitialSplices(changeRecords: Splice[]): Splice[] { - const splices: Splice[] = []; +function project(array: unknown[], changes: Splice[]): Splice[] { + let splices: Splice[] = []; + const initialSplices: Splice[] = []; - for (let i = 0, ii = changeRecords.length; i < ii; i++) { - const record = changeRecords[i]; - mergeSplice(splices, record.index, record.removed, record.addedCount); + for (let i = 0, ii = changes.length; i < ii; i++) { + merge(changes[i], initialSplices); } - return splices; -} - -/** @internal */ -export function projectArraySplices(array: any[], changeRecords: any[]): Splice[] { - let splices: Splice[] = []; - const initialSplices = createInitialSplices(changeRecords); - for (let i = 0, ii = initialSplices.length; i < ii; ++i) { const splice = initialSplices[i]; @@ -434,7 +374,7 @@ export function projectArraySplices(array: any[], changeRecords: any[]): Splice[ } splices = splices.concat( - calcSplices( + calc( array, splice.index, splice.index + splice.addedCount, @@ -447,3 +387,46 @@ export function projectArraySplices(array: any[], changeRecords: any[]): Splice[ return splices; } + +/** + * 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 + * + * + * + * 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 + ) { + return previous === void 0 + ? changes!.length > 1 + ? project(current, changes!) + : changes + : calc(current, 0, current.length, previous, 0, previous.length); + } +} diff --git a/packages/web-components/fast-element/src/observation/array-observer.ts b/packages/web-components/fast-element/src/observation/array-observer.ts index 33af938c546..0ba48efe0d0 100644 --- a/packages/web-components/fast-element/src/observation/array-observer.ts +++ b/packages/web-components/fast-element/src/observation/array-observer.ts @@ -16,17 +16,13 @@ function adjustIndex(changeRecord: Splice, array: any[]): Splice { arrayLength + changeRecord.removed.length + index - changeRecord.addedCount; } - if (index < 0) { - index = 0; - } - - changeRecord.index = index; + changeRecord.index = index < 0 ? 0 : index; return changeRecord; } class ArrayObserver extends SubscriberSet { private oldCollection: any[] | undefined = void 0; - private splices: any[] | undefined = void 0; + private splices: Splice[] | undefined = void 0; private needsQueue: boolean = true; call: () => void = this.flush; @@ -67,21 +63,7 @@ class ArrayObserver extends SubscriberSet { this.splices = void 0; this.oldCollection = void 0; - const finalSplices = - oldCollection === void 0 - ? splices!.length > 1 - ? projectArraySplices(this.subject, splices!) - : splices - : calcSplices( - this.subject, - 0, - this.subject.length, - oldCollection, - 0, - oldCollection.length - ); - - this.notify(finalSplices); + this.notify(Splice.normalize(oldCollection, this.subject, splices)); } private enqueue() { @@ -107,7 +89,7 @@ const arrayOverrides = { const o = this.$fastController as ArrayObserver; if (o !== void 0 && notEmpty) { - o.addSplice(newSplice(this.length, [result], 0)); + o.addSplice(new Splice(this.length, [result], 0)); } return result; @@ -120,7 +102,7 @@ const arrayOverrides = { if (o !== void 0) { o.addSplice( adjustIndex( - newSplice(this.length - arguments.length, [], arguments.length), + new Splice(this.length - arguments.length, [], arguments.length), this ) ); @@ -153,7 +135,7 @@ const arrayOverrides = { const o = this.$fastController as ArrayObserver; if (o !== void 0 && notEmpty) { - o.addSplice(newSplice(0, [result], 0)); + o.addSplice(new Splice(0, [result], 0)); } return result; @@ -184,7 +166,7 @@ const arrayOverrides = { if (o !== void 0) { o.addSplice( adjustIndex( - newSplice( + new Splice( +arguments[0], result, arguments.length > 2 ? arguments.length - 2 : 0 @@ -202,7 +184,7 @@ const arrayOverrides = { const o = this.$fastController as ArrayObserver; if (o !== void 0) { - o.addSplice(adjustIndex(newSplice(0, [], arguments.length), this)); + o.addSplice(adjustIndex(new Splice(0, [], arguments.length), this)); } return result; From 4e88dd70fa8ff8746d99c194ac1db55e0f64f0f7 Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Fri, 5 Nov 2021 09:41:18 -0400 Subject: [PATCH 051/135] refactor: small simplification in array change records --- .../fast-element/src/observation/array-change-records.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/web-components/fast-element/src/observation/array-change-records.ts b/packages/web-components/fast-element/src/observation/array-change-records.ts index 695b0312bf9..46194a9e1e4 100644 --- a/packages/web-components/fast-element/src/observation/array-change-records.ts +++ b/packages/web-components/fast-element/src/observation/array-change-records.ts @@ -111,8 +111,7 @@ function spliceOperationsFromEditDistances(distances: number[][]): number[] { } } - edits.reverse(); - return edits; + return edits.reverse(); } function sharedPrefix(current: any[], old: any[], searchLength: number): number { From 9453999b0eb9a1ad9568a113a5b1fe3dcc797f8b Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Fri, 5 Nov 2021 10:34:09 -0400 Subject: [PATCH 052/135] perf: use less memory and reduce code for signal bindings --- .../fast-element/src/templating/binding.ts | 83 ++++++++++--------- 1 file changed, 43 insertions(+), 40 deletions(-) diff --git a/packages/web-components/fast-element/src/templating/binding.ts b/packages/web-components/fast-element/src/templating/binding.ts index bd55b687160..28d75cf4df6 100644 --- a/packages/web-components/fast-element/src/templating/binding.ts +++ b/packages/web-components/fast-element/src/templating/binding.ts @@ -1,6 +1,5 @@ import { DOM } from "../dom"; import { isString, Mutable } from "../interfaces"; -import { PropertyChangeNotifier } from "../observation/notifier"; import { Binding, BindingObserver, @@ -119,10 +118,13 @@ class OneTimeBinding extends TargetUpdateBinding { } } -const signals = new PropertyChangeNotifier({}); +const signals: Record = Object.create(null); export function sendSignal(signal: string) { - signals.notify(signal); + const found = signals[signal]; + if (found) { + Array.isArray(found) ? found.forEach(x => x()) : found(); + } } class OnSignalBinding extends TargetUpdateBinding { @@ -131,9 +133,30 @@ class OnSignalBinding extends TargetUpdateBinding { context: ExecutionContext, targets: ViewBehaviorTargets ): void { - const handler = this.getOrCreateHandler(source, context, targets); - handler.handleChange(); - signals.subscribe(handler, this.getSignal(source, context)); + const directive = this.directive; + const target = targets[directive.targetId]; + const signal = this.getSignal(source, context); + const handler = (target[directive.uniqueId] = () => { + this.updateTarget( + target, + directive.aspect!, + directive.binding(source, context), + source, + context + ); + }); + + handler(); + + const found = signals[signal]; + + if (found) { + Array.isArray(found) + ? found.push(handler) + : (signals[signal] = [found, handler]); + } else { + signals[signal] = handler; + } } unbind( @@ -141,46 +164,26 @@ class OnSignalBinding extends TargetUpdateBinding { context: ExecutionContext, targets: ViewBehaviorTargets ): void { - const directive = this.directive; - const target = targets[directive.targetId]; - signals.unsubscribe(target[directive.uniqueId], this.getSignal(source, context)); + const signal = this.getSignal(source, context); + const found = signals[signal]; + + if (found && Array.isArray(found)) { + const directive = this.directive; + const target = targets[directive.targetId]; + const handler = target[directive.uniqueId]; + const index = found.indexOf(handler); + if (index !== -1) { + found.splice(index, 1); + } + } else { + signals[signal] = void 0; + } } private getSignal(source: any, context: ExecutionContext) { const options = this.directive.options; return isString(options) ? options : options(source, context); } - - private getOrCreateHandler( - source: any, - context: ExecutionContext, - targets: ViewBehaviorTargets - ) { - const directive = this.directive; - const target = targets[directive.targetId]; - let handler = target[directive.uniqueId]; - - if (!handler) { - handler = { - target, - directive, - updateTarget: this.updateTarget, - handleChange() { - this.updateTarget( - this.target, - this.directive.aspect, - this.directive.binding(this.source, this.context), - this.source, - this.context - ); - }, - }; - } - - handler.source = source; - handler.context = context; - return handler; - } } class OnChangeBinding extends TargetUpdateBinding { From 40aeff34ce5e54f416afb62b7bee2c10e1586e49 Mon Sep 17 00:00:00 2001 From: Nicholas Rice <3213292+nicholasrice@users.noreply.github.com> Date: Fri, 11 Feb 2022 13:27:33 -0800 Subject: [PATCH 053/135] chore: set up SSR package (#5589) * project files and starting to set up test infrastructure * incorporating ts project references and getting tests working * adding .npmignore to ingore tests and server * adding readmes * Update packages/web-components/fast-ssr/package.json Co-authored-by: Chris Holt Co-authored-by: nicholasrice Co-authored-by: Chris Holt --- packages/web-components/fast-ssr/.npmignore | 2 + packages/web-components/fast-ssr/README.md | 7 ++++ packages/web-components/fast-ssr/package.json | 40 +++++++++++++++++++ .../web-components/fast-ssr/server/README.md | 2 + .../web-components/fast-ssr/server/server.ts | 25 ++++++++++++ .../fast-ssr/server/tsconfig.json | 9 +++++ packages/web-components/fast-ssr/src/index.ts | 1 + .../web-components/fast-ssr/src/tsconfig.json | 12 ++++++ .../fast-ssr/test/example.spec.ts | 10 +++++ .../web-components/fast-ssr/test/package.json | 5 +++ .../fast-ssr/test/playwright.config.ts | 11 +++++ .../fast-ssr/test/tsconfig.json | 9 +++++ .../web-components/fast-ssr/tsconfig.json | 17 ++++++++ 13 files changed, 150 insertions(+) create mode 100644 packages/web-components/fast-ssr/.npmignore create mode 100644 packages/web-components/fast-ssr/README.md create mode 100644 packages/web-components/fast-ssr/package.json create mode 100644 packages/web-components/fast-ssr/server/README.md create mode 100644 packages/web-components/fast-ssr/server/server.ts create mode 100644 packages/web-components/fast-ssr/server/tsconfig.json create mode 100644 packages/web-components/fast-ssr/src/index.ts create mode 100644 packages/web-components/fast-ssr/src/tsconfig.json create mode 100644 packages/web-components/fast-ssr/test/example.spec.ts create mode 100644 packages/web-components/fast-ssr/test/package.json create mode 100644 packages/web-components/fast-ssr/test/playwright.config.ts create mode 100644 packages/web-components/fast-ssr/test/tsconfig.json create mode 100644 packages/web-components/fast-ssr/tsconfig.json diff --git a/packages/web-components/fast-ssr/.npmignore b/packages/web-components/fast-ssr/.npmignore new file mode 100644 index 00000000000..4037e8e7be9 --- /dev/null +++ b/packages/web-components/fast-ssr/.npmignore @@ -0,0 +1,2 @@ +test +server diff --git a/packages/web-components/fast-ssr/README.md b/packages/web-components/fast-ssr/README.md new file mode 100644 index 00000000000..c9302b92748 --- /dev/null +++ b/packages/web-components/fast-ssr/README.md @@ -0,0 +1,7 @@ +# FAST SSR +This package contains tools to render FAST components outside the browser. More details to follow... + +## Testing +This package uses Playwright and a lightweight web server for running tests. You can run the tests by running `npm run test`. + +> Playwright may prompt you to install browsers to run the tests. If so, follow the instructions provided or run `npm run install-playwright-browsers`. \ No newline at end of file diff --git a/packages/web-components/fast-ssr/package.json b/packages/web-components/fast-ssr/package.json new file mode 100644 index 00000000000..cdcb62fa1ec --- /dev/null +++ b/packages/web-components/fast-ssr/package.json @@ -0,0 +1,40 @@ +{ + "name": "@microsoft/fast-ssr", + "version": "0.1.0", + "type": "module", + "author": { + "name": "Microsoft", + "url": "https://discord.gg/FcSNfg4" + }, + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/Microsoft/fast.git" + }, + "bugs": { + "url": "https://github.com/Microsoft/fast/issues/new/choose" + }, + "scripts": { + "build": "tsc -b --clean src && tsc -b src", + "build-server": "tsc -b server", + "pretest": "npm run build-server", + "test": "playwright test -c test", + "test-server": "node server/dist/server.js", + "install-playwright-browsers": "npx playwright install" + }, + "description": "A package for rendering FAST components outside the browser.", + "main": "index.js", + "private": true, + "dependencies": { + "@lit-labs/ssr": "^1.0.0-rc.2", + "@microsoft/fast-element": "^1.5.0", + "tslib": "^1.11.1" + }, + "devDependencies": { + "@playwright/test": "^1.18.0", + "@types/express": "^4.17.13", + "@types/node": "^17.0.17", + "express": "^4.17.1", + "typescript": "^3.8.3" + } +} diff --git a/packages/web-components/fast-ssr/server/README.md b/packages/web-components/fast-ssr/server/README.md new file mode 100644 index 00000000000..c41dd618a80 --- /dev/null +++ b/packages/web-components/fast-ssr/server/README.md @@ -0,0 +1,2 @@ +# Server +This project contains the web server that playwright tests are run against. To build, run `npm run build-server`. \ No newline at end of file diff --git a/packages/web-components/fast-ssr/server/server.ts b/packages/web-components/fast-ssr/server/server.ts new file mode 100644 index 00000000000..e47e01fad16 --- /dev/null +++ b/packages/web-components/fast-ssr/server/server.ts @@ -0,0 +1,25 @@ +import { Readable } from "stream"; +import express, { Request, Response } from "express"; + +const PORT = 8080; +function handleRequest(req: Request, res: Response) { + res.set("Content-Type", "text/html"); + const stream = (Readable as any).from("hello world"); + stream.on("readable", function (this: any) { + let data: string; + + while ((data = this.read())) { + res.write(data); + } + }); + + stream.on("close", () => res.end()); + stream.on("error", (e: Error) => { + console.error(e); + process.exit(1); + }); +} + +const app = express(); +app.get("/", handleRequest); +app.listen(PORT); diff --git a/packages/web-components/fast-ssr/server/tsconfig.json b/packages/web-components/fast-ssr/server/tsconfig.json new file mode 100644 index 00000000000..fc7f3f2b6e1 --- /dev/null +++ b/packages/web-components/fast-ssr/server/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "composite": true, + "rootDir": ".", + "outDir": "dist" + }, + "references": [{ "path": "../src"}] +} diff --git a/packages/web-components/fast-ssr/src/index.ts b/packages/web-components/fast-ssr/src/index.ts new file mode 100644 index 00000000000..62073af2a0f --- /dev/null +++ b/packages/web-components/fast-ssr/src/index.ts @@ -0,0 +1 @@ +export default "fast-ssr"; diff --git a/packages/web-components/fast-ssr/src/tsconfig.json b/packages/web-components/fast-ssr/src/tsconfig.json new file mode 100644 index 00000000000..8b2f5bed380 --- /dev/null +++ b/packages/web-components/fast-ssr/src/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "composite": true, + "rootDir": ".", + "outDir": "../dist", + "lib": [ + "dom", + "esnext" + ], + }, +} diff --git a/packages/web-components/fast-ssr/test/example.spec.ts b/packages/web-components/fast-ssr/test/example.spec.ts new file mode 100644 index 00000000000..73c85108fa9 --- /dev/null +++ b/packages/web-components/fast-ssr/test/example.spec.ts @@ -0,0 +1,10 @@ +import { test, expect, ElementHandle } from '@playwright/test'; +import fastSSR from "../src"; + +test("example module test", async () => { + expect(fastSSR).toBe("fast-ssr"); +}) +test("example server test", async ({ page }) => { + await page.goto("/"); + expect(await page.innerText('body')).toBe("hello world"); +}); diff --git a/packages/web-components/fast-ssr/test/package.json b/packages/web-components/fast-ssr/test/package.json new file mode 100644 index 00000000000..890f891f972 --- /dev/null +++ b/packages/web-components/fast-ssr/test/package.json @@ -0,0 +1,5 @@ +{ + "type": "commonjs", + "comment": "This allows Playwright to handle TS compilation for us", + "private": true +} \ No newline at end of file diff --git a/packages/web-components/fast-ssr/test/playwright.config.ts b/packages/web-components/fast-ssr/test/playwright.config.ts new file mode 100644 index 00000000000..4b048b7ff22 --- /dev/null +++ b/packages/web-components/fast-ssr/test/playwright.config.ts @@ -0,0 +1,11 @@ +// playwright.config.ts +import { PlaywrightTestConfig } from "@playwright/test"; +const config: PlaywrightTestConfig = { + webServer: { + command: "npm run test-server", + port: 8080, + timeout: 120 * 1000, + reuseExistingServer: false, + }, +}; +export default config; diff --git a/packages/web-components/fast-ssr/test/tsconfig.json b/packages/web-components/fast-ssr/test/tsconfig.json new file mode 100644 index 00000000000..2ba585452cf --- /dev/null +++ b/packages/web-components/fast-ssr/test/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "rootDir": ".", + "outDir": "dist", + "declaration": false + }, + "references": [{ "path": "../src"}] +} diff --git a/packages/web-components/fast-ssr/tsconfig.json b/packages/web-components/fast-ssr/tsconfig.json new file mode 100644 index 00000000000..cf4cd344bc2 --- /dev/null +++ b/packages/web-components/fast-ssr/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "allowSyntheticDefaultImports": true, + "target": "ES2015", + "module": "ES2015", + "moduleResolution": "node", + "importHelpers": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "noEmitOnError": true, + "strict": true, + "lib": [ + "dom", + "esnext" + ], + }, +} From bc897f98e0fdde1a719d63416f69420d72ea260a Mon Sep 17 00:00:00 2001 From: Nicholas Rice <3213292+nicholasrice@users.noreply.github.com> Date: Tue, 15 Feb 2022 10:11:55 -0800 Subject: [PATCH 054/135] feat: expose fast-element as a module package to NodeJS (#5601) * add lint rule to enforce file extensions in imports * declare fast-element as a ESModule package * convert eslintrc to a commonjs module * adding file extensions to import paths * fixing accidently modified string * fixing build processes * revert change * Change files Co-authored-by: nicholasrice --- ...-b1805a85-fe36-49d9-b65a-2484ae0eaab0.json | 7 +++ .../web-components/fast-element/.eslintrc.cjs | 19 +++++++ .../fast-element/.eslintrc.json | 30 ----------- .../web-components/fast-element/.npmignore | 4 +- .../web-components/fast-element/package.json | 1 + .../fast-element/rollup.config.js | 2 +- .../fast-element/src/components/attributes.ts | 8 +-- .../fast-element/src/components/controller.ts | 1 - .../src/components/fast-definitions.ts | 10 ++-- .../web-components/fast-element/src/dom.ts | 4 +- .../web-components/fast-element/src/index.ts | 52 +++++++++---------- .../src/observation/observable.ts | 8 +-- .../fast-element/src/styles/css.ts | 10 ++-- .../fast-element/src/styles/element-styles.ts | 4 +- .../fast-element/src/templating/binding.ts | 12 ++--- .../fast-element/src/templating/children.ts | 6 +-- .../fast-element/src/templating/compiler.ts | 10 ++-- .../src/templating/html-directive.ts | 6 +-- .../src/templating/node-observation.ts | 6 +-- .../fast-element/src/templating/ref.ts | 6 +-- .../fast-element/src/templating/repeat.ts | 22 ++++---- .../fast-element/src/templating/slotted.ts | 6 +-- .../fast-element/src/templating/template.ts | 16 +++--- .../fast-element/src/templating/view.ts | 10 ++-- .../fast-element/src/templating/when.ts | 6 +-- 25 files changed, 131 insertions(+), 135 deletions(-) create mode 100644 change/@microsoft-fast-element-b1805a85-fe36-49d9-b65a-2484ae0eaab0.json create mode 100644 packages/web-components/fast-element/.eslintrc.cjs delete mode 100644 packages/web-components/fast-element/.eslintrc.json diff --git a/change/@microsoft-fast-element-b1805a85-fe36-49d9-b65a-2484ae0eaab0.json b/change/@microsoft-fast-element-b1805a85-fe36-49d9-b65a-2484ae0eaab0.json new file mode 100644 index 00000000000..f34f3764ec9 --- /dev/null +++ b/change/@microsoft-fast-element-b1805a85-fe36-49d9-b65a-2484ae0eaab0.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "enumerate fast-element package as a ES module package", + "packageName": "@microsoft/fast-element", + "email": "nicholasrice@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/packages/web-components/fast-element/.eslintrc.cjs b/packages/web-components/fast-element/.eslintrc.cjs new file mode 100644 index 00000000000..d5ea064f67e --- /dev/null +++ b/packages/web-components/fast-element/.eslintrc.cjs @@ -0,0 +1,19 @@ +module.exports = { + extends: ["@microsoft/eslint-config-fast-dna", "prettier"], + rules: { + "import/extensions": ["error", "always"], + "max-classes-per-file": "off", + "no-case-declarations": "off", + "@typescript-eslint/ban-types": [ + "error", + { + types: { + "{}": false, + Function: false, + Object: false, + }, + extendDefaults: true, + }, + ], + }, +}; diff --git a/packages/web-components/fast-element/.eslintrc.json b/packages/web-components/fast-element/.eslintrc.json deleted file mode 100644 index cb11089e167..00000000000 --- a/packages/web-components/fast-element/.eslintrc.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "extends": ["@microsoft/eslint-config-fast-dna", "prettier"], - "rules": { - "max-classes-per-file": "off", - "no-case-declarations": "off", - "@typescript-eslint/ban-types": [ - "error", - { - "types": { - "{}": false, - "Function": false, - "Object": false - }, - "extendDefaults": true - } - ], - "@typescript-eslint/no-use-before-define": ["error", { "typedefs": false }], - "@typescript-eslint/no-non-null-assertion": "off", - "@typescript-eslint/explicit-function-return-type": [ - "error", - { - "allowExpressions": true - } - ], - "import/extensions": [ - "error", - "always" - ] - } -} diff --git a/packages/web-components/fast-element/.npmignore b/packages/web-components/fast-element/.npmignore index 08e9c472ee9..79b219206fb 100644 --- a/packages/web-components/fast-element/.npmignore +++ b/packages/web-components/fast-element/.npmignore @@ -9,7 +9,7 @@ src/ # config files .eslintignore -.eslintrc.js +.eslintrc.cjs .mocharc.json .prettierignore api-extractor.json @@ -19,4 +19,4 @@ tsconfig.json # cache .rollupcache -temp \ No newline at end of file +temp diff --git a/packages/web-components/fast-element/package.json b/packages/web-components/fast-element/package.json index 595446d3b57..b812544567c 100644 --- a/packages/web-components/fast-element/package.json +++ b/packages/web-components/fast-element/package.json @@ -16,6 +16,7 @@ "url": "https://github.com/Microsoft/fast/issues/new/choose" }, "main": "dist/esm/index.js", + "type": "module", "types": "dist/fast-element.d.ts", "type": "module", "unpkg": "dist/fast-element.min.js", diff --git a/packages/web-components/fast-element/rollup.config.js b/packages/web-components/fast-element/rollup.config.js index 1f5b65ed616..0bf491c43ac 100644 --- a/packages/web-components/fast-element/rollup.config.js +++ b/packages/web-components/fast-element/rollup.config.js @@ -7,7 +7,7 @@ import typescript from "rollup-plugin-typescript2"; import { transformCSSFragment, transformHTMLFragment, -} from "../../../build/transform-fragments"; +} from "../../../build/transform-fragments.js"; const parserOptions = { sourceType: "module", diff --git a/packages/web-components/fast-element/src/components/attributes.ts b/packages/web-components/fast-element/src/components/attributes.ts index e70ce543b02..cb3051f44f4 100644 --- a/packages/web-components/fast-element/src/components/attributes.ts +++ b/packages/web-components/fast-element/src/components/attributes.ts @@ -1,7 +1,7 @@ -import { Accessor, Observable } from "../observation/observable"; -import { DOM } from "../dom"; -import type { Notifier } from "../observation/notifier"; -import { isString } from "../interfaces"; +import { Accessor, Observable } from "../observation/observable.js"; +import { DOM } from "../dom.js"; +import type { Notifier } from "../observation/notifier.js"; +import { isString } from "../interfaces.js"; /** * Represents objects that can convert values to and from diff --git a/packages/web-components/fast-element/src/components/controller.ts b/packages/web-components/fast-element/src/components/controller.ts index 1eb174f272d..c6d1aeef58d 100644 --- a/packages/web-components/fast-element/src/components/controller.ts +++ b/packages/web-components/fast-element/src/components/controller.ts @@ -1,4 +1,3 @@ -import { DOM } from "../dom.js"; import type { Mutable } from "../interfaces.js"; import type { Behavior } from "../observation/behavior.js"; import { PropertyChangeNotifier } from "../observation/notifier.js"; diff --git a/packages/web-components/fast-element/src/components/fast-definitions.ts b/packages/web-components/fast-element/src/components/fast-definitions.ts index 9c698fbfb87..154b00edc1a 100644 --- a/packages/web-components/fast-element/src/components/fast-definitions.ts +++ b/packages/web-components/fast-element/src/components/fast-definitions.ts @@ -1,8 +1,8 @@ -import { isString, Mutable } from "../interfaces"; -import { Observable } from "../observation/observable"; -import { ComposableStyles, ElementStyles } from "../styles/element-styles"; -import type { ElementViewTemplate } from "../templating/template"; -import { AttributeConfiguration, AttributeDefinition } from "./attributes"; +import { isString, Mutable } from "../interfaces.js"; +import { Observable } from "../observation/observable.js"; +import { ComposableStyles, ElementStyles } from "../styles/element-styles.js"; +import type { ElementViewTemplate } from "../templating/template.js"; +import { AttributeConfiguration, AttributeDefinition } from "./attributes.js"; const defaultShadowOptions: ShadowRootInit = { mode: "open" }; const defaultElementOptions: ElementDefinitionOptions = {}; diff --git a/packages/web-components/fast-element/src/dom.ts b/packages/web-components/fast-element/src/dom.ts index a478147b26a..f95cd66af56 100644 --- a/packages/web-components/fast-element/src/dom.ts +++ b/packages/web-components/fast-element/src/dom.ts @@ -1,5 +1,5 @@ -import type { Callable } from "./interfaces"; -import { $global, TrustedTypesPolicy } from "./platform"; +import type { Callable } from "./interfaces.js"; +import { $global, TrustedTypesPolicy } from "./platform.js"; /* eslint-disable */ const fastHTMLPolicy: TrustedTypesPolicy = $global.trustedTypes.createPolicy( diff --git a/packages/web-components/fast-element/src/index.ts b/packages/web-components/fast-element/src/index.ts index e7ea35b8a32..178aea730f3 100644 --- a/packages/web-components/fast-element/src/index.ts +++ b/packages/web-components/fast-element/src/index.ts @@ -1,29 +1,28 @@ -export * from "./platform"; -export * from "./templating/template"; -export * from "./components/fast-element"; +export * from "./platform.js"; +export * from "./templating/template.js"; +export * from "./components/fast-element.js"; export { FASTElementDefinition, PartialFASTElementDefinition, -} from "./components/fast-definitions"; -export * from "./components/attributes"; -export * from "./components/controller"; -export type { Callable, Constructable, Mutable } from "./interfaces"; -export * from "./templating/compiler"; +} from "./components/fast-definitions.js"; +export * from "./components/attributes.js"; +export * from "./components/controller.js"; +export type { Callable, Constructable, Mutable } from "./interfaces.js"; +export * from "./templating/compiler.js"; export { ElementStyles, ElementStyleFactory, ComposableStyles, StyleTarget, -} from "./styles/element-styles"; -export { css, cssPartial } from "./styles/css"; -export { CSSDirective } from "./styles/css-directive"; -export * from "./templating/view"; -export * from "./observation/observable"; -export * from "./observation/notifier"; -export { Splice } from "./observation/array-change-records"; -export { enableArrayObservation } from "./observation/array-observer"; -export { DOM } from "./dom"; -export type { Behavior } from "./observation/behavior"; +} from "./styles/element-styles.js"; +export { css, cssPartial } from "./styles/css.js"; +export { CSSDirective } from "./styles/css-directive.js"; +export * from "./observation/observable.js"; +export * from "./observation/notifier.js"; +export { Splice } from "./observation/array-change-records.js"; +export { enableArrayObservation } from "./observation/array-observer.js"; +export { DOM } from "./dom.js"; +export type { Behavior } from "./observation/behavior.js"; export { bind, oneTime, @@ -33,7 +32,7 @@ export { BindingType, BindingBehaviorFactory, DefaultBindingOptions, -} from "./templating/binding"; +} from "./templating/binding.js"; export { ViewBehaviorTargets, ViewBehavior, @@ -41,14 +40,15 @@ export { HTMLDirective, AspectedHTMLDirective, InlinableHTMLDirective, -} from "./templating/html-directive"; -export * from "./templating/ref"; -export * from "./templating/when"; -export * from "./templating/repeat"; -export * from "./templating/slotted"; -export * from "./templating/children"; +} from "./templating/html-directive.js"; +export * from "./templating/ref.js"; +export * from "./templating/when.js"; +export * from "./templating/repeat.js"; +export * from "./templating/slotted.js"; +export * from "./templating/children.js"; +export * from "./templating/view.js"; export { elements, ElementsFilter, NodeBehaviorOptions, -} from "./templating/node-observation"; +} from "./templating/node-observation.js"; diff --git a/packages/web-components/fast-element/src/observation/observable.ts b/packages/web-components/fast-element/src/observation/observable.ts index c243f78262b..e4e10c9aea6 100644 --- a/packages/web-components/fast-element/src/observation/observable.ts +++ b/packages/web-components/fast-element/src/observation/observable.ts @@ -1,7 +1,7 @@ -import { DOM } from "../dom"; -import { PropertyChangeNotifier, SubscriberSet } from "./notifier"; -import type { Notifier, Subscriber } from "./notifier"; -import { isFunction, isString } from "../interfaces"; +import { DOM } from "../dom.js"; +import { isFunction, isString } from "../interfaces.js"; +import { PropertyChangeNotifier, SubscriberSet } from "./notifier.js"; +import type { Notifier, Subscriber } from "./notifier.js"; const volatileRegex = /(:|&&|\|\||if)/; const notifierLookup = new WeakMap(); diff --git a/packages/web-components/fast-element/src/styles/css.ts b/packages/web-components/fast-element/src/styles/css.ts index dc94edfb349..195741cd5dc 100644 --- a/packages/web-components/fast-element/src/styles/css.ts +++ b/packages/web-components/fast-element/src/styles/css.ts @@ -1,8 +1,8 @@ -import type { FASTElement } from "../components/fast-element"; -import { isString } from "../interfaces"; -import type { Behavior } from "../observation/behavior"; -import { CSSDirective } from "./css-directive"; -import { ComposableStyles, ElementStyles } from "./element-styles"; +import type { FASTElement } from "../components/fast-element.js"; +import { isString } from "../interfaces.js"; +import type { Behavior } from "../observation/behavior.js"; +import { CSSDirective } from "./css-directive.js"; +import { ComposableStyles, ElementStyles } from "./element-styles.js"; function collectStyles( strings: TemplateStringsArray, diff --git a/packages/web-components/fast-element/src/styles/element-styles.ts b/packages/web-components/fast-element/src/styles/element-styles.ts index 57dc196b334..b8dfed97f22 100644 --- a/packages/web-components/fast-element/src/styles/element-styles.ts +++ b/packages/web-components/fast-element/src/styles/element-styles.ts @@ -1,5 +1,5 @@ -import type { Behavior } from "../observation/behavior"; -import { DOM, nextId } from "../dom"; +import type { Behavior } from "../observation/behavior.js"; +import { DOM, nextId } from "../dom.js"; /** * A node that can be targeted by styles. diff --git a/packages/web-components/fast-element/src/templating/binding.ts b/packages/web-components/fast-element/src/templating/binding.ts index 28d75cf4df6..03af099c526 100644 --- a/packages/web-components/fast-element/src/templating/binding.ts +++ b/packages/web-components/fast-element/src/templating/binding.ts @@ -1,19 +1,19 @@ -import { DOM } from "../dom"; -import { isString, Mutable } from "../interfaces"; +import { DOM } from "../dom.js"; +import { isString, Mutable } from "../interfaces.js"; import { Binding, BindingObserver, ExecutionContext, Observable, setCurrentEvent, -} from "../observation/observable"; +} from "../observation/observable.js"; import { InlinableHTMLDirective, ViewBehavior, ViewBehaviorTargets, -} from "./html-directive"; -import type { CaptureType } from "./template"; -import type { SyntheticView } from "./view"; +} from "./html-directive.js"; +import type { CaptureType } from "./template.js"; +import type { SyntheticView } from "./view.js"; export type BindingBehaviorFactory = { createBehavior(targets: ViewBehaviorTargets): ViewBehavior; diff --git a/packages/web-components/fast-element/src/templating/children.ts b/packages/web-components/fast-element/src/templating/children.ts index 85419d018eb..9ba2a32c9b0 100644 --- a/packages/web-components/fast-element/src/templating/children.ts +++ b/packages/web-components/fast-element/src/templating/children.ts @@ -1,6 +1,6 @@ -import { isString } from "../interfaces"; -import { NodeObservationDirective, NodeBehaviorOptions } from "./node-observation"; -import type { CaptureType } from "./template"; +import { isString } from "../interfaces.js"; +import { NodeBehaviorOptions, NodeObservationDirective } from "./node-observation.js"; +import type { CaptureType } from "./template.js"; /** * The options used to configure child list observation. diff --git a/packages/web-components/fast-element/src/templating/compiler.ts b/packages/web-components/fast-element/src/templating/compiler.ts index 8fe9274aa2f..211284ac28a 100644 --- a/packages/web-components/fast-element/src/templating/compiler.ts +++ b/packages/web-components/fast-element/src/templating/compiler.ts @@ -1,14 +1,14 @@ +import { _interpolationEnd, _interpolationStart, DOM } from "../dom.js"; +import { isString } from "../interfaces.js"; +import type { ExecutionContext } from "../observation/observable.js"; +import { bind, oneTime } from "./binding.js"; import type { AspectedHTMLDirective, HTMLDirective, InlinableHTMLDirective, ViewBehaviorFactory, ViewBehaviorTargets, -} from "./html-directive"; -import { _interpolationEnd, _interpolationStart, DOM } from "../dom"; -import type { ExecutionContext } from "../observation/observable"; -import { bind, oneTime } from "./binding"; -import { isString } from "../interfaces"; +} from "./html-directive.js"; const targetIdFrom = (parentId: string, nodeIndex: number) => `${parentId}.${nodeIndex}`; const descriptorCache: PropertyDescriptorMap = {}; 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 d7cc532c76c..47071acc2d3 100644 --- a/packages/web-components/fast-element/src/templating/html-directive.ts +++ b/packages/web-components/fast-element/src/templating/html-directive.ts @@ -1,6 +1,6 @@ -import { DOM, nextId } from "../dom"; -import type { Behavior } from "../observation/behavior"; -import type { Binding, ExecutionContext } from "../observation/observable"; +import { DOM, nextId } from "../dom.js"; +import type { Behavior } from "../observation/behavior.js"; +import type { Binding, ExecutionContext } from "../observation/observable.js"; /** * The target nodes available to a behavior. diff --git a/packages/web-components/fast-element/src/templating/node-observation.ts b/packages/web-components/fast-element/src/templating/node-observation.ts index 489b3e6569c..0b4e5ee6ef3 100644 --- a/packages/web-components/fast-element/src/templating/node-observation.ts +++ b/packages/web-components/fast-element/src/templating/node-observation.ts @@ -1,9 +1,9 @@ -import type { ExecutionContext } from "../observation/observable"; -import { emptyArray } from "../platform"; +import type { ExecutionContext } from "../observation/observable.js"; +import { emptyArray } from "../platform.js"; import { StatelessAttachedAttributeDirective, ViewBehaviorTargets, -} from "./html-directive"; +} from "./html-directive.js"; /** * Options for configuring node observation behavior. diff --git a/packages/web-components/fast-element/src/templating/ref.ts b/packages/web-components/fast-element/src/templating/ref.ts index fb4d47a795e..af0b647077a 100644 --- a/packages/web-components/fast-element/src/templating/ref.ts +++ b/packages/web-components/fast-element/src/templating/ref.ts @@ -1,9 +1,9 @@ -import type { CaptureType } from "./template"; +import type { ExecutionContext } from "../observation/observable.js"; import { StatelessAttachedAttributeDirective, ViewBehaviorTargets, -} from "./html-directive"; -import type { ExecutionContext } from "../observation/observable"; +} from "./html-directive.js"; +import type { CaptureType } from "./template.js"; /** * The runtime behavior for template references. diff --git a/packages/web-components/fast-element/src/templating/repeat.ts b/packages/web-components/fast-element/src/templating/repeat.ts index c1748ce96f2..fce7b46d902 100644 --- a/packages/web-components/fast-element/src/templating/repeat.ts +++ b/packages/web-components/fast-element/src/templating/repeat.ts @@ -1,19 +1,19 @@ -import { DOM } from "../dom"; +import { DOM } from "../dom.js"; +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 { Binding, BindingObserver, ExecutionContext, Observable, -} from "../observation/observable"; -import type { Notifier, Subscriber } from "../observation/notifier"; -import { enableArrayObservation } from "../observation/array-observer"; -import type { Splice } from "../observation/array-change-records"; -import type { Behavior } from "../observation/behavior"; -import { emptyArray } from "../platform"; -import { ViewBehaviorTargets, HTMLDirective } from "./html-directive"; -import { HTMLView, SyntheticView } from "./view"; -import type { CaptureType, SyntheticViewTemplate } from "./template"; -import { isFunction } from "../interfaces"; +} from "../observation/observable.js"; +import { emptyArray } from "../platform.js"; +import { HTMLDirective, ViewBehaviorTargets } from "./html-directive.js"; +import type { CaptureType, SyntheticViewTemplate } from "./template.js"; +import { HTMLView, SyntheticView } from "./view.js"; /** * Options for configuring repeat behavior. diff --git a/packages/web-components/fast-element/src/templating/slotted.ts b/packages/web-components/fast-element/src/templating/slotted.ts index c65e4491c5d..d34b334b1ec 100644 --- a/packages/web-components/fast-element/src/templating/slotted.ts +++ b/packages/web-components/fast-element/src/templating/slotted.ts @@ -1,6 +1,6 @@ -import { isString } from "../interfaces"; -import { NodeObservationDirective, NodeBehaviorOptions } from "./node-observation"; -import type { CaptureType } from "./template"; +import { isString } from "../interfaces.js"; +import { NodeBehaviorOptions, NodeObservationDirective } from "./node-observation.js"; +import type { CaptureType } from "./template.js"; /** * The options used to configure slotted node observation. diff --git a/packages/web-components/fast-element/src/templating/template.ts b/packages/web-components/fast-element/src/templating/template.ts index 97ab51fefad..4d9da0160c1 100644 --- a/packages/web-components/fast-element/src/templating/template.ts +++ b/packages/web-components/fast-element/src/templating/template.ts @@ -1,11 +1,11 @@ -import { DOM } from "../dom"; -import { Binding, defaultExecutionContext } from "../observation/observable"; -import { compileTemplate } from "./compiler"; -import type { HTMLTemplateCompilationResult } from "./compiler"; -import { ElementView, HTMLView, SyntheticView } from "./view"; -import { HTMLDirective, AspectedHTMLDirective } from "./html-directive"; -import { bind, oneTime } from "./binding"; -import { isFunction, isString } from "../interfaces"; +import { DOM } from "../dom.js"; +import { isFunction, isString } from "../interfaces.js"; +import { Binding, defaultExecutionContext } from "../observation/observable.js"; +import { bind, oneTime } from "./binding.js"; +import { compileTemplate } from "./compiler.js"; +import type { HTMLTemplateCompilationResult } from "./compiler.js"; +import { AspectedHTMLDirective, HTMLDirective } from "./html-directive.js"; +import { ElementView, HTMLView, SyntheticView } from "./view.js"; /** * A template capable of creating views specifically for rendering custom elements. diff --git a/packages/web-components/fast-element/src/templating/view.ts b/packages/web-components/fast-element/src/templating/view.ts index 36eca6e8cf0..135cee099af 100644 --- a/packages/web-components/fast-element/src/templating/view.ts +++ b/packages/web-components/fast-element/src/templating/view.ts @@ -1,10 +1,10 @@ -import type { Behavior } from "../observation/behavior"; -import type { ExecutionContext } from "../observation/observable"; +import type { Behavior } from "../observation/behavior.js"; +import type { ExecutionContext } from "../observation/observable.js"; import type { - ViewBehaviorTargets, - ViewBehaviorFactory, ViewBehavior, -} from "./html-directive"; + ViewBehaviorFactory, + ViewBehaviorTargets, +} from "./html-directive.js"; /** * Represents a collection of DOM nodes which can be bound to a data source. diff --git a/packages/web-components/fast-element/src/templating/when.ts b/packages/web-components/fast-element/src/templating/when.ts index b2a5cdca59a..bd2c1ac3fcc 100644 --- a/packages/web-components/fast-element/src/templating/when.ts +++ b/packages/web-components/fast-element/src/templating/when.ts @@ -1,6 +1,6 @@ -import { isFunction } from "../interfaces"; -import type { Binding, ExecutionContext } from "../observation/observable"; -import type { CaptureType, SyntheticViewTemplate } from "./template"; +import { isFunction } from "../interfaces.js"; +import type { Binding, ExecutionContext } from "../observation/observable.js"; +import type { CaptureType, SyntheticViewTemplate } from "./template.js"; /** * A directive that enables basic conditional rendering in a template. From 7d63f3201502787f24baa1994bccc23f7050581f Mon Sep 17 00:00:00 2001 From: Jane Chu <7559015+janechu@users.noreply.github.com> Date: Tue, 15 Feb 2022 13:13:51 -0800 Subject: [PATCH 055/135] Add the fast-style web component (#5600) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Pull Request ## 📖 Description This branch adds the following: - A FAST Style element that will hydrate styles client side - A webpack config that will build the minimal JS for the logic for this web component so that the file `fast-style.min.js` can be referred to in server side generated HTML ### 🎫 Issues Closes #5575 ## 👩‍💻 Reviewer Notes If you run the test server after building all the necessary files, you can navigate to `localhost:8080/fast-style` and see the component working there. One thing is the fixture uses `shadowroot="open"` to be able to see which styles are applied to nested elements during Playwright testing. This does not need the case for actual implementation, the shadowroot can be closed, this has been manually verified. ## 📑 Test Plan The added tests are Playwright tests, I'll let reviewers comment as to whether more tests are needed, this however tests the base case of applying styles to the container of the fast-style web component. ## ✅ Checklist ### General - [ ] I have included a change request file using `$ yarn change` - [x] I have added tests for my changes. - [x] I have tested my changes. - [ ] I have updated the project documentation to reflect my changes. - [ ] I have read the [CONTRIBUTING](https://github.com/Microsoft/fast/blob/master/CONTRIBUTING.md) documentation and followed the [standards](https://www.fast.design/docs/community/code-of-conduct/#our-standards) for this project. ## ⏭ Next Steps - Integrate the FASTStyle wc to the HTML renderer #5576 --- .../web-components/fast-ssr/server/server.ts | 47 ++ .../src/fast-style/index.fixture.html | 436 ++++++++++++++++++ .../fast-ssr/src/fast-style/index.ts | 79 ++++ .../fast-ssr/test/fast-style.spec.ts | 62 +++ 4 files changed, 624 insertions(+) create mode 100644 packages/web-components/fast-ssr/src/fast-style/index.fixture.html create mode 100644 packages/web-components/fast-ssr/src/fast-style/index.ts create mode 100644 packages/web-components/fast-ssr/test/fast-style.spec.ts diff --git a/packages/web-components/fast-ssr/server/server.ts b/packages/web-components/fast-ssr/server/server.ts index e47e01fad16..866fa10b5e8 100644 --- a/packages/web-components/fast-ssr/server/server.ts +++ b/packages/web-components/fast-ssr/server/server.ts @@ -1,6 +1,9 @@ import { Readable } from "stream"; import express, { Request, Response } from "express"; +import fs from "fs"; +import path from "path"; +const __dirname = path.resolve(path.dirname("")); const PORT = 8080; function handleRequest(req: Request, res: Response) { res.set("Content-Type", "text/html"); @@ -20,6 +23,50 @@ function handleRequest(req: Request, res: Response) { }); } +function handleStyleRequest(req: Request, res: Response) { + res.set("Content-Type", "text/html"); + fs.readFile( + path.resolve(__dirname, "./src/fast-style/index.fixture.html"), + { encoding: "utf8" }, + (err, data) => { + const stream = (Readable as any).from(data); + stream.on("readable", function (this: any) { + while ((data = this.read())) { + res.write(data); + } + }); + stream.on("close", () => res.end()); + stream.on("error", (e: Error) => { + console.error(e); + process.exit(1); + }); + } + ); +} + +function handleStyleScriptRequest(req: Request, res: Response) { + res.set("Content-Type", "application/javascript"); + fs.readFile( + path.resolve(__dirname, "./dist/fast-style/index.js"), + { encoding: "utf8" }, + (err, data) => { + const stream = (Readable as any).from(data); + stream.on("readable", function (this: any) { + while ((data = this.read())) { + res.write(data); + } + }); + stream.on("close", () => res.end()); + stream.on("error", (e: Error) => { + console.error(e); + process.exit(1); + }); + } + ); +} + const app = express(); app.get("/", handleRequest); +app.get("/fast-style", handleStyleRequest); +app.get("/fast-style.js", handleStyleScriptRequest); app.listen(PORT); diff --git a/packages/web-components/fast-ssr/src/fast-style/index.fixture.html b/packages/web-components/fast-ssr/src/fast-style/index.fixture.html new file mode 100644 index 00000000000..92820fa5ab9 --- /dev/null +++ b/packages/web-components/fast-ssr/src/fast-style/index.fixture.html @@ -0,0 +1,436 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/web-components/fast-ssr/src/fast-style/index.ts b/packages/web-components/fast-ssr/src/fast-style/index.ts new file mode 100644 index 00000000000..cb480301651 --- /dev/null +++ b/packages/web-components/fast-ssr/src/fast-style/index.ts @@ -0,0 +1,79 @@ +interface StyleCache { + [key: string]: CSSStyleSheet | string; +} + +/** + * The FASTStyle web component that takes attributes: + * - css - a string version of the CSS to be applied to the parent web component, this can be omitted if the data-style-id has been used in the DOM as the cached value will be fetched + * - data-style-id - a dataset attribute used as an identifier for the CSS attr string + */ +export default class FASTStyle extends HTMLElement { + private static cache: StyleCache = {}; + private static hashIdDataSetName: string = "data-style-id"; + private static supportsAdoptedStyleSheets: boolean = + Array.isArray((document as any).adoptedStyleSheets) && + "replace" in CSSStyleSheet.prototype; + + /** + * @internal + */ + public connectedCallback(): void { + const hashId: string = this.getAttribute(FASTStyle.hashIdDataSetName) as string; + const css: string = this.getAttribute("css") as string; + this.registerStyles(hashId, css); + } + + /** + * Register styles if they are not part of the cache and attach them + */ + private registerStyles = (hashId: string, css: string): void => { + if (FASTStyle.supportsAdoptedStyleSheets) { + if (!(hashId in FASTStyle.cache)) { + this.memoizeAdoptedStylesheetStyles(hashId, css); + } + this.attachAdoptedStylesheetStyles(this.parentNode as ShadowRoot, hashId); + } else { + if (!(hashId in FASTStyle.cache)) { + this.memoizeStyleElementStyles(hashId, css); + } + this.attachStyleElementStyles(this.parentNode as ShadowRoot, hashId); + } + }; + + /** + * Memoize CSSStyleSheets + */ + private memoizeAdoptedStylesheetStyles(hashId: string, css: string) { + const sheet = new CSSStyleSheet(); + (sheet as any).replaceSync(css); + FASTStyle.cache[hashId] = sheet; + } + + /** + * Attach CSSStyleSheets + */ + private attachAdoptedStylesheetStyles(shadowRoot: ShadowRoot, hashId: string) { + (shadowRoot as any).adoptedStyleSheets = [ + ...(shadowRoot as any).adoptedStyleSheets!, + FASTStyle.cache[hashId] as CSSStyleSheet, + ]; + } + + /** + * Memoize css strings + */ + private memoizeStyleElementStyles(hashId: string, css: string) { + FASTStyle.cache[hashId] = css; + } + + /** + * Attach style elements + */ + private attachStyleElementStyles(shadowRoot: ShadowRoot, hashId: string) { + const element = document.createElement("style"); + element.innerHTML = FASTStyle.cache[hashId] as string; + shadowRoot.append(element); + } +} + +customElements.define("fast-style", FASTStyle); diff --git a/packages/web-components/fast-ssr/test/fast-style.spec.ts b/packages/web-components/fast-ssr/test/fast-style.spec.ts new file mode 100644 index 00000000000..08f74b18e39 --- /dev/null +++ b/packages/web-components/fast-ssr/test/fast-style.spec.ts @@ -0,0 +1,62 @@ +import { expect, test } from "@playwright/test" + +test("Check that the first element has styles assigned", async ({ page }) => { + await page.goto("/fast-style"); + + const cards = page.locator("fast-card"); + const styles = await cards.evaluateAll((cardList) => { + return cardList.map((card) => { + return window.getComputedStyle(card, null).getPropertyValue("background-color"); + }); + }); + + expect(styles[0]).toEqual("rgb(26, 26, 26)"); +}); +test("Check that the nested element in the first element has styles assigned", async ({ page }) => { + await page.goto("/fast-style"); + + const cards = page.locator("fast-card"); + const styles = await cards.evaluateAll((cardList) => { + return cardList.map((card) => { + return window.getComputedStyle( + (card.shadowRoot?.querySelector("fast-button") as Element), null + ).getPropertyValue("background-color"); + }); + }); + + expect(styles[0]).toEqual("rgb(43, 43, 43)"); +}); +test("Check that all elements have styles assigned", async ({ page }) => { + await page.goto("/fast-style"); + + const cards = page.locator("fast-card"); + const styles = await cards.evaluateAll((cardList) => { + return cardList.map((card) => { + return window.getComputedStyle(card, null).getPropertyValue("background-color"); + }); + }); + + expect(styles).toHaveLength(10); + + styles.forEach((style) => { + expect(style).toEqual("rgb(26, 26, 26)"); + }); +}); +test("Check that all nested elements have styles assigned", async ({ page}) => { + await page.goto("/fast-style"); + + const cards = page.locator("fast-card"); + const styles = await cards.evaluateAll((cardList) => { + return cardList.map((card) => { + return window.getComputedStyle( + (card.shadowRoot?.querySelector("fast-button") as Element), null + ).getPropertyValue("background-color"); + }); + }); + + expect(styles).toHaveLength(10); + + styles.forEach((style) => { + expect(style).toEqual("rgb(43, 43, 43)"); + }); +}); From ed63ade378d9a542e000a5b202647d1f977dcfdb Mon Sep 17 00:00:00 2001 From: Jane Chu <7559015+janechu@users.noreply.github.com> Date: Wed, 16 Feb 2022 14:17:27 -0800 Subject: [PATCH 056/135] Pin the checkchange to the feature branch (#5611) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Pull Request ## 📖 Description This change will pin the `checkchange` check in the github workflow to check for changes against the `features/fast-element-2` branch. Since the branch should only be checking PRs against its contents and not the default branch this is the current workaround for checking for changefiles against non-default branches. This change must be removed prior to inclusion to the default branch. ## ✅ Checklist ### General - [ ] I have included a change request file using `$ yarn change` - [ ] I have added tests for my changes. - [x] I have tested my changes. - [ ] I have updated the project documentation to reflect my changes. - [x] I have read the [CONTRIBUTING](https://github.com/Microsoft/fast/blob/master/CONTRIBUTING.md) documentation and followed the [standards](https://www.fast.design/docs/community/code-of-conduct/#our-standards) for this project. ## ⏭ Next Steps - Add an issue to remove this change in the feature branch tracking project (https://github.com/microsoft/fast/issues/5612) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d73c55edd5c..345133ced39 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "scripts": { "bump": "beachball bump", "change": "beachball change", - "checkchange": "beachball check --scope \"!sites/*\" --changehint \"Run 'yarn change' to generate a change file\"", + "checkchange": "beachball check --scope \"!sites/*\" --changehint \"Run 'yarn change' to generate a change file\" -b origin/features/fast-element-2", "check": "beachball check ", "publish": "beachball publish", "publish-ci": "beachball publish -y --access public", From 37da7a77e26caa375df7ae007629d022ad8b9e13 Mon Sep 17 00:00:00 2001 From: Jane Chu <7559015+janechu@users.noreply.github.com> Date: Wed, 16 Feb 2022 14:19:41 -0800 Subject: [PATCH 057/135] Added change files for FAST Element 2 (#5610) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Pull Request ## 📖 Description These are the necessary change files for the current state of work for FAST Element 2.0. Subsequent changes will require a change file after a PR has been added to pin the `checkchange` to the feature branch. ## 👩‍💻 Reviewer Notes Read the change files carefully and see if you agree with the wording/change type/package/etc. ## ✅ Checklist ### General - [x] I have included a change request file using `$ yarn change` - [ ] I have added tests for my changes. - [ ] I have tested my changes. - [ ] I have updated the project documentation to reflect my changes. - [x] I have read the [CONTRIBUTING](https://github.com/Microsoft/fast/blob/master/CONTRIBUTING.md) documentation and followed the [standards](https://www.fast.design/docs/community/code-of-conduct/#our-standards) for this project. ## ⏭ Next Steps - Add a PR to pin `checkchange` to the feature branch (#5611) - Add an issue to remove `checkchange` pinning once feature branch work is complete (https://github.com/microsoft/fast/issues/5612) --- ...-fast-element-08318f12-b72b-4832-b3d2-093ab5a7869a.json | 7 +++++++ ...-fast-element-08318f12-b72b-4832-b3d2-093ab5a7869b.json | 7 +++++++ ...-fast-element-08318f12-b72b-4832-b3d2-093ab5a7869c.json | 7 +++++++ ...-fast-element-08318f12-b72b-4832-b3d2-093ab5a7869e.json | 7 +++++++ ...-fast-element-08318f12-b72b-4832-b3d2-093ab5a7869f.json | 7 +++++++ ...-fast-element-08318f12-b72b-4832-b3d2-093ab5a7869g.json | 7 +++++++ ...-fast-element-08318f12-b72b-4832-b3d2-093ab5a7869h.json | 7 +++++++ ...-fast-element-08318f12-b72b-4832-b3d2-093ab5a7869i.json | 7 +++++++ ...-fast-element-08318f12-b72b-4832-b3d2-093ab5a7869j.json | 7 +++++++ ...-fast-element-422bda9e-1e66-46b2-a665-ca4e8a685cbc.json | 7 +++++++ ...-fast-element-422bda9e-1e66-46b2-a665-ca4e8a685cbd.json | 7 +++++++ 11 files changed, 77 insertions(+) create mode 100644 change/@microsoft-fast-element-08318f12-b72b-4832-b3d2-093ab5a7869a.json create mode 100644 change/@microsoft-fast-element-08318f12-b72b-4832-b3d2-093ab5a7869b.json create mode 100644 change/@microsoft-fast-element-08318f12-b72b-4832-b3d2-093ab5a7869c.json create mode 100644 change/@microsoft-fast-element-08318f12-b72b-4832-b3d2-093ab5a7869e.json create mode 100644 change/@microsoft-fast-element-08318f12-b72b-4832-b3d2-093ab5a7869f.json create mode 100644 change/@microsoft-fast-element-08318f12-b72b-4832-b3d2-093ab5a7869g.json create mode 100644 change/@microsoft-fast-element-08318f12-b72b-4832-b3d2-093ab5a7869h.json create mode 100644 change/@microsoft-fast-element-08318f12-b72b-4832-b3d2-093ab5a7869i.json create mode 100644 change/@microsoft-fast-element-08318f12-b72b-4832-b3d2-093ab5a7869j.json create mode 100644 change/@microsoft-fast-element-422bda9e-1e66-46b2-a665-ca4e8a685cbc.json create mode 100644 change/@microsoft-fast-element-422bda9e-1e66-46b2-a665-ca4e8a685cbd.json diff --git a/change/@microsoft-fast-element-08318f12-b72b-4832-b3d2-093ab5a7869a.json b/change/@microsoft-fast-element-08318f12-b72b-4832-b3d2-093ab5a7869a.json new file mode 100644 index 00000000000..7bf0ffe70c9 --- /dev/null +++ b/change/@microsoft-fast-element-08318f12-b72b-4832-b3d2-093ab5a7869a.json @@ -0,0 +1,7 @@ +{ + "type": "major", + "comment": "`View` and `HTMLView` - Type parameters added to enable strongly typed views based on their data source. The constructor of `HTMLView` has a new signature based on changes to the compiler's output. Internals have been cleaned up and no longer rely on the Range type.", + "packageName": "@microsoft/fast-element", + "email": "roeisenb@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@microsoft-fast-element-08318f12-b72b-4832-b3d2-093ab5a7869b.json b/change/@microsoft-fast-element-08318f12-b72b-4832-b3d2-093ab5a7869b.json new file mode 100644 index 00000000000..34ada7b73be --- /dev/null +++ b/change/@microsoft-fast-element-08318f12-b72b-4832-b3d2-093ab5a7869b.json @@ -0,0 +1,7 @@ +{ + "type": "major", + "comment": "`ElementViewTemplate`, `SyntheticViewTemplate`, and `ViewTemplate` - Added type parameters throughout. Logic to instantiate and apply behaviors moved out of the template and into the view where it can be lazily executed. Removed the ability of the `render` method to take a string id of the node to render to. You must provide a node.", + "packageName": "@microsoft/fast-element", + "email": "roeisenb@microsoft.com", + "dependentChangeType": "patch" +} \ No newline at end of file diff --git a/change/@microsoft-fast-element-08318f12-b72b-4832-b3d2-093ab5a7869c.json b/change/@microsoft-fast-element-08318f12-b72b-4832-b3d2-093ab5a7869c.json new file mode 100644 index 00000000000..74a147fe307 --- /dev/null +++ b/change/@microsoft-fast-element-08318f12-b72b-4832-b3d2-093ab5a7869c.json @@ -0,0 +1,7 @@ +{ + "type": "major", + "comment": "`DOM` - Tree Walker methods are no longer used and are thus removed. The API for removing child nodes has been removed as well since it was only used in one place and could be inlined. The helper `createCustomAttributePlaceholder()` no longer requires an attribute name. It will be uniquely generated internally.", + "packageName": "@microsoft/fast-element", + "email": "roeisenb@microsoft.com", + "dependentChangeType": "patch" +} \ No newline at end of file diff --git a/change/@microsoft-fast-element-08318f12-b72b-4832-b3d2-093ab5a7869e.json b/change/@microsoft-fast-element-08318f12-b72b-4832-b3d2-093ab5a7869e.json new file mode 100644 index 00000000000..a83663e4297 --- /dev/null +++ b/change/@microsoft-fast-element-08318f12-b72b-4832-b3d2-093ab5a7869e.json @@ -0,0 +1,7 @@ +{ + "type": "major", + "comment": "`class` - Bindings to `class` are now more nuanced. Binding directly to `class` will simply set the `className` property. If you need to bind to `class` knowing that manual JS will also manipulate the `classList` in addition to the binding, then you should now bind to `:classList` instead. This allows for performance optimizations in the simple, most common case.", + "packageName": "@microsoft/fast-element", + "email": "roeisenb@microsoft.com", + "dependentChangeType": "patch" +} \ No newline at end of file diff --git a/change/@microsoft-fast-element-08318f12-b72b-4832-b3d2-093ab5a7869f.json b/change/@microsoft-fast-element-08318f12-b72b-4832-b3d2-093ab5a7869f.json new file mode 100644 index 00000000000..d74aeaf826b --- /dev/null +++ b/change/@microsoft-fast-element-08318f12-b72b-4832-b3d2-093ab5a7869f.json @@ -0,0 +1,7 @@ +{ + "type": "major", + "comment": "`Behavior` and `ViewBehavior` - `Behavior` now requires an `ExecutionContext` for `unbind`. Behaviors can be used for elements or views. `ViewBehavior` has been introduced for use exclusively with views, and provides some optimization opportunities.", + "packageName": "@microsoft/fast-element", + "email": "roeisenb@microsoft.com", + "dependentChangeType": "patch" +} \ No newline at end of file diff --git a/change/@microsoft-fast-element-08318f12-b72b-4832-b3d2-093ab5a7869g.json b/change/@microsoft-fast-element-08318f12-b72b-4832-b3d2-093ab5a7869g.json new file mode 100644 index 00000000000..4dc8866a841 --- /dev/null +++ b/change/@microsoft-fast-element-08318f12-b72b-4832-b3d2-093ab5a7869g.json @@ -0,0 +1,7 @@ +{ + "type": "major", + "comment": "`RefBehavior` has been replaced with `RefDirective`. The directive also implements `ViewBehavior` allowing a single directive instance to be shared across all template instances that use the ref.", + "packageName": "@microsoft/fast-element", + "email": "roeisenb@microsoft.com", + "dependentChangeType": "patch" +} \ No newline at end of file diff --git a/change/@microsoft-fast-element-08318f12-b72b-4832-b3d2-093ab5a7869h.json b/change/@microsoft-fast-element-08318f12-b72b-4832-b3d2-093ab5a7869h.json new file mode 100644 index 00000000000..1e8f21753ee --- /dev/null +++ b/change/@microsoft-fast-element-08318f12-b72b-4832-b3d2-093ab5a7869h.json @@ -0,0 +1,7 @@ +{ + "type": "major", + "comment": "Removed `SlottedBehavior` and `ChildrenBehavior` have been replaced with `SlottedDirective` and `ChildrenDirective`. These directives allow a single directive instance to be shared across all template instances that use the ref.", + "packageName": "@microsoft/fast-element", + "email": "roeisenb@microsoft.com", + "dependentChangeType": "patch" +} \ No newline at end of file diff --git a/change/@microsoft-fast-element-08318f12-b72b-4832-b3d2-093ab5a7869i.json b/change/@microsoft-fast-element-08318f12-b72b-4832-b3d2-093ab5a7869i.json new file mode 100644 index 00000000000..54067b1c944 --- /dev/null +++ b/change/@microsoft-fast-element-08318f12-b72b-4832-b3d2-093ab5a7869i.json @@ -0,0 +1,7 @@ +{ + "type": "major", + "comment": "Removed `AttachedBehaviorHTMLDirective` and `AttachedBehaviorType` since they are no longer used in the new directive/behavior architecture for ref, slotted, and children.", + "packageName": "@microsoft/fast-element", + "email": "roeisenb@microsoft.com", + "dependentChangeType": "patch" +} \ No newline at end of file diff --git a/change/@microsoft-fast-element-08318f12-b72b-4832-b3d2-093ab5a7869j.json b/change/@microsoft-fast-element-08318f12-b72b-4832-b3d2-093ab5a7869j.json new file mode 100644 index 00000000000..711102a95c7 --- /dev/null +++ b/change/@microsoft-fast-element-08318f12-b72b-4832-b3d2-093ab5a7869j.json @@ -0,0 +1,7 @@ +{ + "type": "major", + "comment": "Renamed `Notifier#source` to `Notifier#subject` to align with other observable terminology and prevent name clashes with `BindingObserver` properties.", + "packageName": "@microsoft/fast-element", + "email": "roeisenb@microsoft.com", + "dependentChangeType": "patch" +} \ No newline at end of file diff --git a/change/@microsoft-fast-element-422bda9e-1e66-46b2-a665-ca4e8a685cbc.json b/change/@microsoft-fast-element-422bda9e-1e66-46b2-a665-ca4e8a685cbc.json new file mode 100644 index 00000000000..34b693b231f --- /dev/null +++ b/change/@microsoft-fast-element-422bda9e-1e66-46b2-a665-ca4e8a685cbc.json @@ -0,0 +1,7 @@ +{ + "type": "major", + "comment": "`HTMLDirective` - The `targetIndex: number` property has been replaced by a `targetId: string` property. The `createBehavior` method no longer takes a target `Node` but instead takes a `BehaviorTargets` instance. The actual target can be looked up on the `BehaviorTargets` instance by indexing with the `targetId` property.", + "packageName": "@microsoft/fast-element", + "email": "roeisenb@microsoft.com", + "dependentChangeType": "patch" +} \ No newline at end of file diff --git a/change/@microsoft-fast-element-422bda9e-1e66-46b2-a665-ca4e8a685cbd.json b/change/@microsoft-fast-element-422bda9e-1e66-46b2-a665-ca4e8a685cbd.json new file mode 100644 index 00000000000..7ca02e25ea9 --- /dev/null +++ b/change/@microsoft-fast-element-422bda9e-1e66-46b2-a665-ca4e8a685cbd.json @@ -0,0 +1,7 @@ +{ + "type": "major", + "comment": "`compileTemplate()` - Internals have been significantly changed. The implementation no longer uses a TreeWalker. The return type has change to an `HTMLTemplateCompilationResult` with different properties.", + "packageName": "@microsoft/fast-element", + "email": "roeisenb@microsoft.com", + "dependentChangeType": "patch" +} \ No newline at end of file From 2936dcff62b1b81ddd5d7d8bb546ef6a77910bbc Mon Sep 17 00:00:00 2001 From: Jane Chu <7559015+janechu@users.noreply.github.com> Date: Thu, 17 Feb 2022 08:26:27 -0800 Subject: [PATCH 058/135] Add branch pointing to config instead of package scripts (#5616) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Pull Request ## 📖 Description This change moves the branch specificity to beachball config to allow `yarn change` to target the feature branch and also removes the restriction on "major" change updates. ## ✅ Checklist ### General - [ ] I have included a change request file using `$ yarn change` - [ ] I have added tests for my changes. - [x] I have tested my changes. - [ ] I have updated the project documentation to reflect my changes. - [x] I have read the [CONTRIBUTING](https://github.com/Microsoft/fast/blob/master/CONTRIBUTING.md) documentation and followed the [standards](https://www.fast.design/docs/community/code-of-conduct/#our-standards) for this project. --- beachball.config.js | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/beachball.config.js b/beachball.config.js index 5d2838343b0..220c756cc6f 100644 --- a/beachball.config.js +++ b/beachball.config.js @@ -1,5 +1,5 @@ module.exports = { - disallowedChangeTypes: ["major"], + branch: "origin/features/fast-element-2", ignorePatterns: [ ".ignore", ".github/", diff --git a/package.json b/package.json index 345133ced39..d73c55edd5c 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "scripts": { "bump": "beachball bump", "change": "beachball change", - "checkchange": "beachball check --scope \"!sites/*\" --changehint \"Run 'yarn change' to generate a change file\" -b origin/features/fast-element-2", + "checkchange": "beachball check --scope \"!sites/*\" --changehint \"Run 'yarn change' to generate a change file\"", "check": "beachball check ", "publish": "beachball publish", "publish-ci": "beachball publish -y --access public", From 34f17e3fb4d7b7c9af51721aa661e8986758438c Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Thu, 17 Feb 2022 13:07:13 -0500 Subject: [PATCH 059/135] feat: enable synchronous dom updates for SSR (#5615) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: enable synchronous dom updates for SSR * Add branch pointing to config instead of package scripts (#5616) # Pull Request ## 📖 Description This change moves the branch specificity to beachball config to allow `yarn change` to target the feature branch and also removes the restriction on "major" change updates. ## ✅ Checklist ### General - [ ] I have included a change request file using `$ yarn change` - [ ] I have added tests for my changes. - [x] I have tested my changes. - [ ] I have updated the project documentation to reflect my changes. - [x] I have read the [CONTRIBUTING](https://github.com/Microsoft/fast/blob/master/CONTRIBUTING.md) documentation and followed the [standards](https://www.fast.design/docs/community/code-of-conduct/#our-standards) for this project. * feat: enable synchronous dom updates for SSR * Change files * Change files Co-authored-by: Jane Chu <7559015+janechu@users.noreply.github.com> Co-authored-by: EisenbergEffect --- ...-ecbe6511-b19c-4312-91f2-1c354ebd5245.json | 7 ++ ...-08a4dedc-a404-44ca-ac6a-fe3b4b22a2a1.json | 7 ++ .../fast-element/docs/api-report.md | 1 + .../fast-element/src/dom.spec.ts | 98 ++++++++++++++++++- .../web-components/fast-element/src/dom.ts | 33 +++++-- 5 files changed, 139 insertions(+), 7 deletions(-) create mode 100644 change/@microsoft-fast-element-ecbe6511-b19c-4312-91f2-1c354ebd5245.json create mode 100644 change/@microsoft-fast-router-08a4dedc-a404-44ca-ac6a-fe3b4b22a2a1.json diff --git a/change/@microsoft-fast-element-ecbe6511-b19c-4312-91f2-1c354ebd5245.json b/change/@microsoft-fast-element-ecbe6511-b19c-4312-91f2-1c354ebd5245.json new file mode 100644 index 00000000000..6d5e05205b9 --- /dev/null +++ b/change/@microsoft-fast-element-ecbe6511-b19c-4312-91f2-1c354ebd5245.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "feat: enable synchronous dom updates for SSR", + "packageName": "@microsoft/fast-element", + "email": "roeisenb@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@microsoft-fast-router-08a4dedc-a404-44ca-ac6a-fe3b4b22a2a1.json b/change/@microsoft-fast-router-08a4dedc-a404-44ca-ac6a-fe3b4b22a2a1.json new file mode 100644 index 00000000000..1a0e4e6db55 --- /dev/null +++ b/change/@microsoft-fast-router-08a4dedc-a404-44ca-ac6a-fe3b4b22a2a1.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "fix: update router to work with new template primitives", + "packageName": "@microsoft/fast-router", + "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 bfb4247549b..fa09944427e 100644 --- a/packages/web-components/fast-element/docs/api-report.md +++ b/packages/web-components/fast-element/docs/api-report.md @@ -209,6 +209,7 @@ export const DOM: Readonly<{ createInterpolationPlaceholder(index: number): string; createCustomAttributePlaceholder(index: number): string; createBlockPlaceholder(index: number): string; + setUpdateMode(isAsync: boolean): void; queueUpdate(callable: Callable): void; nextUpdate(): Promise; processUpdates(): void; diff --git a/packages/web-components/fast-element/src/dom.spec.ts b/packages/web-components/fast-element/src/dom.spec.ts index eec75bb63d4..a201d7808e5 100644 --- a/packages/web-components/fast-element/src/dom.spec.ts +++ b/packages/web-components/fast-element/src/dom.spec.ts @@ -25,7 +25,7 @@ function watchSetTimeoutForErrors() { } describe("The DOM facade", () => { - context("when updating DOM", () => { + context("when updating DOM asynchronously", () => { it("calls task in a future turn", done => { let called = false; @@ -430,4 +430,100 @@ describe("The DOM facade", () => { }, waitMilliseconds); }); }); + + context("when updating DOM synchronously", () => { + beforeEach(() => { + DOM.setUpdateMode(false); + }); + + afterEach(() => { + DOM.setUpdateMode(true); + }); + + it("calls task immediately", () => { + let called = false; + + DOM.queueUpdate(() => { + called = true; + }); + + expect(called).to.equal(true); + }); + + it("calls task.call method immediately", () => { + let called = false; + + DOM.queueUpdate({ + call: () => { + called = true; + } + }); + + expect(called).to.equal(true); + }); + + it("calls multiple tasks in order", () => { + const calls:number[] = []; + + DOM.queueUpdate(() => { + calls.push(0); + }); + DOM.queueUpdate(() => { + calls.push(1); + }); + DOM.queueUpdate(() => { + calls.push(2); + }); + + expect(calls).to.eql([0, 1, 2]); + }); + + it("can schedule tasks recursively", () => { + const steps: number[] = []; + + DOM.queueUpdate(() => { + steps.push(0); + DOM.queueUpdate(() => { + steps.push(2); + DOM.queueUpdate(() => { + steps.push(4); + }); + steps.push(3); + }); + steps.push(1); + }); + + expect(steps).to.eql([0, 1, 2, 3, 4]); + }); + + it(`can recurse ${maxRecursion} tasks deep`, () => { + let recurseCount = 0; + function go() { + if (++recurseCount < maxRecursion) { + DOM.queueUpdate(go); + } + } + + DOM.queueUpdate(go); + + expect(recurseCount).to.equal(maxRecursion); + }); + + it("throws errors immediately", () => { + const calls: number[] = []; + let caught: any; + + try { + DOM.queueUpdate(() => { + calls.push(0); + throw 0; + }); + } catch(error) { + caught = error; + } + + expect(calls).to.eql([0]); + expect(caught).to.eql(0); + }); + }); }); diff --git a/packages/web-components/fast-element/src/dom.ts b/packages/web-components/fast-element/src/dom.ts index f95cd66af56..45942985dbf 100644 --- a/packages/web-components/fast-element/src/dom.ts +++ b/packages/web-components/fast-element/src/dom.ts @@ -13,6 +13,8 @@ const fastHTMLPolicy: TrustedTypesPolicy = $global.trustedTypes.createPolicy( let htmlPolicy: TrustedTypesPolicy = fastHTMLPolicy; const updateQueue: Callable[] = []; const pendingErrors: any[] = []; +const rAF = $global.requestAnimationFrame; +let updateAsync = true; function throwFirstError(): void { if (pendingErrors.length) { @@ -24,8 +26,13 @@ function tryRunTask(task: Callable): void { try { (task as any).call(); } catch (error) { - pendingErrors.push(error); - setTimeout(throwFirstError, 0); + if (updateAsync) { + pendingErrors.push(error); + setTimeout(throwFirstError, 0); + } else { + updateQueue.length = 0; + throw error; + } } } @@ -128,16 +135,30 @@ export const DOM = Object.freeze({ return ``; }, + /** + * Sets the update mode used by queueUpdate. + * @param isAsync Indicates whether DOM updates should be asynchronous. + * @remarks + * By default, the update mode is asynchronous, since that provides the best + * performance in the browser. Passing false to setUpdateMode will instead cause + * the queue to be immediately processed for each call to queueUpdate. However, + * ordering will still be preserved so that nested tasks do not run until + * after parent tasks complete. + */ + setUpdateMode(isAsync: boolean) { + updateAsync = isAsync; + }, + /** * Schedules DOM update work in the next async batch. * @param callable - The callable function or object to queue. */ queueUpdate(callable: Callable) { - if (updateQueue.length < 1) { - $global.requestAnimationFrame(DOM.processUpdates); - } - updateQueue.push(callable); + + if (updateQueue.length < 2) { + updateAsync ? rAF(DOM.processUpdates) : DOM.processUpdates(); + } }, /** From 307f7d974ac8bbdea7fdb5df6553ab5441cff22d Mon Sep 17 00:00:00 2001 From: Nicholas Rice <3213292+nicholasrice@users.noreply.github.com> Date: Thu, 17 Feb 2022 10:17:41 -0800 Subject: [PATCH 060/135] feat: adds FASTElementRenderer to fast-ssr (#5613) * adds FASTElementRenderer as a ElementRenderer implementation * adding test * adding eslint for file extensions * re-structure tests to better support dependency structures * Fixing tests for FASTElementRenderer.matchesClass * adding code comments * Update packages/web-components/fast-ssr/src/element-renderer/element-renderer.ts Co-authored-by: Jane Chu <7559015+janechu@users.noreply.github.com> Co-authored-by: nicholasrice Co-authored-by: Jane Chu <7559015+janechu@users.noreply.github.com> --- .../web-components/fast-ssr/.eslintrc.cjs | 6 ++ packages/web-components/fast-ssr/package.json | 9 ++- ...wright.config.ts => playwright.config.cjs} | 6 +- .../web-components/fast-ssr/server/server.ts | 2 +- .../elemenent-renderer.spec.ts | 17 +++++ .../src/element-renderer/element-renderer.ts | 74 +++++++++++++++++++ .../fast-style}/fast-style.spec.ts | 0 packages/web-components/fast-ssr/src/index.ts | 2 +- .../web-components/fast-ssr/src/tsconfig.json | 3 +- .../fast-ssr/test/example.spec.ts | 10 --- .../web-components/fast-ssr/test/package.json | 5 -- .../fast-ssr/test/tsconfig.json | 9 --- .../web-components/fast-ssr/tsconfig.json | 1 + 13 files changed, 110 insertions(+), 34 deletions(-) create mode 100644 packages/web-components/fast-ssr/.eslintrc.cjs rename packages/web-components/fast-ssr/{test/playwright.config.ts => playwright.config.cjs} (51%) create mode 100644 packages/web-components/fast-ssr/src/element-renderer/elemenent-renderer.spec.ts create mode 100644 packages/web-components/fast-ssr/src/element-renderer/element-renderer.ts rename packages/web-components/fast-ssr/{test => src/fast-style}/fast-style.spec.ts (100%) delete mode 100644 packages/web-components/fast-ssr/test/example.spec.ts delete mode 100644 packages/web-components/fast-ssr/test/package.json delete mode 100644 packages/web-components/fast-ssr/test/tsconfig.json diff --git a/packages/web-components/fast-ssr/.eslintrc.cjs b/packages/web-components/fast-ssr/.eslintrc.cjs new file mode 100644 index 00000000000..82074b3cb5e --- /dev/null +++ b/packages/web-components/fast-ssr/.eslintrc.cjs @@ -0,0 +1,6 @@ +module.exports = { + extends: ["@microsoft/eslint-config-fast-dna", "prettier"], + rules: { + "import/extensions": ["error", "always"], + }, +}; diff --git a/packages/web-components/fast-ssr/package.json b/packages/web-components/fast-ssr/package.json index cdcb62fa1ec..a9cf3c7a1d3 100644 --- a/packages/web-components/fast-ssr/package.json +++ b/packages/web-components/fast-ssr/package.json @@ -17,13 +17,16 @@ "scripts": { "build": "tsc -b --clean src && tsc -b src", "build-server": "tsc -b server", - "pretest": "npm run build-server", - "test": "playwright test -c test", + "eslint": "eslint . --ext .ts", + "eslint:fix": "eslint . --ext .ts --fix", + "pretest": "npm run build-server && npm run build", + "test": "playwright test --config=playwright.config.cjs", "test-server": "node server/dist/server.js", "install-playwright-browsers": "npx playwright install" }, "description": "A package for rendering FAST components outside the browser.", - "main": "index.js", + "main": "./dist/esm/index.js", + "types": "./dist/dts/index.d.ts", "private": true, "dependencies": { "@lit-labs/ssr": "^1.0.0-rc.2", diff --git a/packages/web-components/fast-ssr/test/playwright.config.ts b/packages/web-components/fast-ssr/playwright.config.cjs similarity index 51% rename from packages/web-components/fast-ssr/test/playwright.config.ts rename to packages/web-components/fast-ssr/playwright.config.cjs index 4b048b7ff22..3bd6075cf09 100644 --- a/packages/web-components/fast-ssr/test/playwright.config.ts +++ b/packages/web-components/fast-ssr/playwright.config.cjs @@ -1,6 +1,5 @@ -// playwright.config.ts -import { PlaywrightTestConfig } from "@playwright/test"; -const config: PlaywrightTestConfig = { +module.exports = { + testDir: "./dist/esm", webServer: { command: "npm run test-server", port: 8080, @@ -8,4 +7,3 @@ const config: PlaywrightTestConfig = { reuseExistingServer: false, }, }; -export default config; diff --git a/packages/web-components/fast-ssr/server/server.ts b/packages/web-components/fast-ssr/server/server.ts index 866fa10b5e8..0507d622326 100644 --- a/packages/web-components/fast-ssr/server/server.ts +++ b/packages/web-components/fast-ssr/server/server.ts @@ -47,7 +47,7 @@ function handleStyleRequest(req: Request, res: Response) { function handleStyleScriptRequest(req: Request, res: Response) { res.set("Content-Type", "application/javascript"); fs.readFile( - path.resolve(__dirname, "./dist/fast-style/index.js"), + path.resolve(__dirname, "./dist/esm/fast-style/index.js"), { encoding: "utf8" }, (err, data) => { const stream = (Readable as any).from(data); diff --git a/packages/web-components/fast-ssr/src/element-renderer/elemenent-renderer.spec.ts b/packages/web-components/fast-ssr/src/element-renderer/elemenent-renderer.spec.ts new file mode 100644 index 00000000000..717eb76599f --- /dev/null +++ b/packages/web-components/fast-ssr/src/element-renderer/elemenent-renderer.spec.ts @@ -0,0 +1,17 @@ +import "@lit-labs/ssr/lib/install-global-dom-shim.js"; +import { FASTElement } from "@microsoft/fast-element"; +import { expect, test } from '@playwright/test'; +import { FASTElementRenderer } from "./element-renderer.js"; + +test.describe("FASTElementRenderer", () => { + test.describe("should have a 'matchesClass' method", () => { + test("that returns true when invoked with a class that extends FASTElement ", () => { + class MyElement extends FASTElement {} + expect(FASTElementRenderer.matchesClass(MyElement)).toBe(true); + }); + test("that returns false when invoked with a class that does not extend FASTElement ", () => { + class MyElement extends HTMLElement {} + expect(FASTElementRenderer.matchesClass(MyElement)).toBe(false); + }); + }) +}) diff --git a/packages/web-components/fast-ssr/src/element-renderer/element-renderer.ts b/packages/web-components/fast-ssr/src/element-renderer/element-renderer.ts new file mode 100644 index 00000000000..11980a038f7 --- /dev/null +++ b/packages/web-components/fast-ssr/src/element-renderer/element-renderer.ts @@ -0,0 +1,74 @@ +import { ElementRenderer, RenderInfo } from "@lit-labs/ssr"; +import { FASTElement } from "@microsoft/fast-element"; + +export class FASTElementRenderer extends ElementRenderer { + /** + * The element instance represented by the {@link FASTElementRenderer}. + */ + public readonly element!: FASTElement; + + /** + * Tests a constructor to determine if it should be managed by a {@link FASTElementRenderer}. + * @param ctor - The constructor to test. + */ + public static matchesClass(ctor: typeof HTMLElement): boolean { + return ctor.prototype instanceof FASTElement; + } + + /** + * Indicate to the {@link FASTElementRenderer} that the instance should execute DOM connection behavior. + */ + public connectedCallback(): void { + this.element.connectedCallback(); + } + + /** + * Constructs a new {@link FASTElementRenderer}. + * @param tagName - the tag-name of the element to create. + */ + constructor(tagName: string) { + super(tagName); + + const ctor: typeof FASTElement | null = customElements.get(this.tagName); + + if (ctor) { + this.element = new ctor(); + } else { + throw new Error( + `FASTElementRenderer was unable to find a constructor for a custom element with the tag name '${tagName}'.` + ); + } + } + + /** + * Renders the component internals to light DOM instead of shadow DOM. + * @param renderInfo - information about the current rendering context. + */ + *renderLight(renderInfo: RenderInfo): IterableIterator { + // TODO - this will yield out the element's template using the template renderer, skipping any shadow-DOM specific emission. + yield ""; + } + + /** + * Render the component internals to shadow DOM. + * @param renderInfo - information about the current rendering context. + */ + *renderShadow(renderInfo: RenderInfo): IterableIterator { + // TODO - this will yield out the element's template using the template renderer + yield ""; + } + + /** + * Indicate to the {@link FASTElementRenderer} that an attribute has been changed. + * @param name - The name of the changed attribute + * @param old - The old attribute value + * @param value - The new attribute value + */ + attributeChangedCallback( + name: string, + old: string | null, + value: string | null + ): void { + this.element.attributeChangedCallback(name, old, value); + } +} diff --git a/packages/web-components/fast-ssr/test/fast-style.spec.ts b/packages/web-components/fast-ssr/src/fast-style/fast-style.spec.ts similarity index 100% rename from packages/web-components/fast-ssr/test/fast-style.spec.ts rename to packages/web-components/fast-ssr/src/fast-style/fast-style.spec.ts diff --git a/packages/web-components/fast-ssr/src/index.ts b/packages/web-components/fast-ssr/src/index.ts index 62073af2a0f..09491d50f08 100644 --- a/packages/web-components/fast-ssr/src/index.ts +++ b/packages/web-components/fast-ssr/src/index.ts @@ -1 +1 @@ -export default "fast-ssr"; +export { FASTElementRenderer } from "./element-renderer/element-renderer.js"; diff --git a/packages/web-components/fast-ssr/src/tsconfig.json b/packages/web-components/fast-ssr/src/tsconfig.json index 8b2f5bed380..d7363176aab 100644 --- a/packages/web-components/fast-ssr/src/tsconfig.json +++ b/packages/web-components/fast-ssr/src/tsconfig.json @@ -3,7 +3,8 @@ "compilerOptions": { "composite": true, "rootDir": ".", - "outDir": "../dist", + "outDir": "../dist/esm", + "declarationDir": "../dist/dts", "lib": [ "dom", "esnext" diff --git a/packages/web-components/fast-ssr/test/example.spec.ts b/packages/web-components/fast-ssr/test/example.spec.ts deleted file mode 100644 index 73c85108fa9..00000000000 --- a/packages/web-components/fast-ssr/test/example.spec.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { test, expect, ElementHandle } from '@playwright/test'; -import fastSSR from "../src"; - -test("example module test", async () => { - expect(fastSSR).toBe("fast-ssr"); -}) -test("example server test", async ({ page }) => { - await page.goto("/"); - expect(await page.innerText('body')).toBe("hello world"); -}); diff --git a/packages/web-components/fast-ssr/test/package.json b/packages/web-components/fast-ssr/test/package.json deleted file mode 100644 index 890f891f972..00000000000 --- a/packages/web-components/fast-ssr/test/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "type": "commonjs", - "comment": "This allows Playwright to handle TS compilation for us", - "private": true -} \ No newline at end of file diff --git a/packages/web-components/fast-ssr/test/tsconfig.json b/packages/web-components/fast-ssr/test/tsconfig.json deleted file mode 100644 index 2ba585452cf..00000000000 --- a/packages/web-components/fast-ssr/test/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../tsconfig.json", - "compilerOptions": { - "rootDir": ".", - "outDir": "dist", - "declaration": false - }, - "references": [{ "path": "../src"}] -} diff --git a/packages/web-components/fast-ssr/tsconfig.json b/packages/web-components/fast-ssr/tsconfig.json index cf4cd344bc2..4cd3f5779d5 100644 --- a/packages/web-components/fast-ssr/tsconfig.json +++ b/packages/web-components/fast-ssr/tsconfig.json @@ -9,6 +9,7 @@ "emitDecoratorMetadata": true, "noEmitOnError": true, "strict": true, + "skipLibCheck": true, "lib": [ "dom", "esnext" From ad21701fdfddb42c7fa8a71846c1ddf5baf75ac9 Mon Sep 17 00:00:00 2001 From: Jane Chu <7559015+janechu@users.noreply.github.com> Date: Thu, 17 Feb 2022 15:02:33 -0800 Subject: [PATCH 061/135] Added prettier to fast-ssr package (#5617) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Pull Request ## 📖 Description This change adds prettier to the `@microsoft/fast-ssr` package. Note: I ran prettier after the changes and it looks like the current files are gtg. This is more for future proofing. ## ✅ Checklist ### General - [ ] I have included a change request file using `$ yarn change` - [ ] I have added tests for my changes. - [x] I have tested my changes. - [ ] I have updated the project documentation to reflect my changes. - [x] I have read the [CONTRIBUTING](https://github.com/Microsoft/fast/blob/master/CONTRIBUTING.md) documentation and followed the [standards](https://www.fast.design/docs/community/code-of-conduct/#our-standards) for this project. --- packages/web-components/fast-ssr/.prettierignore | 3 +++ packages/web-components/fast-ssr/package.json | 2 ++ 2 files changed, 5 insertions(+) create mode 100644 packages/web-components/fast-ssr/.prettierignore diff --git a/packages/web-components/fast-ssr/.prettierignore b/packages/web-components/fast-ssr/.prettierignore new file mode 100644 index 00000000000..28c7caee553 --- /dev/null +++ b/packages/web-components/fast-ssr/.prettierignore @@ -0,0 +1,3 @@ +coverage/* +dist/* +*.spec.ts diff --git a/packages/web-components/fast-ssr/package.json b/packages/web-components/fast-ssr/package.json index a9cf3c7a1d3..840d00ab066 100644 --- a/packages/web-components/fast-ssr/package.json +++ b/packages/web-components/fast-ssr/package.json @@ -20,6 +20,8 @@ "eslint": "eslint . --ext .ts", "eslint:fix": "eslint . --ext .ts --fix", "pretest": "npm run build-server && npm run build", + "prettier:diff": "prettier --config ../../../.prettierrc \"**/*.{ts,html}\" --list-different", + "prettier": "prettier --config ../../../.prettierrc --write \"**/*.{ts,html}\"", "test": "playwright test --config=playwright.config.cjs", "test-server": "node server/dist/server.js", "install-playwright-browsers": "npx playwright install" From 2bc4c7aa16779b3b88b3b981dacd730b3e7e59cc Mon Sep 17 00:00:00 2001 From: Jane Chu <7559015+janechu@users.noreply.github.com> Date: Thu, 17 Feb 2022 18:14:36 -0800 Subject: [PATCH 062/135] Fixed linting errors and warnings (#5619) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Pull Request ## 📖 Description This change does the following: - Allow `no-non-null-assertion` - Fixes all other warnings and errors ## 👩‍💻 Reviewer Notes @EisenbergEffect since these changes affect your work please take a close look, most of the changes are benign but I did move code around to best minimize ignoring the no-use-before-define issues. ## ✅ Checklist ### General - [ ] I have included a change request file using `$ yarn change` - [ ] I have added tests for my changes. - [x] I have tested my changes. - [ ] I have updated the project documentation to reflect my changes. - [x] I have read the [CONTRIBUTING](https://github.com/Microsoft/fast/blob/master/CONTRIBUTING.md) documentation and followed the [standards](https://www.fast.design/docs/community/code-of-conduct/#our-standards) for this project. --- ...-486e25bf-0f0b-42c6-adcc-8a0e99ed88a3.json | 7 + .../web-components/fast-element/.eslintrc.cjs | 1 + .../web-components/fast-element/src/dom.ts | 2 +- .../src/observation/array-change-records.ts | 88 ++--- .../src/observation/array-observer.ts | 43 ++- .../fast-element/src/templating/binding.ts | 305 +++++++++--------- .../fast-element/src/templating/children.ts | 4 +- .../fast-element/src/templating/compiler.ts | 27 +- .../src/templating/node-observation.ts | 2 +- .../fast-element/src/templating/slotted.ts | 4 +- .../fast-element/src/templating/view.ts | 2 +- 11 files changed, 257 insertions(+), 228 deletions(-) create mode 100644 change/@microsoft-fast-element-486e25bf-0f0b-42c6-adcc-8a0e99ed88a3.json diff --git a/change/@microsoft-fast-element-486e25bf-0f0b-42c6-adcc-8a0e99ed88a3.json b/change/@microsoft-fast-element-486e25bf-0f0b-42c6-adcc-8a0e99ed88a3.json new file mode 100644 index 00000000000..b96487b1cff --- /dev/null +++ b/change/@microsoft-fast-element-486e25bf-0f0b-42c6-adcc-8a0e99ed88a3.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "Fixed linting errors and warnings", + "packageName": "@microsoft/fast-element", + "email": "7559015+janechu@users.noreply.github.com", + "dependentChangeType": "none" +} diff --git a/packages/web-components/fast-element/.eslintrc.cjs b/packages/web-components/fast-element/.eslintrc.cjs index d5ea064f67e..4b5f0dd67d2 100644 --- a/packages/web-components/fast-element/.eslintrc.cjs +++ b/packages/web-components/fast-element/.eslintrc.cjs @@ -4,6 +4,7 @@ module.exports = { "import/extensions": ["error", "always"], "max-classes-per-file": "off", "no-case-declarations": "off", + "@typescript-eslint/no-non-null-assertion": "off", "@typescript-eslint/ban-types": [ "error", { diff --git a/packages/web-components/fast-element/src/dom.ts b/packages/web-components/fast-element/src/dom.ts index 45942985dbf..34d5fc660eb 100644 --- a/packages/web-components/fast-element/src/dom.ts +++ b/packages/web-components/fast-element/src/dom.ts @@ -40,7 +40,7 @@ const marker = `fast-${Math.random().toString(36).substring(2, 8)}`; let id = 0; /** @internal */ -export const nextId = () => `${marker}-${++id}`; +export const nextId = (): string => `${marker}-${++id}`; /** @internal */ export const _interpolationStart = `${marker}{`; diff --git a/packages/web-components/fast-element/src/observation/array-change-records.ts b/packages/web-components/fast-element/src/observation/array-change-records.ts index 46194a9e1e4..2dfe6ce5221 100644 --- a/packages/web-components/fast-element/src/observation/array-change-records.ts +++ b/packages/web-components/fast-element/src/observation/array-change-records.ts @@ -164,6 +164,51 @@ 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 + * + * + * + * 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 @@ -386,46 +431,3 @@ function project(array: unknown[], changes: Splice[]): Splice[] { return splices; } - -/** - * 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 - * - * - * - * 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 - ) { - return previous === void 0 - ? changes!.length > 1 - ? project(current, changes!) - : changes - : calc(current, 0, current.length, previous, 0, previous.length); - } -} diff --git a/packages/web-components/fast-element/src/observation/array-observer.ts b/packages/web-components/fast-element/src/observation/array-observer.ts index 0ba48efe0d0..8572c89ee67 100644 --- a/packages/web-components/fast-element/src/observation/array-observer.ts +++ b/packages/web-components/fast-element/src/observation/array-observer.ts @@ -66,7 +66,7 @@ class ArrayObserver extends SubscriberSet { this.notify(Splice.normalize(oldCollection, this.subject, splices)); } - private enqueue() { + private enqueue(): void { if (this.needsQueue) { this.needsQueue = false; DOM.queueUpdate(this); @@ -83,9 +83,9 @@ const sort = proto.sort; const splice = proto.splice; const unshift = proto.unshift; const arrayOverrides = { - pop() { + pop(...args) { const notEmpty = this.length > 0; - const result = pop.apply(this, arguments); + const result = pop.apply(this, args); const o = this.$fastController as ArrayObserver; if (o !== void 0 && notEmpty) { @@ -95,23 +95,20 @@ const arrayOverrides = { return result; }, - push() { - const result = push.apply(this, arguments); + 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 - arguments.length, [], arguments.length), - this - ) + adjustIndex(new Splice(this.length - args.length, [], args.length), this) ); } return result; }, - reverse() { + reverse(...args) { let oldArray; const o = this.$fastController as ArrayObserver; @@ -120,7 +117,7 @@ const arrayOverrides = { oldArray = this.slice(); } - const result = reverse.apply(this, arguments); + const result = reverse.apply(this, args); if (o !== void 0) { o.reset(oldArray); @@ -129,9 +126,9 @@ const arrayOverrides = { return result; }, - shift() { + shift(...args) { const notEmpty = this.length > 0; - const result = shift.apply(this, arguments); + const result = shift.apply(this, args); const o = this.$fastController as ArrayObserver; if (o !== void 0 && notEmpty) { @@ -141,7 +138,7 @@ const arrayOverrides = { return result; }, - sort() { + sort(...args) { let oldArray; const o = this.$fastController as ArrayObserver; @@ -150,7 +147,7 @@ const arrayOverrides = { oldArray = this.slice(); } - const result = sort.apply(this, arguments); + const result = sort.apply(this, args); if (o !== void 0) { o.reset(oldArray); @@ -159,18 +156,14 @@ const arrayOverrides = { return result; }, - splice() { - const result = splice.apply(this, arguments); + splice(...args) { + const result = splice.apply(this, args); const o = this.$fastController as ArrayObserver; if (o !== void 0) { o.addSplice( adjustIndex( - new Splice( - +arguments[0], - result, - arguments.length > 2 ? arguments.length - 2 : 0 - ), + new Splice(+args[0], result, args.length > 2 ? args.length - 2 : 0), this ) ); @@ -179,12 +172,12 @@ const arrayOverrides = { return result; }, - unshift() { - const result = unshift.apply(this, arguments); + unshift(...args) { + const result = unshift.apply(this, args); const o = this.$fastController as ArrayObserver; if (o !== void 0) { - o.addSplice(adjustIndex(new Splice(0, [], arguments.length), this)); + o.addSplice(adjustIndex(new Splice(0, [], args.length), this)); } 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 03af099c526..cefec33ba7e 100644 --- a/packages/web-components/fast-element/src/templating/binding.ts +++ b/packages/web-components/fast-element/src/templating/binding.ts @@ -33,6 +33,7 @@ export interface BindingMode { event: BindingType; } +/* eslint-disable-next-line @typescript-eslint/no-unused-vars */ export interface BindingConfig { mode: BindingMode; options: any; @@ -62,16 +63,163 @@ class BindingBase { } } +function createContentBinding( + BaseType: typeof TargetUpdateBinding +): typeof TargetUpdateBinding { + return class extends BaseType { + unbind( + source: any, + context: ExecutionContext, + targets: ViewBehaviorTargets + ): void { + super.unbind(source, context, targets); + + const target = targets[this.directive.targetId] as ContentTarget; + const view = target.$fastView as ComposableView; + + if (view !== void 0 && view.isComposed) { + view.unbind(); + view.needsBindOnly = true; + } + } + }; +} + +function updateContentTarget( + target: ContentTarget, + aspect: string, + value: any, + source: any, + context: ExecutionContext +): void { + // If there's no actual value, then this equates to the + // empty string for the purposes of content bindings. + if (value === null || value === undefined) { + value = ""; + } + + // If the value has a "create" method, then it's a template-like. + if (value.create) { + target.textContent = ""; + let view = target.$fastView as ComposableView; + + // If there's no previous view that we might be able to + // reuse then create a new view from the template. + if (view === void 0) { + view = value.create() as SyntheticView; + } else { + // If there is a previous view, but it wasn't created + // from the same template as the new value, then we + // need to remove the old view if it's still in the DOM + // and create a new view from the template. + if (target.$fastTemplate !== value) { + if (view.isComposed) { + view.remove(); + view.unbind(); + } + + view = value.create() as SyntheticView; + } + } + + // It's possible that the value is the same as the previous template + // and that there's actually no need to compose it. + if (!view.isComposed) { + view.isComposed = true; + view.bind(source, context!); + view.insertBefore(target); + target.$fastView = view; + target.$fastTemplate = value; + } else if (view.needsBindOnly) { + view.needsBindOnly = false; + view.bind(source, context!); + } + } else { + const view = target.$fastView as ComposableView; + + // If there is a view and it's currently composed into + // the DOM, then we need to remove it. + if (view !== void 0 && view.isComposed) { + view.isComposed = false; + view.remove(); + + if (view.needsBindOnly) { + view.needsBindOnly = false; + } else { + view.unbind(); + } + } + + target.textContent = value; + } +} + +interface TokenListState { + v: {}; + c: number; +} + +function updateTokenListTarget( + this: UpdateTargetThis, + target: Element, + aspect: string, + value: any +): void { + const directive = this.directive; + const state: TokenListState = + target[directive.uniqueId] ?? + (target[directive.uniqueId] = { c: 0, v: Object.create(null) }); + const versions = state.v; + let currentVersion = state.c; + const tokenList = target[aspect] as DOMTokenList; + + // Add the classes, tracking the version at which they were added. + if (value !== null && value !== undefined && value.length) { + const names = value.split(/\s+/); + + for (let i = 0, ii = names.length; i < ii; ++i) { + const currentName = names[i]; + + if (currentName === "") { + continue; + } + + versions[currentName] = currentVersion; + tokenList.add(currentName); + } + } + + state.v = currentVersion + 1; + + // If this is the first call to add classes, there's no need to remove old ones. + if (currentVersion === 0) { + return; + } + + // Remove classes from the previous version. + currentVersion -= 1; + + for (const name in versions) { + if (versions[name] === currentVersion) { + tokenList.remove(name); + } + } +} + +type BindingConfigResolver = (options: T) => BindingConfig; + class TargetUpdateBinding extends BindingBase { constructor(directive: HTMLBindingDirective, protected updateTarget: UpdateTarget) { super(directive); } - static createBindingConfig(defaultOptions: T, eventType?: BindingType) { - const config: BindingConfig & - ((options?: typeof defaultOptions) => BindingConfig) = ( - options: typeof defaultOptions - ): BindingConfig => { + static createBindingConfig( + defaultOptions: T, + eventType?: BindingType + ): BindingConfig & BindingConfigResolver { + const config: BindingConfig & BindingConfigResolver = ( + options: T + ): BindingConfig => { return { mode: config.mode, options: Object.assign({}, defaultOptions, options), @@ -120,7 +268,7 @@ class OneTimeBinding extends TargetUpdateBinding { const signals: Record = Object.create(null); -export function sendSignal(signal: string) { +export function sendSignal(signal: string): void { const found = signals[signal]; if (found) { Array.isArray(found) ? found.forEach(x => x()) : found(); @@ -180,7 +328,7 @@ class OnSignalBinding extends TargetUpdateBinding { } } - private getSignal(source: any, context: ExecutionContext) { + private getSignal(source: any, context: ExecutionContext): string { const options = this.directive.options; return isString(options) ? options : options(source, context); } @@ -242,58 +390,6 @@ class OnChangeBinding extends TargetUpdateBinding { } } -interface TokenListState { - v: {}; - c: number; -} - -function updateTokenListTarget( - this: UpdateTargetThis, - target: Element, - aspect: string, - value: any -): void { - const directive = this.directive; - const state: TokenListState = - target[directive.uniqueId] ?? - (target[directive.uniqueId] = { c: 0, v: Object.create(null) }); - const versions = state.v; - let currentVersion = state.c; - const tokenList = target[aspect] as DOMTokenList; - - // Add the classes, tracking the version at which they were added. - if (value !== null && value !== undefined && value.length) { - const names = value.split(/\s+/); - - for (let i = 0, ii = names.length; i < ii; ++i) { - const currentName = names[i]; - - if (currentName === "") { - continue; - } - - versions[currentName] = currentVersion; - tokenList.add(currentName); - } - } - - state.v = currentVersion + 1; - - // If this is the first call to add classes, there's no need to remove old ones. - if (currentVersion === 0) { - return; - } - - // Remove classes from the previous version. - currentVersion -= 1; - - for (const name in versions) { - if (versions[name] === currentVersion) { - tokenList.remove(name); - } - } -} - type ComposableView = SyntheticView & { isComposed?: boolean; needsBindOnly?: boolean; @@ -304,91 +400,6 @@ type ContentTarget = Node & { $fastTemplate?: { create(): SyntheticView }; }; -function updateContentTarget( - target: ContentTarget, - aspect: string, - value: any, - source: any, - context: ExecutionContext -): void { - // If there's no actual value, then this equates to the - // empty string for the purposes of content bindings. - if (value === null || value === undefined) { - value = ""; - } - - // If the value has a "create" method, then it's a template-like. - if (value.create) { - target.textContent = ""; - let view = target.$fastView as ComposableView; - - // If there's no previous view that we might be able to - // reuse then create a new view from the template. - if (view === void 0) { - view = value.create() as SyntheticView; - } else { - // If there is a previous view, but it wasn't created - // from the same template as the new value, then we - // need to remove the old view if it's still in the DOM - // and create a new view from the template. - if (target.$fastTemplate !== value) { - if (view.isComposed) { - view.remove(); - view.unbind(); - } - - view = value.create() as SyntheticView; - } - } - - // It's possible that the value is the same as the previous template - // and that there's actually no need to compose it. - if (!view.isComposed) { - view.isComposed = true; - view.bind(source, context!); - view.insertBefore(target); - target.$fastView = view; - target.$fastTemplate = value; - } else if (view.needsBindOnly) { - view.needsBindOnly = false; - view.bind(source, context!); - } - } else { - const view = target.$fastView as ComposableView; - - // If there is a view and it's currently composed into - // the DOM, then we need to remove it. - if (view !== void 0 && view.isComposed) { - view.isComposed = false; - view.remove(); - - if (view.needsBindOnly) { - view.needsBindOnly = false; - } else { - view.unbind(); - } - } - - target.textContent = value; - } -} - -function createContentBinding(BaseType: typeof TargetUpdateBinding) { - return class extends BaseType { - unbind(source: any, context: ExecutionContext, targets: ViewBehaviorTargets) { - super.unbind(source, context, targets); - - const target = targets[this.directive.targetId] as ContentTarget; - const view = target.$fastView as ComposableView; - - if (view !== void 0 && view.isComposed) { - view.unbind(); - view.needsBindOnly = true; - } - } - }; -} - type FASTEventSource = Node & { $fastSource: any; $fastContext: ExecutionContext | null; @@ -407,7 +418,7 @@ class EventListener extends BindingBase { this.removeEventListener(targets[this.directive.targetId] as FASTEventSource); } - protected removeEventListener(target: FASTEventSource) { + protected removeEventListener(target: FASTEventSource): void { target.$fastSource = null; target.$fastContext = null; target.removeEventListener(this.directive.aspect!, this, this.directive.options); @@ -429,7 +440,7 @@ class EventListener extends BindingBase { } class OneTimeEventListener extends EventListener { - handleEvent(event: Event) { + handleEvent(event: Event): void { super.handleEvent(event); this.removeEventListener(event.currentTarget as FASTEventSource); } @@ -476,7 +487,7 @@ export class HTMLBindingDirective extends InlinableHTMLDirective { super(); } - public setAspect(value: string) { + public setAspect(value: string): void { (this as Mutable).rawAspect = value; if (!value) { diff --git a/packages/web-components/fast-element/src/templating/children.ts b/packages/web-components/fast-element/src/templating/children.ts index 9ba2a32c9b0..ba6d113bbbd 100644 --- a/packages/web-components/fast-element/src/templating/children.ts +++ b/packages/web-components/fast-element/src/templating/children.ts @@ -57,7 +57,7 @@ export class ChildrenDirective extends NodeObservationDirective< * Begins observation of the nodes. * @param target - The target to observe. */ - observe(target: any) { + observe(target: any): void { const observer = target[this.uniqueId] ?? (target[this.uniqueId] = new MutationObserver(this.handleEvent)); @@ -69,7 +69,7 @@ export class ChildrenDirective extends NodeObservationDirective< * Disconnects observation of the nodes. * @param target - The target to unobserve. */ - disconnect(target: any) { + disconnect(target: any): void { const observer = target[this.uniqueId]; observer.$fastTarget = null; observer.disconnect(); diff --git a/packages/web-components/fast-element/src/templating/compiler.ts b/packages/web-components/fast-element/src/templating/compiler.ts index 211284ac28a..bee114952e9 100644 --- a/packages/web-components/fast-element/src/templating/compiler.ts +++ b/packages/web-components/fast-element/src/templating/compiler.ts @@ -10,11 +10,17 @@ import type { ViewBehaviorTargets, } from "./html-directive.js"; -const targetIdFrom = (parentId: string, nodeIndex: number) => `${parentId}.${nodeIndex}`; +const targetIdFrom = (parentId: string, nodeIndex: number): string => + `${parentId}.${nodeIndex}`; const descriptorCache: PropertyDescriptorMap = {}; +interface NextNode { + index: number; + node: ChildNode | null; +} + // used to prevent creating lots of objects just to track node and index while compiling -const next = { +const next: NextNode = { index: 0, node: null as ChildNode | null, }; @@ -86,7 +92,11 @@ class CompilationContext implements HTMLTemplateCompilationResult { return targets; } - private addTargetDescriptor(parentId: string, targetId: string, targetIndex: number) { + private addTargetDescriptor( + parentId: string, + targetId: string, + targetIndex: number + ): void { const descriptors = this.descriptors; if ( @@ -229,7 +239,7 @@ function compileContent( parentId, nodeId, nodeIndex -) { +): NextNode { const parseResult = parseContent(context, node.textContent!); if (parseResult === null) { next.node = node.nextSibling; @@ -267,11 +277,16 @@ function compileContent( return next; } -function compileChildren(context: CompilationContext, parent: Node, parentId: string) { +function compileChildren( + context: CompilationContext, + parent: Node, + parentId: string +): void { let nodeIndex = 0; let childNode = parent.firstChild; while (childNode) { + /* eslint-disable-next-line @typescript-eslint/no-use-before-define */ const result = compileNode(context, parentId, childNode, nodeIndex); childNode = result.node; nodeIndex = result.index; @@ -283,7 +298,7 @@ function compileNode( parentId: string, node: Node, nodeIndex: number -) { +): NextNode { const nodeId = targetIdFrom(parentId, nodeIndex); switch (node.nodeType) { diff --git a/packages/web-components/fast-element/src/templating/node-observation.ts b/packages/web-components/fast-element/src/templating/node-observation.ts index 0b4e5ee6ef3..b87d1bb67d5 100644 --- a/packages/web-components/fast-element/src/templating/node-observation.ts +++ b/packages/web-components/fast-element/src/templating/node-observation.ts @@ -32,7 +32,7 @@ export interface NodeBehaviorOptions { */ export type ElementsFilter = (value: Node, index: number, array: Node[]) => boolean; -const selectElements = value => value.nodeType === 1; +const selectElements = (value: Node): boolean => value.nodeType === 1; /** * Creates a function that can be used to filter a Node array, selecting only elements. diff --git a/packages/web-components/fast-element/src/templating/slotted.ts b/packages/web-components/fast-element/src/templating/slotted.ts index d34b334b1ec..a231041da16 100644 --- a/packages/web-components/fast-element/src/templating/slotted.ts +++ b/packages/web-components/fast-element/src/templating/slotted.ts @@ -19,7 +19,7 @@ export class SlottedDirective extends NodeObservationDirective dispose(): void; } -function removeNodeSequence(firstNode: Node, lastNode: Node) { +function removeNodeSequence(firstNode: Node, lastNode: Node): void { const parent = firstNode.parentNode!; let current = firstNode; let next: ChildNode | null; From 1f124b8a4f2c2cf6989c25c0032cd4b3ea9e78b3 Mon Sep 17 00:00:00 2001 From: Jane Chu <7559015+janechu@users.noreply.github.com> Date: Tue, 22 Feb 2022 11:38:44 -0800 Subject: [PATCH 063/135] Added watch script and chokidar (#5621) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Pull Request ## 📖 Description This PR adds `chokidar` so that we can watch files and re-run the playwright tests. Playwright does not currently have a watcher for this purpose so `chokidar-cli` was added to facilitate this. ## ✅ Checklist ### General - [ ] I have included a change request file using `$ yarn change` - [ ] I have added tests for my changes. - [x] I have tested my changes. - [ ] I have updated the project documentation to reflect my changes. - [x] I have read the [CONTRIBUTING](https://github.com/Microsoft/fast/blob/master/CONTRIBUTING.md) documentation and followed the [standards](https://www.fast.design/docs/community/code-of-conduct/#our-standards) for this project. --- packages/web-components/fast-ssr/package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/web-components/fast-ssr/package.json b/packages/web-components/fast-ssr/package.json index 840d00ab066..a01f2139ae4 100644 --- a/packages/web-components/fast-ssr/package.json +++ b/packages/web-components/fast-ssr/package.json @@ -23,6 +23,7 @@ "prettier:diff": "prettier --config ../../../.prettierrc \"**/*.{ts,html}\" --list-different", "prettier": "prettier --config ../../../.prettierrc --write \"**/*.{ts,html}\"", "test": "playwright test --config=playwright.config.cjs", + "test:watch": "chokidar '**/*.spec.ts' -c 'npm run build && npm run test'", "test-server": "node server/dist/server.js", "install-playwright-browsers": "npx playwright install" }, @@ -39,6 +40,7 @@ "@playwright/test": "^1.18.0", "@types/express": "^4.17.13", "@types/node": "^17.0.17", + "chokidar-cli": "^3.0.0", "express": "^4.17.1", "typescript": "^3.8.3" } From 121594fde55296b96f9530e1e40c814326f0bd82 Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Tue, 22 Feb 2022 15:06:49 -0500 Subject: [PATCH 064/135] feat: enable pluggable style handling strategies (#5623) * feat: enable pluggable style handling strategies * refactor: move unnecessary internal function * test: fix up style tests based on new API * refactor: improve the style strategy interface * Change files * refactor: improve strategy factory design via constructible types Co-authored-by: EisenbergEffect --- ...-58e983d2-9c67-4292-b678-88bc265b1fe8.json | 7 + .../fast-element/docs/api-report.md | 25 +- .../src/components/fast-definitions.spec.ts | 10 +- .../src/components/fast-definitions.ts | 4 +- .../web-components/fast-element/src/index.ts | 3 +- .../fast-element/src/styles/css.ts | 4 +- .../fast-element/src/styles/element-styles.ts | 247 +++++++++--------- .../fast-element/src/styles/styles.spec.ts | 92 ++++--- 8 files changed, 206 insertions(+), 186 deletions(-) create mode 100644 change/@microsoft-fast-element-58e983d2-9c67-4292-b678-88bc265b1fe8.json diff --git a/change/@microsoft-fast-element-58e983d2-9c67-4292-b678-88bc265b1fe8.json b/change/@microsoft-fast-element-58e983d2-9c67-4292-b678-88bc265b1fe8.json new file mode 100644 index 00000000000..f3e338dda05 --- /dev/null +++ b/change/@microsoft-fast-element-58e983d2-9c67-4292-b678-88bc265b1fe8.json @@ -0,0 +1,7 @@ +{ + "type": "major", + "comment": "feat: enable pluggable style handling strategies", + "packageName": "@microsoft/fast-element", + "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 fa09944427e..49e5989ee7f 100644 --- a/packages/web-components/fast-element/docs/api-report.md +++ b/packages/web-components/fast-element/docs/api-report.md @@ -150,6 +150,11 @@ export type Constructable = { new (...args: any[]): T; }; +// @public +export type ConstructibleStyleStrategy = { + new (styles: (string | CSSStyleSheet)[]): StyleStrategy; +}; + // @public export class Controller extends PropertyChangeNotifier { // @internal @@ -224,25 +229,23 @@ export const elements: (selector?: string | undefined) => ElementsFilter; export type ElementsFilter = (value: Node, index: number, array: Node[]) => boolean; // @public -export type ElementStyleFactory = (styles: ReadonlyArray) => ElementStyles; - -// @public -export abstract class ElementStyles { +export class ElementStyles { constructor( - styles: ReadonlyArray, - behaviors: ReadonlyArray> | null); + styles: ReadonlyArray); // @internal (undocumented) addStylesTo(target: StyleTarget): void; // @internal (undocumented) readonly behaviors: ReadonlyArray> | null; - static readonly create: ElementStyleFactory; // @internal (undocumented) isAttachedTo(target: StyleTarget): boolean; // @internal (undocumented) removeStylesFrom(target: StyleTarget): void; + static setDefaultStrategy(Strategy: ConstructibleStyleStrategy): void; + get strategy(): StyleStrategy; // @internal (undocumented) readonly styles: ReadonlyArray; withBehaviors(...behaviors: Behavior[]): this; + withStrategy(Strategy: ConstructibleStyleStrategy): this; } // @public @@ -501,12 +504,16 @@ export class Splice { removed: any[]; } +// @public +export interface StyleStrategy { + addStylesTo(target: StyleTarget): void; + removeStylesFrom(target: StyleTarget): void; +} + // @public export interface StyleTarget { adoptedStyleSheets?: CSSStyleSheet[]; append(styles: HTMLStyleElement): void; - // @deprecated - prepend(styles: HTMLStyleElement): void; querySelectorAll(selectors: string): NodeListOf; removeChild(styles: HTMLStyleElement): void; } diff --git a/packages/web-components/fast-element/src/components/fast-definitions.spec.ts b/packages/web-components/fast-element/src/components/fast-definitions.spec.ts index f7b19f2a9c7..77754ee3682 100644 --- a/packages/web-components/fast-element/src/components/fast-definitions.spec.ts +++ b/packages/web-components/fast-element/src/components/fast-definitions.spec.ts @@ -33,7 +33,7 @@ describe("FASTElementDefinition", () => { it("can accept ElementStyles", () => { const css = ".class { color: red; }"; - const styles = ElementStyles.create([css]); + const styles = new ElementStyles([css]); const options = { name: "test-element", styles, @@ -45,8 +45,8 @@ describe("FASTElementDefinition", () => { it("can accept multiple ElementStyles", () => { const css1 = ".class { color: red; }"; const css2 = ".class2 { color: red; }"; - const existingStyles1 = ElementStyles.create([css1]); - const existingStyles2 = ElementStyles.create([css2]); + const existingStyles1 = new ElementStyles([css1]); + const existingStyles2 = new ElementStyles([css2]); const options = { name: "test-element", styles: [existingStyles1, existingStyles2], @@ -60,7 +60,7 @@ describe("FASTElementDefinition", () => { it("can accept mixed strings and ElementStyles", () => { const css1 = ".class { color: red; }"; const css2 = ".class2 { color: red; }"; - const existingStyles2 = ElementStyles.create([css2]); + const existingStyles2 = new ElementStyles([css2]); const options = { name: "test-element", styles: [css1, existingStyles2], @@ -98,7 +98,7 @@ describe("FASTElementDefinition", () => { it("can accept mixed strings, ElementStyles, and CSSStyleSheets", () => { const css1 = ".class { color: red; }"; const css2 = ".class2 { color: red; }"; - const existingStyles2 = ElementStyles.create([css2]); + const existingStyles2 = new ElementStyles([css2]); const styleSheet3 = new CSSStyleSheet(); const options = { name: "test-element", diff --git a/packages/web-components/fast-element/src/components/fast-definitions.ts b/packages/web-components/fast-element/src/components/fast-definitions.ts index 154b00edc1a..3b20062dd54 100644 --- a/packages/web-components/fast-element/src/components/fast-definitions.ts +++ b/packages/web-components/fast-element/src/components/fast-definitions.ts @@ -170,10 +170,10 @@ export class FASTElementDefinition { nameOrConfig.styles === void 0 ? void 0 : Array.isArray(nameOrConfig.styles) - ? ElementStyles.create(nameOrConfig.styles) + ? new ElementStyles(nameOrConfig.styles) : nameOrConfig.styles instanceof ElementStyles ? nameOrConfig.styles - : ElementStyles.create([nameOrConfig.styles]); + : new ElementStyles([nameOrConfig.styles]); } /** diff --git a/packages/web-components/fast-element/src/index.ts b/packages/web-components/fast-element/src/index.ts index 178aea730f3..ee4218147c8 100644 --- a/packages/web-components/fast-element/src/index.ts +++ b/packages/web-components/fast-element/src/index.ts @@ -11,7 +11,8 @@ export type { Callable, Constructable, Mutable } from "./interfaces.js"; export * from "./templating/compiler.js"; export { ElementStyles, - ElementStyleFactory, + StyleStrategy, + ConstructibleStyleStrategy, ComposableStyles, StyleTarget, } from "./styles/element-styles.js"; diff --git a/packages/web-components/fast-element/src/styles/css.ts b/packages/web-components/fast-element/src/styles/css.ts index 195741cd5dc..73caf675903 100644 --- a/packages/web-components/fast-element/src/styles/css.ts +++ b/packages/web-components/fast-element/src/styles/css.ts @@ -62,7 +62,7 @@ export function css( ...values: (ComposableStyles | CSSDirective)[] ): ElementStyles { const { styles, behaviors } = collectStyles(strings, values); - const elementStyles = ElementStyles.create(styles); + const elementStyles = new ElementStyles(styles); return behaviors.length ? elementStyles.withBehaviors(...behaviors) : elementStyles; } @@ -91,7 +91,7 @@ class CSSPartial extends CSSDirective implements Behavior { ); if (stylesheets.length) { - this.styles = ElementStyles.create(stylesheets); + this.styles = new ElementStyles(stylesheets); } } diff --git a/packages/web-components/fast-element/src/styles/element-styles.ts b/packages/web-components/fast-element/src/styles/element-styles.ts index b8dfed97f22..2b6e5c61cd3 100644 --- a/packages/web-components/fast-element/src/styles/element-styles.ts +++ b/packages/web-components/fast-element/src/styles/element-styles.ts @@ -17,13 +17,6 @@ export interface StyleTarget { */ append(styles: HTMLStyleElement): void; - /** - * Adds styles to the target by prepending the styles. - * @param styles - The styles element to add. - * @deprecated - use append() - */ - prepend(styles: HTMLStyleElement): void; - /** * Removes styles from the target. * @param styles - The styles element to remove. @@ -44,34 +37,101 @@ export interface StyleTarget { export type ComposableStyles = string | ElementStyles | CSSStyleSheet; /** - * Creates an ElementStyles instance for an array of ComposableStyles. + * Implemented to provide specific behavior when adding/removing styles + * for elements. * @public */ -export type ElementStyleFactory = ( +export interface StyleStrategy { + /** + * Adds styles to the target. + * @param target - The target to add the styles to. + */ + addStylesTo(target: StyleTarget): void; + + /** + * Removes styles from the target. + * @param target - The target to remove the styles from. + */ + removeStylesFrom(target: StyleTarget): void; +} + +/** + * A type that instantiates a StyleStrategy. + * @public + */ +export type ConstructibleStyleStrategy = { + /** + * Creates an instance of the strategy. + * @param styles - The styles to initialize the strategy with. + */ + new (styles: (string | CSSStyleSheet)[]): StyleStrategy; +}; + +const styleSheetCache = new Map(); +let DefaultStyleStrategy: ConstructibleStyleStrategy; + +function reduceStyles( styles: ReadonlyArray -) => ElementStyles; +): (string | CSSStyleSheet)[] { + return styles + .map((x: ComposableStyles) => + x instanceof ElementStyles ? reduceStyles(x.styles) : [x] + ) + .reduce((prev: string[], curr: string[]) => prev.concat(curr), []); +} /** * Represents styles that can be applied to a custom element. * @public */ -export abstract class ElementStyles { +export class ElementStyles { private targets: WeakSet = new WeakSet(); + private _strategy: StyleStrategy | null = null; - constructor( - /** @internal */ - public readonly styles: ReadonlyArray, + /** @internal */ + public readonly behaviors: ReadonlyArray> | null; + + /** + * Gets the StyleStrategy associated with these element styles. + */ + public get strategy(): StyleStrategy { + if (this._strategy === null) { + this.withStrategy(DefaultStyleStrategy); + } + + return this._strategy!; + } + + /** + * Creates an instance of ElementStyles. + * @param styles - The styles that will be associated with elements. + */ + public constructor( /** @internal */ - public readonly behaviors: ReadonlyArray> | null - ) {} + public readonly styles: ReadonlyArray + ) { + this.behaviors = styles + .map((x: ComposableStyles) => + x instanceof ElementStyles ? x.behaviors : null + ) + .reduce( + ( + prev: Behavior[] | null, + curr: Behavior[] | null + ) => (curr === null ? prev : prev === null ? curr : prev.concat(curr)), + null as Behavior[] | null + ); + } /** @internal */ public addStylesTo(target: StyleTarget): void { + this.strategy.addStylesTo(target); this.targets.add(target); } /** @internal */ public removeStylesFrom(target: StyleTarget): void { + this.strategy.removeStylesFrom(target); this.targets.delete(target); } @@ -92,53 +152,21 @@ export abstract class ElementStyles { } /** - * Create ElementStyles from ComposableStyles. + * Sets the strategy that handles adding/removing these styles for an element. + * @param strategy - The strategy to use. */ - public static readonly create: ElementStyleFactory = (() => { - if (DOM.supportsAdoptedStyleSheets) { - const styleSheetCache = new Map(); - return (styles: ComposableStyles[]) => - // eslint-disable-next-line @typescript-eslint/no-use-before-define - new AdoptedStyleSheetsStyles(styles, styleSheetCache); - } - - // eslint-disable-next-line @typescript-eslint/no-use-before-define - return (styles: ComposableStyles[]) => new StyleElementStyles(styles); - })(); -} - -function reduceStyles( - styles: ReadonlyArray -): (string | CSSStyleSheet)[] { - return styles - .map((x: ComposableStyles) => - x instanceof ElementStyles ? reduceStyles(x.styles) : [x] - ) - .reduce((prev: string[], curr: string[]) => prev.concat(curr), []); -} + public withStrategy(Strategy: ConstructibleStyleStrategy): this { + this._strategy = new Strategy(reduceStyles(this.styles)); + return this; + } -function reduceBehaviors( - styles: ReadonlyArray -): ReadonlyArray> | null { - return styles - .map((x: ComposableStyles) => (x instanceof ElementStyles ? x.behaviors : null)) - .reduce( - ( - prev: Behavior[] | null, - curr: Behavior[] | null - ) => { - if (curr === null) { - return prev; - } - - if (prev === null) { - prev = []; - } - - return prev.concat(curr); - }, - null as Behavior[] | null - ); + /** + * Sets the default strategy type to use when creating style strategies. + * @param Strategy - The strategy type to construct. + */ + public static setDefaultStrategy(Strategy: ConstructibleStyleStrategy) { + DefaultStyleStrategy = Strategy; + } } /** @@ -147,102 +175,81 @@ function reduceBehaviors( * * @internal */ -export class AdoptedStyleSheetsStyles extends ElementStyles { - private _styleSheets: CSSStyleSheet[] | undefined = void 0; - - private get styleSheets(): CSSStyleSheet[] { - if (this._styleSheets === void 0) { - const styles = this.styles; - const styleSheetCache = this.styleSheetCache; - this._styleSheets = reduceStyles(styles).map((x: string | CSSStyleSheet) => { - if (x instanceof CSSStyleSheet) { - return x; - } - - let sheet = styleSheetCache.get(x); - - if (sheet === void 0) { - sheet = new CSSStyleSheet(); - (sheet as any).replaceSync(x); - styleSheetCache.set(x, sheet); - } - - return sheet; - }); - } +export class AdoptedStyleSheetsStrategy implements StyleStrategy { + /** @internal */ + public readonly sheets: CSSStyleSheet[]; - return this._styleSheets; - } + public constructor(styles: (string | CSSStyleSheet)[]) { + this.sheets = styles.map((x: string | CSSStyleSheet) => { + if (x instanceof CSSStyleSheet) { + return x; + } - public constructor( - styles: ComposableStyles[], - private styleSheetCache: Map - ) { - super(styles, reduceBehaviors(styles)); + let sheet = styleSheetCache.get(x); + + if (sheet === void 0) { + sheet = new CSSStyleSheet(); + (sheet as any).replaceSync(x); + styleSheetCache.set(x, sheet); + } + + return sheet; + }); } public addStylesTo(target: StyleTarget): void { - target.adoptedStyleSheets = [...target.adoptedStyleSheets!, ...this.styleSheets]; - super.addStylesTo(target); + target.adoptedStyleSheets = [...target.adoptedStyleSheets!, ...this.sheets]; } public removeStylesFrom(target: StyleTarget): void { - const sourceSheets = this.styleSheets; + const sheets = this.sheets; target.adoptedStyleSheets = target.adoptedStyleSheets!.filter( - (x: CSSStyleSheet) => sourceSheets.indexOf(x) === -1 + (x: CSSStyleSheet) => sheets.indexOf(x) === -1 ); - super.removeStylesFrom(target); } } +function usableTarget(target: StyleTarget): StyleTarget { + return target === document ? document.body : target; +} + /** * @internal */ -export class StyleElementStyles extends ElementStyles { - private readonly styleSheets: string[]; +export class StyleElementStrategy implements StyleStrategy { private readonly styleClass: string; - public constructor(styles: ComposableStyles[]) { - super(styles, reduceBehaviors(styles)); - this.styleSheets = reduceStyles(styles) as string[]; + public constructor(private readonly styles: string[]) { this.styleClass = nextId(); } public addStylesTo(target: StyleTarget): void { - const styleSheets = this.styleSheets; - const styleClass = this.styleClass; + target = usableTarget(target); - target = this.normalizeTarget(target); + const styles = this.styles; + const styleClass = this.styleClass; - for (let i = 0; i < styleSheets.length; i++) { + for (let i = 0; i < styles.length; i++) { const element = document.createElement("style"); - element.innerHTML = styleSheets[i]; + element.innerHTML = styles[i]; element.className = styleClass; target.append(element); } - - super.addStylesTo(target); } public removeStylesFrom(target: StyleTarget): void { - target = this.normalizeTarget(target); - const styles: NodeListOf = target.querySelectorAll( `.${this.styleClass}` ); + target = usableTarget(target); + for (let i = 0, ii = styles.length; i < ii; ++i) { target.removeChild(styles[i]); } - - super.removeStylesFrom(target); - } - - public isAttachedTo(target: StyleTarget): boolean { - return super.isAttachedTo(this.normalizeTarget(target)); - } - - private normalizeTarget(target: StyleTarget): StyleTarget { - return target === document ? document.body : target; } } + +ElementStyles.setDefaultStrategy( + DOM.supportsAdoptedStyleSheets ? AdoptedStyleSheetsStrategy : StyleElementStrategy +); diff --git a/packages/web-components/fast-element/src/styles/styles.spec.ts b/packages/web-components/fast-element/src/styles/styles.spec.ts index 9814ddb0037..19ba0c007bc 100644 --- a/packages/web-components/fast-element/src/styles/styles.spec.ts +++ b/packages/web-components/fast-element/src/styles/styles.spec.ts @@ -1,7 +1,7 @@ import { expect } from "chai"; import { - AdoptedStyleSheetsStyles, - StyleElementStyles, + AdoptedStyleSheetsStrategy, + StyleElementStrategy, StyleTarget, ElementStyles, } from "./element-styles"; @@ -13,44 +13,41 @@ import { defaultExecutionContext } from "../observation/observable"; import type { FASTElement } from ".."; if (DOM.supportsAdoptedStyleSheets) { - describe("AdoptedStyleSheetsStyles", () => { + describe("AdoptedStyleSheetsStrategy", () => { context("when removing styles", () => { it("should remove an associated stylesheet", () => { - const cache = new Map(); - const sheet = new AdoptedStyleSheetsStyles([``], cache); + const strategy = new AdoptedStyleSheetsStrategy([``]); const target: Pick = { adoptedStyleSheets: [], }; - sheet.addStylesTo(target as StyleTarget); + strategy.addStylesTo(target as StyleTarget); expect(target.adoptedStyleSheets!.length).to.equal(1); - sheet.removeStylesFrom(target as StyleTarget); + strategy.removeStylesFrom(target as StyleTarget); expect(target.adoptedStyleSheets!.length).to.equal(0); }); it("should not remove unassociated styles", () => { - const cache = new Map(); - const sheet = new AdoptedStyleSheetsStyles(["test"], cache); + const strategy = new AdoptedStyleSheetsStrategy(["test"]); const style = new CSSStyleSheet(); const target: Pick = { adoptedStyleSheets: [style], }; - sheet.addStylesTo(target as StyleTarget); + strategy.addStylesTo(target as StyleTarget); expect(target.adoptedStyleSheets!.length).to.equal(2); - expect(target.adoptedStyleSheets).to.contain(cache.get("test")); + expect(target.adoptedStyleSheets).to.contain(strategy.sheets[0]); - sheet.removeStylesFrom(target as StyleTarget); + strategy.removeStylesFrom(target as StyleTarget); expect(target.adoptedStyleSheets!.length).to.equal(1); - expect(target.adoptedStyleSheets).not.to.contain(cache.get("test")); + expect(target.adoptedStyleSheets).not.to.contain(strategy.sheets[0]); }); it("should track when added and removed from a target", () => { - const cache = new Map(); const styles = ``; - const elementStyles = new AdoptedStyleSheetsStyles([styles], cache); + const elementStyles = new ElementStyles([styles]); const target = { adoptedStyleSheets: [], } as unknown as StyleTarget; @@ -65,9 +62,8 @@ if (DOM.supportsAdoptedStyleSheets) { }); it("should order HTMLStyleElement order by addStyleTo() call order", () => { - const cache = new Map(); - const red = new AdoptedStyleSheetsStyles(['r'], cache); - const green = new AdoptedStyleSheetsStyles(['g'], cache); + const red = new AdoptedStyleSheetsStrategy(['r']); + const green = new AdoptedStyleSheetsStrategy(['g']); const target: Pick = { adoptedStyleSheets: [], }; @@ -75,29 +71,29 @@ if (DOM.supportsAdoptedStyleSheets) { red.addStylesTo(target as StyleTarget); green.addStylesTo(target as StyleTarget); - expect((target.adoptedStyleSheets![0])).to.equal(cache.get('r')); - expect((target.adoptedStyleSheets![1])).to.equal(cache.get('g')); + expect((target.adoptedStyleSheets![0])).to.equal(red.sheets[0]); + expect((target.adoptedStyleSheets![1])).to.equal(green.sheets[0]); }); it("should order HTMLStyleElements in array order of provided sheets", () => { - const cache = new Map(); - const red = new AdoptedStyleSheetsStyles(['r', 'g'], cache); + const red = new AdoptedStyleSheetsStrategy(['r', 'g']); const target: Pick = { adoptedStyleSheets: [], }; red.addStylesTo(target as StyleTarget); - expect((target.adoptedStyleSheets![0])).to.equal(cache.get('r')); - expect((target.adoptedStyleSheets![1])).to.equal(cache.get('g')); + expect((target.adoptedStyleSheets![0])).to.equal(red.sheets[0]); + expect((target.adoptedStyleSheets![1])).to.equal(red.sheets[1]); }); }); }); } -describe("StyleSheetStyles", () => { +describe("StyleElementStrategy", () => { it("can add and remove from the document directly", () => { - const styles = ``; - const elementStyles = new StyleElementStyles([styles]); + const styles = [``]; + const elementStyles = new ElementStyles(styles) + .withStrategy(StyleElementStrategy); document.body.innerHTML = ""; elementStyles.addStylesTo(document); @@ -111,23 +107,24 @@ describe("StyleSheetStyles", () => { it("can add and remove from a ShadowRoot", () => { const styles = ``; - const elementStyles = new StyleElementStyles([styles]); + const strategy = new StyleElementStrategy([styles]); document.body.innerHTML = ""; const element = document.createElement("div"); const shadowRoot = element.attachShadow({ mode: "open" }); - elementStyles.addStylesTo(shadowRoot); + strategy.addStylesTo(shadowRoot); expect(shadowRoot.childNodes[0]).to.be.instanceof(HTMLStyleElement); - elementStyles.removeStylesFrom(shadowRoot); + strategy.removeStylesFrom(shadowRoot); expect(shadowRoot.childNodes.length).to.equal(0); }); + it("should track when added and removed from a target", () => { const styles = ``; - const elementStyles = new StyleElementStyles([styles]); + const elementStyles = new ElementStyles([styles]); document.body.innerHTML = ""; expect(elementStyles.isAttachedTo(document)).to.equal(false) @@ -140,8 +137,8 @@ describe("StyleSheetStyles", () => { }); it("should order HTMLStyleElement order by addStyleTo() call order", () => { - const red = new StyleElementStyles([`body:{color:red;}`]); - const green = new StyleElementStyles([`body:{color:green;}`]); + const red = new StyleElementStrategy([`body:{color:red;}`]); + const green = new StyleElementStrategy([`body:{color:green;}`]); document.body.innerHTML = ""; const element = document.createElement("div"); @@ -152,8 +149,9 @@ describe("StyleSheetStyles", () => { expect((shadowRoot.childNodes[0] as HTMLStyleElement).innerHTML).to.equal("body:{color:red;}"); expect((shadowRoot.childNodes[1] as HTMLStyleElement).innerHTML).to.equal("body:{color:green;}"); }); + it("should order the HTMLStyleElements in array order of provided sheets", () => { - const red = new StyleElementStyles([`body:{color:red;}`, `body:{color:green;}`]); + const red = new StyleElementStrategy([`body:{color:red;}`, `body:{color:green;}`]); document.body.innerHTML = ""; const element = document.createElement("div"); @@ -168,14 +166,14 @@ describe("StyleSheetStyles", () => { describe("ElementStyles", () => { it("can create from a string", () => { const css = ".class { color: red; }"; - const styles = ElementStyles.create([css]); + const styles = new ElementStyles([css]); expect(styles.styles).to.contain(css); }); it("can create from multiple strings", () => { const css1 = ".class { color: red; }"; const css2 = ".class2 { color: red; }"; - const styles = ElementStyles.create([css1, css2]); + const styles = new ElementStyles([css1, css2]); expect(styles.styles).to.contain(css1); expect(styles.styles.indexOf(css1)).to.equal(0); expect(styles.styles).to.contain(css2); @@ -183,17 +181,17 @@ describe("ElementStyles", () => { it("can create from an ElementStyles", () => { const css = ".class { color: red; }"; - const existingStyles = ElementStyles.create([css]); - const styles = ElementStyles.create([existingStyles]); + const existingStyles = new ElementStyles([css]); + const styles = new ElementStyles([existingStyles]); expect(styles.styles).to.contain(existingStyles); }); it("can create from multiple ElementStyles", () => { const css1 = ".class { color: red; }"; const css2 = ".class2 { color: red; }"; - const existingStyles1 = ElementStyles.create([css1]); - const existingStyles2 = ElementStyles.create([css2]); - const styles = ElementStyles.create([existingStyles1, existingStyles2]); + const existingStyles1 = new ElementStyles([css1]); + const existingStyles2 = new ElementStyles([css2]); + const styles = new ElementStyles([existingStyles1, existingStyles2]); expect(styles.styles).to.contain(existingStyles1); expect(styles.styles.indexOf(existingStyles1)).to.equal(0); expect(styles.styles).to.contain(existingStyles2); @@ -202,8 +200,8 @@ describe("ElementStyles", () => { it("can create from mixed strings and ElementStyles", () => { const css1 = ".class { color: red; }"; const css2 = ".class2 { color: red; }"; - const existingStyles2 = ElementStyles.create([css2]); - const styles = ElementStyles.create([css1, existingStyles2]); + const existingStyles2 = new ElementStyles([css2]); + const styles = new ElementStyles([css1, existingStyles2]); expect(styles.styles).to.contain(css1); expect(styles.styles.indexOf(css1)).to.equal(0); expect(styles.styles).to.contain(existingStyles2); @@ -212,14 +210,14 @@ describe("ElementStyles", () => { if (DOM.supportsAdoptedStyleSheets) { it("can create from a CSSStyleSheet", () => { const styleSheet = new CSSStyleSheet(); - const styles = ElementStyles.create([styleSheet]); + const styles = new ElementStyles([styleSheet]); expect(styles.styles).to.contain(styleSheet); }); it("can create from multiple CSSStyleSheets", () => { const styleSheet1 = new CSSStyleSheet(); const styleSheet2 = new CSSStyleSheet(); - const styles = ElementStyles.create([styleSheet1, styleSheet2]); + const styles = new ElementStyles([styleSheet1, styleSheet2]); expect(styles.styles).to.contain(styleSheet1); expect(styles.styles.indexOf(styleSheet1)).to.equal(0); expect(styles.styles).to.contain(styleSheet2); @@ -228,9 +226,9 @@ describe("ElementStyles", () => { it("can create from mixed strings, ElementStyles, and CSSStyleSheets", () => { const css1 = ".class { color: red; }"; const css2 = ".class2 { color: red; }"; - const existingStyles2 = ElementStyles.create([css2]); + const existingStyles2 = new ElementStyles([css2]); const styleSheet3 = new CSSStyleSheet(); - const styles = ElementStyles.create([css1, existingStyles2, styleSheet3]); + const styles = new ElementStyles([css1, existingStyles2, styleSheet3]); expect(styles.styles).to.contain(css1); expect(styles.styles.indexOf(css1)).to.equal(0); expect(styles.styles).to.contain(existingStyles2); From be42c7f2d81545ff672c2d9e211ff8471b411025 Mon Sep 17 00:00:00 2001 From: Jane Chu <7559015+janechu@users.noreply.github.com> Date: Tue, 22 Feb 2022 14:17:35 -0800 Subject: [PATCH 065/135] Updated api-extractor version and exported markdown (#5640) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Pull Request ## 📖 Description When running `npm run build` inside `@microsoft/fast-element` package, the following error is encountered: `Error: Error parsing tsconfig.json content: Unknown compiler option 'importsNotUsedAsValues'.` This happens because running `api-extractor` happens as a final step in the `build`, and the reason this error occurs is that the currently pinned version `typescript` used by `api-extractor` is below `3.8.0` when `importsNotUsedAsValues` was introduced as a flag. This change updates the version, we now have no errors, but the following warnings with this change: ``` Warning: You have changed the public API signature for this project. Updating docs/api-report.md Warning: dist/dts/dom.d.ts:70:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen Warning: dist/dts/observation/array-change-records.d.ts:6:33 - (tsdoc-escape-greater-than) The ">" character should be escaped using a backslash to avoid confusion with an HTML tag Warning: dist/dts/observation/array-change-records.d.ts:6:6 - (tsdoc-html-tag-missing-greater-than) The HTML tag has invalid syntax: Expecting an attribute or ">" or "/>" Warning: dist/dts/templating/binding.d.ts:4:1 - (ae-missing-release-tag) "BindingBehaviorFactory" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) Warning: dist/dts/templating/binding.d.ts:7:1 - (ae-missing-release-tag) "BindingType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) Warning: dist/dts/templating/binding.d.ts:9:1 - (ae-missing-release-tag) "BindingMode" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) Warning: dist/dts/templating/binding.d.ts:17:1 - (ae-missing-release-tag) "BindingConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) Warning: dist/dts/templating/binding.d.ts:23:1 - (ae-missing-release-tag) "DefaultBindingOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) Warning: dist/dts/templating/binding.d.ts:26:22 - (ae-missing-release-tag) "onChange" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) Warning: dist/dts/templating/binding.d.ts:27:22 - (ae-missing-release-tag) "oneTime" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) Warning: dist/dts/templating/binding.d.ts:43:1 - (ae-missing-release-tag) "bind" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) Warning: dist/dts/templating/html-directive.d.ts:84:1 - (ae-missing-release-tag) "InlinableHTMLDirective" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) ``` ### 🎫 Issues Partially addresses #5641 ## 👩‍💻 Reviewer Notes Please take a look at the above warnings and determine if these are legitimate and we should proceed with the version update. ## ✅ Checklist ### General - [ ] I have included a change request file using `$ yarn change` - [ ] I have added tests for my changes. - [x] I have tested my changes. - [ ] I have updated the project documentation to reflect my changes. - [x] I have read the [CONTRIBUTING](https://github.com/Microsoft/fast/blob/master/CONTRIBUTING.md) documentation and followed the [standards](https://www.fast.design/docs/community/code-of-conduct/#our-standards) for this project. --- ...ment-7ca9a49d-04d0-477b-8e6b-23249c19a3f2.json | 7 +++++++ .../fast-element/docs/api-report.md | 15 ++++++++------- 2 files changed, 15 insertions(+), 7 deletions(-) create mode 100644 change/@microsoft-fast-element-7ca9a49d-04d0-477b-8e6b-23249c19a3f2.json diff --git a/change/@microsoft-fast-element-7ca9a49d-04d0-477b-8e6b-23249c19a3f2.json b/change/@microsoft-fast-element-7ca9a49d-04d0-477b-8e6b-23249c19a3f2.json new file mode 100644 index 00000000000..082fe1cbb76 --- /dev/null +++ b/change/@microsoft-fast-element-7ca9a49d-04d0-477b-8e6b-23249c19a3f2.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "Updated api-extractor version and exported markdown", + "packageName": "@microsoft/fast-element", + "email": "7559015+janechu@users.noreply.github.com", + "dependentChangeType": "none" +} diff --git a/packages/web-components/fast-element/docs/api-report.md b/packages/web-components/fast-element/docs/api-report.md index 49e5989ee7f..76e05dc4ee8 100644 --- a/packages/web-components/fast-element/docs/api-report.md +++ b/packages/web-components/fast-element/docs/api-report.md @@ -49,7 +49,7 @@ export class AttributeDefinition implements Accessor { onAttributeChangedCallback(element: HTMLElement, value: any): void; readonly Owner: Function; setValue(source: HTMLElement, newValue: any): void; - } +} // @public export type AttributeMode = "reflect" | "boolean" | "fromView"; @@ -409,11 +409,13 @@ export interface ObservationRecord { propertySource: any; } +// Warning: (ae-forgotten-export) The symbol "BindingConfigResolver" needs to be exported by the entry point index.d.ts +// // @public (undocumented) -export const onChange: BindingConfig & ((options?: DefaultBindingOptions | undefined) => BindingConfig); +export const onChange: BindingConfig & BindingConfigResolver; // @public (undocumented) -export const oneTime: BindingConfig & ((options?: DefaultBindingOptions | undefined) => BindingConfig); +export const oneTime: BindingConfig & BindingConfigResolver; // @public export interface PartialFASTElementDefinition { @@ -455,14 +457,14 @@ export class RepeatBehavior implements Behavior, Subscriber { // @internal (undocumented) handleChange(source: any, args: Splice[]): void; unbind(): void; - } +} // @public export class RepeatDirective extends HTMLDirective { constructor(itemsBinding: Binding, templateBinding: Binding, options: RepeatOptions); createBehavior(targets: ViewBehaviorTargets): RepeatBehavior; createPlaceholder: (index: number) => string; - } +} // @public export interface RepeatOptions { @@ -605,7 +607,7 @@ export class ViewTemplate impl readonly directives: ReadonlyArray; readonly html: string | HTMLTemplateElement; render(source: TSource, host: Node, hostBindingTarget?: Element): HTMLView; - } +} // @public export function volatile(target: {}, name: string | Accessor, descriptor: PropertyDescriptor): PropertyDescriptor; @@ -613,7 +615,6 @@ export function volatile(target: {}, name: string | Accessor, descriptor: Proper // @public export function when(binding: Binding, templateOrTemplateBinding: SyntheticViewTemplate | Binding): CaptureType; - // (No @packageDocumentation comment for this package) ``` From 375154ff8ca522c629def4857c6bf946b0f485bd Mon Sep 17 00:00:00 2001 From: Jane Chu <7559015+janechu@users.noreply.github.com> Date: Wed, 23 Feb 2022 08:54:45 -0800 Subject: [PATCH 066/135] Update the fast-router with the updates to fast-element APIs (#5643) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Pull Request ## 📖 Description This change updates `@microsoft/fast-router` so that it uses the new APIs available from `@microsoft/fast-element`. This unblocks part of the `yarn install` errors currently happening in the build gate. ### 🎫 Issues Relates to #5641 ## 👩‍💻 Reviewer Notes @EisenbergEffect could you take a look at these changes and verify this is the intended use for the updated `ElementStyles` and `ViewBehaviorTargets`. ## ✅ Checklist ### General - [ ] I have included a change request file using `$ yarn change` - [ ] I have added tests for my changes. - [x] I have tested my changes. - [ ] I have updated the project documentation to reflect my changes. - [x] I have read the [CONTRIBUTING](https://github.com/Microsoft/fast/blob/master/CONTRIBUTING.md) documentation and followed the [standards](https://www.fast.design/docs/community/code-of-conduct/#our-standards) for this project. --- ...t-router-aa2ec3e2-c2e1-4b6c-8fbb-d42b1cd8f35a.json | 7 +++++++ .../web-components/fast-router/src/contributors.ts | 11 ++++++++--- packages/web-components/fast-router/src/view.ts | 4 ++-- 3 files changed, 17 insertions(+), 5 deletions(-) create mode 100644 change/@microsoft-fast-router-aa2ec3e2-c2e1-4b6c-8fbb-d42b1cd8f35a.json diff --git a/change/@microsoft-fast-router-aa2ec3e2-c2e1-4b6c-8fbb-d42b1cd8f35a.json b/change/@microsoft-fast-router-aa2ec3e2-c2e1-4b6c-8fbb-d42b1cd8f35a.json new file mode 100644 index 00000000000..f8f40bd2cb9 --- /dev/null +++ b/change/@microsoft-fast-router-aa2ec3e2-c2e1-4b6c-8fbb-d42b1cd8f35a.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Update the fast-router with the updates to fast-element 2.0 APIs", + "packageName": "@microsoft/fast-router", + "email": "7559015+janechu@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/packages/web-components/fast-router/src/contributors.ts b/packages/web-components/fast-router/src/contributors.ts index 9d5c77b695d..5ba154b227c 100644 --- a/packages/web-components/fast-router/src/contributors.ts +++ b/packages/web-components/fast-router/src/contributors.ts @@ -1,4 +1,9 @@ -import { Behavior, HTMLDirective, DOM, BehaviorTargets } from "@microsoft/fast-element"; +import { + Behavior, + HTMLDirective, + DOM, + ViewBehaviorTargets, +} from "@microsoft/fast-element"; import { NavigationCommitPhaseHook, NavigationPhaseHook, @@ -45,10 +50,10 @@ class NavigationContributorDirective extends HTMLDirective { } createPlaceholder(index: number) { - return DOM.createCustomAttributePlaceholder("fast-navigation-contributor", index); + return DOM.createCustomAttributePlaceholder(index); } - createBehavior(targets: BehaviorTargets) { + createBehavior(targets: ViewBehaviorTargets) { return new NavigationContributorBehavior( targets[this.targetId] as HTMLElement & NavigationContributor, this.options diff --git a/packages/web-components/fast-router/src/view.ts b/packages/web-components/fast-router/src/view.ts index 598975a9f54..834efdaaf41 100644 --- a/packages/web-components/fast-router/src/view.ts +++ b/packages/web-components/fast-router/src/view.ts @@ -95,10 +95,10 @@ export class FASTElementLayout implements Layout { styles === void 0 || styles === null ? null : Array.isArray(styles) - ? ElementStyles.create(styles) + ? new ElementStyles(styles) : styles instanceof ElementStyles ? styles - : ElementStyles.create([styles]); + : new ElementStyles([styles]); } async beforeCommit(routerElement: HTMLElement) { From 0e38b733b938819bda857c1116874e8eec003dc0 Mon Sep 17 00:00:00 2001 From: Nicholas Rice <3213292+nicholasrice@users.noreply.github.com> Date: Wed, 23 Feb 2022 10:29:33 -0800 Subject: [PATCH 067/135] fix: add null value as acceptable value to FASTElement.attributeChangedCallback (#5642) * allow null values in attributeChangedCallback * Change files Co-authored-by: nicholasrice --- ...lement-11926a01-e901-4013-acaa-a512f7281849.json | 7 +++++++ .../web-components/fast-element/docs/api-report.md | 13 +++++++------ .../fast-element/src/components/controller.ts | 4 ++-- .../fast-element/src/components/fast-element.ts | 10 +++++++--- 4 files changed, 23 insertions(+), 11 deletions(-) create mode 100644 change/@microsoft-fast-element-11926a01-e901-4013-acaa-a512f7281849.json diff --git a/change/@microsoft-fast-element-11926a01-e901-4013-acaa-a512f7281849.json b/change/@microsoft-fast-element-11926a01-e901-4013-acaa-a512f7281849.json new file mode 100644 index 00000000000..4bf75ca4b62 --- /dev/null +++ b/change/@microsoft-fast-element-11926a01-e901-4013-acaa-a512f7281849.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "allow null values in attributeChangedCallback", + "packageName": "@microsoft/fast-element", + "email": "nicholasrice@users.noreply.github.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 76e05dc4ee8..51a51cff553 100644 --- a/packages/web-components/fast-element/docs/api-report.md +++ b/packages/web-components/fast-element/docs/api-report.md @@ -49,7 +49,7 @@ export class AttributeDefinition implements Accessor { onAttributeChangedCallback(element: HTMLElement, value: any): void; readonly Owner: Function; setValue(source: HTMLElement, newValue: any): void; -} + } // @public export type AttributeMode = "reflect" | "boolean" | "fromView"; @@ -166,7 +166,7 @@ export class Controller extends PropertyChangeNotifier { emit(type: string, detail?: any, options?: Omit): void | boolean; static forCustomElement(element: HTMLElement): Controller; get isConnected(): boolean; - onAttributeChangedCallback(name: string, oldValue: string, newValue: string): void; + onAttributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void; onConnectedCallback(): void; onDisconnectedCallback(): void; removeBehaviors(behaviors: ReadonlyArray>, force?: boolean): void; @@ -285,7 +285,7 @@ export class ExecutionContext { export interface FASTElement extends HTMLElement { $emit(type: string, detail?: any, options?: Omit): boolean | void; readonly $fastController: Controller; - attributeChangedCallback(name: string, oldValue: string, newValue: string): void; + attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void; connectedCallback(): void; disconnectedCallback(): void; } @@ -457,14 +457,14 @@ export class RepeatBehavior implements Behavior, Subscriber { // @internal (undocumented) handleChange(source: any, args: Splice[]): void; unbind(): void; -} + } // @public export class RepeatDirective extends HTMLDirective { constructor(itemsBinding: Binding, templateBinding: Binding, options: RepeatOptions); createBehavior(targets: ViewBehaviorTargets): RepeatBehavior; createPlaceholder: (index: number) => string; -} + } // @public export interface RepeatOptions { @@ -607,7 +607,7 @@ export class ViewTemplate impl readonly directives: ReadonlyArray; readonly html: string | HTMLTemplateElement; render(source: TSource, host: Node, hostBindingTarget?: Element): HTMLView; -} + } // @public export function volatile(target: {}, name: string | Accessor, descriptor: PropertyDescriptor): PropertyDescriptor; @@ -615,6 +615,7 @@ export function volatile(target: {}, name: string | Accessor, descriptor: Proper // @public export function when(binding: Binding, templateOrTemplateBinding: SyntheticViewTemplate | Binding): CaptureType; + // (No @packageDocumentation comment for this package) ``` diff --git a/packages/web-components/fast-element/src/components/controller.ts b/packages/web-components/fast-element/src/components/controller.ts index c6d1aeef58d..5f48328ed1d 100644 --- a/packages/web-components/fast-element/src/components/controller.ts +++ b/packages/web-components/fast-element/src/components/controller.ts @@ -333,8 +333,8 @@ export class Controller extends PropertyChangeNotifier { */ public onAttributeChangedCallback( name: string, - oldValue: string, - newValue: string + oldValue: string | null, + newValue: string | null ): void { const attrDef = this.definition.attributeLookup[name]; diff --git a/packages/web-components/fast-element/src/components/fast-element.ts b/packages/web-components/fast-element/src/components/fast-element.ts index 71946541456..0053801dee8 100644 --- a/packages/web-components/fast-element/src/components/fast-element.ts +++ b/packages/web-components/fast-element/src/components/fast-element.ts @@ -54,7 +54,11 @@ export interface FASTElement extends HTMLElement { * This method is invoked by the platform whenever an observed * attribute of FASTElement has a value change. */ - attributeChangedCallback(name: string, oldValue: string, newValue: string): void; + attributeChangedCallback( + name: string, + oldValue: string | null, + newValue: string | null + ): void; } /* eslint-disable-next-line @typescript-eslint/explicit-function-return-type */ @@ -88,8 +92,8 @@ function createFASTElement( public attributeChangedCallback( name: string, - oldValue: string, - newValue: string + oldValue: string | null, + newValue: string | null ): void { this.$fastController.onAttributeChangedCallback(name, oldValue, newValue); } From 82cb590f66d0c2208faba5ed37863e53ce9d6fba Mon Sep 17 00:00:00 2001 From: Jane Chu <7559015+janechu@users.noreply.github.com> Date: Wed, 23 Feb 2022 12:54:01 -0800 Subject: [PATCH 068/135] Updated to use the new FAST element 2.0 APIs for creating behaviors and creating element styles (#5648) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Pull Request ## 📖 Description This work updates components in `@microsoft/fast-foundation` to use updated APIs from FAST Element 2.0. ### 🎫 Issues Relates to #5641 ## ✅ Checklist ### General - [x] I have included a change request file using `$ yarn change` - [ ] I have added tests for my changes. - [x] I have tested my changes. - [ ] I have updated the project documentation to reflect my changes. - [x] I have read the [CONTRIBUTING](https://github.com/Microsoft/fast/blob/master/CONTRIBUTING.md) documentation and followed the [standards](https://www.fast.design/docs/community/code-of-conduct/#our-standards) for this project. --- ...ation-3f378ae2-8f62-4b34-a458-937dc73de19e.json | 7 +++++++ .../fast-foundation/src/data-grid/data-grid-row.ts | 7 +++++-- .../fast-foundation/src/data-grid/data-grid.ts | 7 +++++-- .../src/design-system/component-presentation.ts | 4 ++-- .../src/design-token/custom-property-manager.ts | 2 +- .../fast-foundation/src/picker/picker.ts | 14 ++++++++++---- 6 files changed, 30 insertions(+), 11 deletions(-) create mode 100644 change/@microsoft-fast-foundation-3f378ae2-8f62-4b34-a458-937dc73de19e.json diff --git a/change/@microsoft-fast-foundation-3f378ae2-8f62-4b34-a458-937dc73de19e.json b/change/@microsoft-fast-foundation-3f378ae2-8f62-4b34-a458-937dc73de19e.json new file mode 100644 index 00000000000..6c9c50060fe --- /dev/null +++ b/change/@microsoft-fast-foundation-3f378ae2-8f62-4b34-a458-937dc73de19e.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Updated to use the new FAST element 2.0 APIs for creating behaviors and creating element styles", + "packageName": "@microsoft/fast-foundation", + "email": "7559015+janechu@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/packages/web-components/fast-foundation/src/data-grid/data-grid-row.ts b/packages/web-components/fast-foundation/src/data-grid/data-grid-row.ts index 221e5b1e9bd..d3a09b4a895 100644 --- a/packages/web-components/fast-foundation/src/data-grid/data-grid-row.ts +++ b/packages/web-components/fast-foundation/src/data-grid/data-grid-row.ts @@ -177,11 +177,14 @@ export class DataGridRow extends FoundationElement { this.updateItemTemplate(); - this.cellsRepeatBehavior = new RepeatDirective( + const cellsRepeatDirective = new RepeatDirective( x => x.columnDefinitions, x => x.activeCellItemTemplate, { positioning: true } - ).createBehavior(this.cellsPlaceholder); + ); + this.cellsRepeatBehavior = cellsRepeatDirective.createBehavior({ + [cellsRepeatDirective.targetId]: this.cellsPlaceholder, + }); /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */ this.$fastController.addBehaviors([this.cellsRepeatBehavior!]); } diff --git a/packages/web-components/fast-foundation/src/data-grid/data-grid.ts b/packages/web-components/fast-foundation/src/data-grid/data-grid.ts index f022a2c5464..ae2f2ea32ce 100644 --- a/packages/web-components/fast-foundation/src/data-grid/data-grid.ts +++ b/packages/web-components/fast-foundation/src/data-grid/data-grid.ts @@ -340,11 +340,14 @@ export class DataGrid extends FoundationElement { this.toggleGeneratedHeader(); - this.rowsRepeatBehavior = new RepeatDirective( + const rowsRepeatDirective = new RepeatDirective( x => x.rowsData, x => x.rowItemTemplate, { positioning: true } - ).createBehavior(this.rowsPlaceholder); + ); + this.rowsRepeatBehavior = rowsRepeatDirective.createBehavior({ + [rowsRepeatDirective.targetId]: this.rowsPlaceholder, + }); /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */ this.$fastController.addBehaviors([this.rowsRepeatBehavior!]); diff --git a/packages/web-components/fast-foundation/src/design-system/component-presentation.ts b/packages/web-components/fast-foundation/src/design-system/component-presentation.ts index 1f722a1f14d..ea9a7a138f6 100644 --- a/packages/web-components/fast-foundation/src/design-system/component-presentation.ts +++ b/packages/web-components/fast-foundation/src/design-system/component-presentation.ts @@ -109,10 +109,10 @@ export class DefaultComponentPresentation implements ComponentPresentation { styles === void 0 ? null : Array.isArray(styles) - ? ElementStyles.create(styles) + ? new ElementStyles(styles) : styles instanceof ElementStyles ? styles - : ElementStyles.create([styles]); + : new ElementStyles([styles]); } /** diff --git a/packages/web-components/fast-foundation/src/design-token/custom-property-manager.ts b/packages/web-components/fast-foundation/src/design-token/custom-property-manager.ts index c3fe1ed1f9a..2ecb8899f00 100644 --- a/packages/web-components/fast-foundation/src/design-token/custom-property-manager.ts +++ b/packages/web-components/fast-foundation/src/design-token/custom-property-manager.ts @@ -39,7 +39,7 @@ class ConstructableStyleSheetTarget extends QueuedStyleSheetTarget { const sheet = new CSSStyleSheet(); this.target = (sheet.cssRules[sheet.insertRule(":host{}")] as CSSStyleRule).style; - source.$fastController.addStyles(ElementStyles.create([sheet])); + source.$fastController.addStyles(new ElementStyles([sheet])); } } diff --git a/packages/web-components/fast-foundation/src/picker/picker.ts b/packages/web-components/fast-foundation/src/picker/picker.ts index e210b65507d..db046885c2d 100644 --- a/packages/web-components/fast-foundation/src/picker/picker.ts +++ b/packages/web-components/fast-foundation/src/picker/picker.ts @@ -539,11 +539,14 @@ export class Picker extends FormAssociatedPicker { this.updateListItemTemplate(); this.updateOptionTemplate(); - this.itemsRepeatBehavior = new RepeatDirective( + const itemsRepeatDirective = new RepeatDirective( x => x.selectedItems, x => x.activeListItemTemplate, { positioning: true } - ).createBehavior(this.itemsPlaceholderElement); + ); + this.itemsRepeatBehavior = itemsRepeatDirective.createBehavior({ + [itemsRepeatDirective.targetId]: this.itemsPlaceholderElement, + }); this.inputElement.addEventListener("input", this.handleTextInput); this.inputElement.addEventListener("click", this.handleInputClick); @@ -556,11 +559,14 @@ export class Picker extends FormAssociatedPicker { this.handleMenuOptionsUpdated ); - this.optionsRepeatBehavior = new RepeatDirective( + const optionsRepeatDirective = new RepeatDirective( x => x.filteredOptionsList, x => x.activeMenuOptionTemplate, { positioning: true } - ).createBehavior(this.optionsPlaceholder); + ); + this.optionsRepeatBehavior = optionsRepeatDirective.createBehavior({ + [optionsRepeatDirective.targetId]: this.optionsPlaceholder, + }); /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */ this.$fastController.addBehaviors([this.optionsRepeatBehavior!]); From 7303eeeaca2aef5e217c89c1c1ecaf98cc1c7154 Mon Sep 17 00:00:00 2001 From: Jane Chu <7559015+janechu@users.noreply.github.com> Date: Wed, 23 Feb 2022 13:05:20 -0800 Subject: [PATCH 069/135] Update the API report based on foundation updates for FAST element 2.0 (#5650) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Pull Request ## 📖 Description This api report update comes after the work in #5648 where the installation was finally able to happen, this now allowed for the updates in FAST element 2.0 to update the api report for `@microsoft/fast-components`. ## ✅ Checklist ### General - [ ] I have included a change request file using `$ yarn change` - [ ] I have added tests for my changes. - [x] I have tested my changes. - [x] I have updated the project documentation to reflect my changes. - [x] I have read the [CONTRIBUTING](https://github.com/Microsoft/fast/blob/master/CONTRIBUTING.md) documentation and followed the [standards](https://www.fast.design/docs/community/code-of-conduct/#our-standards) for this project. --- ...st-components-45865c97-bd37-4f7e-9658-a87af2ae2a75.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/@microsoft-fast-components-45865c97-bd37-4f7e-9658-a87af2ae2a75.json diff --git a/change/@microsoft-fast-components-45865c97-bd37-4f7e-9658-a87af2ae2a75.json b/change/@microsoft-fast-components-45865c97-bd37-4f7e-9658-a87af2ae2a75.json new file mode 100644 index 00000000000..d30c60638ec --- /dev/null +++ b/change/@microsoft-fast-components-45865c97-bd37-4f7e-9658-a87af2ae2a75.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "Update the API report based on foundation updates for FAST element 2.0", + "packageName": "@microsoft/fast-components", + "email": "7559015+janechu@users.noreply.github.com", + "dependentChangeType": "none" +} From a350e008b8f31287cca0e4ad22699a769b0c3aa1 Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Wed, 23 Feb 2022 17:03:02 -0500 Subject: [PATCH 070/135] feat: expose official Markup and Parser APIs (#5649) * feat: expose official Markup and Parser APIs * Change files Co-authored-by: EisenbergEffect --- ...-725e8774-1f96-49df-a63d-64d0b4bf5d6b.json | 7 + .../fast-element/docs/api-report.md | 22 ++- .../web-components/fast-element/src/dom.ts | 63 +------- .../web-components/fast-element/src/index.ts | 1 + .../src/observation/array-change-records.ts | 4 +- .../fast-element/src/styles/element-styles.ts | 3 +- .../src/templating/compiler.spec.ts | 5 +- .../fast-element/src/templating/compiler.ts | 83 ++-------- .../src/templating/html-directive.ts | 11 +- .../fast-element/src/templating/markup.ts | 150 ++++++++++++++++++ .../fast-element/src/templating/repeat.ts | 4 +- .../src/templating/template.spec.ts | 72 ++++----- 12 files changed, 238 insertions(+), 187 deletions(-) create mode 100644 change/@microsoft-fast-element-725e8774-1f96-49df-a63d-64d0b4bf5d6b.json create mode 100644 packages/web-components/fast-element/src/templating/markup.ts diff --git a/change/@microsoft-fast-element-725e8774-1f96-49df-a63d-64d0b4bf5d6b.json b/change/@microsoft-fast-element-725e8774-1f96-49df-a63d-64d0b4bf5d6b.json new file mode 100644 index 00000000000..e1f521c8323 --- /dev/null +++ b/change/@microsoft-fast-element-725e8774-1f96-49df-a63d-64d0b4bf5d6b.json @@ -0,0 +1,7 @@ +{ + "type": "major", + "comment": "feat: expose official Markup and Parser APIs", + "packageName": "@microsoft/fast-element", + "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 51a51cff553..e38a9a06d73 100644 --- a/packages/web-components/fast-element/docs/api-report.md +++ b/packages/web-components/fast-element/docs/api-report.md @@ -209,11 +209,6 @@ export const DOM: Readonly<{ supportsAdoptedStyleSheets: boolean; setHTMLPolicy(policy: TrustedTypesPolicy): void; createHTML(html: string): string; - isMarker(node: Node): node is Comment; - extractDirectiveIndexFromMarker(node: Comment): number; - createInterpolationPlaceholder(index: number): string; - createCustomAttributePlaceholder(index: number): string; - createBlockPlaceholder(index: number): string; setUpdateMode(isAsync: boolean): void; queueUpdate(callable: Callable): void; nextUpdate(): Promise; @@ -355,7 +350,7 @@ export class HTMLView implemen unbind(): void; } -// @public (undocumented) +// @public export abstract class InlinableHTMLDirective extends AspectedHTMLDirective { // (undocumented) abstract readonly binding: Binding; @@ -363,6 +358,15 @@ export abstract class InlinableHTMLDirective extends AspectedHTMLDirective { abstract readonly rawAspect?: string; } +// @public +export const Markup: Readonly<{ + marker: string; + interpolation(index: number): string; + attribute(index: number): string; + comment(index: number): string; + indexFromComment(node: Comment): number; +}>; + // Warning: (ae-internal-missing-underscore) The name "Mutable" should be prefixed with an underscore because the declaration is marked as @internal // // @internal @@ -417,6 +421,12 @@ export const onChange: BindingConfig & BindingConfigResol // @public (undocumented) export const oneTime: BindingConfig & BindingConfigResolver; +// @public +export const Parser: Readonly<{ + parse(value: string, directives: readonly HTMLDirective[]): (string | HTMLDirective)[] | null; + aggregate(parts: (string | HTMLDirective)[]): InlinableHTMLDirective; +}>; + // @public export interface PartialFASTElementDefinition { readonly attributes?: (AttributeConfiguration | string)[]; diff --git a/packages/web-components/fast-element/src/dom.ts b/packages/web-components/fast-element/src/dom.ts index 34d5fc660eb..0c9fa1c31ce 100644 --- a/packages/web-components/fast-element/src/dom.ts +++ b/packages/web-components/fast-element/src/dom.ts @@ -36,18 +36,6 @@ function tryRunTask(task: Callable): void { } } -const marker = `fast-${Math.random().toString(36).substring(2, 8)}`; -let id = 0; - -/** @internal */ -export const nextId = (): string => `${marker}-${++id}`; - -/** @internal */ -export const _interpolationStart = `${marker}{`; - -/** @internal */ -export const _interpolationEnd = `}${marker}`; - /** * Common DOM APIs. * @public @@ -86,58 +74,9 @@ export const DOM = Object.freeze({ return htmlPolicy.createHTML(html); }, - /** - * Determines if the provided node is a template marker used by the runtime. - * @param node - The node to test. - */ - isMarker(node: Node): node is Comment { - return node && node.nodeType === 8 && (node as Comment).data.startsWith(marker); - }, - - /** - * Given a marker node, extract the {@link HTMLDirective} index from the placeholder. - * @param node - The marker node to extract the index from. - */ - extractDirectiveIndexFromMarker(node: Comment): number { - return parseInt(node.data.replace(`${marker}:`, "")); - }, - - /** - * Creates a placeholder string suitable for marking out a location *within* - * an attribute value or HTML content. - * @param index - The directive index to create the placeholder for. - * @remarks - * Used internally by binding directives. - */ - createInterpolationPlaceholder(index: number): string { - return `${_interpolationStart}${index}${_interpolationEnd}`; - }, - - /** - * Creates a placeholder that manifests itself as an attribute on an - * element. - * @param attributeName - The name of the custom attribute. - * @param index - The directive index to create the placeholder for. - * @remarks - * Used internally by attribute directives such as `ref`, `slotted`, and `children`. - */ - createCustomAttributePlaceholder(index: number): string { - return `${nextId()}="${this.createInterpolationPlaceholder(index)}"`; - }, - - /** - * Creates a placeholder that manifests itself as a marker within the DOM structure. - * @param index - The directive index to create the placeholder for. - * @remarks - * Used internally by structural directives such as `repeat`. - */ - createBlockPlaceholder(index: number): string { - return ``; - }, - /** * Sets the update mode used by queueUpdate. - * @param isAsync Indicates whether DOM updates should be asynchronous. + * @param isAsync - Indicates whether DOM updates should be asynchronous. * @remarks * By default, the update mode is asynchronous, since that provides the best * performance in the browser. Passing false to setUpdateMode will instead cause diff --git a/packages/web-components/fast-element/src/index.ts b/packages/web-components/fast-element/src/index.ts index ee4218147c8..a947c4cb003 100644 --- a/packages/web-components/fast-element/src/index.ts +++ b/packages/web-components/fast-element/src/index.ts @@ -24,6 +24,7 @@ export { Splice } from "./observation/array-change-records.js"; export { enableArrayObservation } from "./observation/array-observer.js"; export { DOM } from "./dom.js"; export type { Behavior } from "./observation/behavior.js"; +export { Markup, Parser } from "./templating/markup.js"; export { bind, oneTime, diff --git a/packages/web-components/fast-element/src/observation/array-change-records.ts b/packages/web-components/fast-element/src/observation/array-change-records.ts index 2dfe6ce5221..33bc6ad55ce 100644 --- a/packages/web-components/fast-element/src/observation/array-change-records.ts +++ b/packages/web-components/fast-element/src/observation/array-change-records.ts @@ -13,7 +13,7 @@ const enum Edit { // followed by an add. By retaining this, we optimize for "keeping" the // maximum array items in the original array. For example: // -// 'xxxx123' -> '123yyyy' +// 'xxxx123' to '123yyyy' // // With 1-edit updates, the shortest path would be just to update all seven // characters. With 2-edit updates, we delete 4, leave 3, and add 4. This @@ -169,7 +169,7 @@ function intersect(start1: number, end1: number, start2: number, end2: number): * 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 diff --git a/packages/web-components/fast-element/src/styles/element-styles.ts b/packages/web-components/fast-element/src/styles/element-styles.ts index 2b6e5c61cd3..9743105fb72 100644 --- a/packages/web-components/fast-element/src/styles/element-styles.ts +++ b/packages/web-components/fast-element/src/styles/element-styles.ts @@ -1,5 +1,6 @@ import type { Behavior } from "../observation/behavior.js"; -import { DOM, nextId } from "../dom.js"; +import { DOM } from "../dom.js"; +import { nextId } from "../templating/markup.js"; /** * A node that can be targeted by styles. diff --git a/packages/web-components/fast-element/src/templating/compiler.spec.ts b/packages/web-components/fast-element/src/templating/compiler.spec.ts index cfeca6b29be..4b0599d72a1 100644 --- a/packages/web-components/fast-element/src/templating/compiler.spec.ts +++ b/packages/web-components/fast-element/src/templating/compiler.spec.ts @@ -1,6 +1,7 @@ import { expect } from "chai"; -import { customElement, FASTElement } from "../components/fast-element"; import { DOM } from "../dom"; +import { customElement, FASTElement } from "../components/fast-element"; +import { Markup } from './markup'; import { defaultExecutionContext } from "../observation/observable"; import { css } from "../styles/css"; import type { StyleTarget } from "../styles/element-styles"; @@ -18,7 +19,7 @@ describe("The template compiler", () => { } function inline(index: number) { - return DOM.createInterpolationPlaceholder(index); + return Markup.interpolation(index); } function binding(result = "result") { diff --git a/packages/web-components/fast-element/src/templating/compiler.ts b/packages/web-components/fast-element/src/templating/compiler.ts index bee114952e9..6c832dad180 100644 --- a/packages/web-components/fast-element/src/templating/compiler.ts +++ b/packages/web-components/fast-element/src/templating/compiler.ts @@ -1,11 +1,9 @@ -import { _interpolationEnd, _interpolationStart, DOM } from "../dom.js"; +import { Markup, Parser } from "./markup.js"; import { isString } from "../interfaces.js"; -import type { ExecutionContext } from "../observation/observable.js"; import { bind, oneTime } from "./binding.js"; import type { AspectedHTMLDirective, HTMLDirective, - InlinableHTMLDirective, ViewBehaviorFactory, ViewBehaviorTargets, } from "./html-directive.js"; @@ -133,70 +131,10 @@ class CompilationContext implements HTMLTemplateCompilationResult { } } -function createAggregateBinding(parts: (string | HTMLDirective)[]): HTMLDirective { - if (parts.length === 1) { - return parts[0] as HTMLDirective; - } - - let aspect: string | undefined; - const partCount = parts.length; - const finalParts = parts.map((x: string | InlinableHTMLDirective) => { - if (isString(x)) { - return (): string => x; - } - - aspect = x.rawAspect || aspect; - return x.binding; - }); - - const binding = (scope: unknown, context: ExecutionContext): string => { - let output = ""; - - for (let i = 0; i < partCount; ++i) { - output += finalParts[i](scope, context); - } - - return output; - }; - - const directive = bind(binding) as AspectedHTMLDirective; - directive.setAspect(aspect!); - return directive; -} - -const interpolationEndLength = _interpolationEnd.length; - -function parseContent( - context: CompilationContext, - value: string -): (string | HTMLDirective)[] | null { - const valueParts = value.split(_interpolationStart); - - if (valueParts.length === 1) { - return null; - } - - const bindingParts: any[] = []; - - for (let i = 0, ii = valueParts.length; i < ii; ++i) { - const current = valueParts[i]; - const index = current.indexOf(_interpolationEnd); - let literal; - - if (index === -1) { - literal = current; - } else { - const directiveIndex = parseInt(current.substring(0, index)); - bindingParts.push(context.directives[directiveIndex]); - literal = current.substring(index + interpolationEndLength); - } - - if (literal !== "") { - bindingParts.push(literal); - } - } +const marker = Markup.marker; - return bindingParts; +function isMarker(node: Node): node is Comment { + return node && node.nodeType === 8 && (node as Comment).data.startsWith(marker); } function compileAttributes( @@ -208,11 +146,12 @@ function compileAttributes( includeBasicValues: boolean = false ): void { const attributes = node.attributes; + const directives = context.directives; for (let i = 0, ii = attributes.length; i < ii; ++i) { const attr = attributes[i]; const attrValue = attr.value; - const parseResult = parseContent(context, attrValue); + const parseResult = Parser.parse(attrValue, directives); let result: HTMLDirective | null = null; if (parseResult === null) { @@ -221,7 +160,7 @@ function compileAttributes( (result as AspectedHTMLDirective).setAspect(attr.name); } } else { - result = createAggregateBinding(parseResult); + result = Parser.aggregate(parseResult); } if (result !== null) { @@ -240,7 +179,7 @@ function compileContent( nodeId, nodeIndex ): NextNode { - const parseResult = parseContent(context, node.textContent!); + const parseResult = Parser.parse(node.textContent!, context.directives); if (parseResult === null) { next.node = node.nextSibling; next.index = nodeIndex + 1; @@ -309,9 +248,9 @@ function compileNode( case 3: // text node return compileContent(context, node as Text, parentId, nodeId, nodeIndex); case 8: // comment - if (DOM.isMarker(node)) { + if (isMarker(node)) { context.addFactory( - context.directives[DOM.extractDirectiveIndexFromMarker(node)], + context.directives[Markup.indexFromComment(node)], parentId, nodeId, nodeIndex @@ -351,7 +290,7 @@ export function compileTemplate( // because something like a when, repeat, etc. could add nodes before the marker. // To mitigate this, we insert a stable first node. However, if we insert a node, // that will alter the result of the TreeWalker. So, we also need to offset the target index. - DOM.isMarker(fragment.firstChild!) || + isMarker(fragment.firstChild!) || // Or if there is only one node, it means the template's content // is *only* the directive. In that case, HTMLView.dispose() misses any nodes inserted by // the directive. Inserting a new node ensures proper disposal of nodes added by the directive. 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 47071acc2d3..81a29327c6e 100644 --- a/packages/web-components/fast-element/src/templating/html-directive.ts +++ b/packages/web-components/fast-element/src/templating/html-directive.ts @@ -1,4 +1,4 @@ -import { DOM, nextId } from "../dom.js"; +import { Markup, nextId } from "./markup.js"; import type { Behavior } from "../observation/behavior.js"; import type { Binding, ExecutionContext } from "../observation/observable.js"; @@ -99,10 +99,13 @@ export abstract class AspectedHTMLDirective extends HTMLDirective { * Creates a placeholder string based on the directive's index within the template. * @param index - The index of the directive within the template. */ - public createPlaceholder: (index: number) => string = - DOM.createInterpolationPlaceholder; + public createPlaceholder: (index: number) => string = Markup.interpolation; } +/** + * A {@link HTMLDirective} that can be inlined within an attribute or text content. + * @public + */ export abstract class InlinableHTMLDirective extends AspectedHTMLDirective { abstract readonly binding: Binding; abstract readonly rawAspect?: string; @@ -134,7 +137,7 @@ export abstract class StatelessAttachedAttributeDirective extends HTMLDirecti * Creates a custom attribute placeholder. */ public createPlaceholder(index: number): string { - return DOM.createCustomAttributePlaceholder(index); + return Markup.attribute(index); } /** diff --git a/packages/web-components/fast-element/src/templating/markup.ts b/packages/web-components/fast-element/src/templating/markup.ts new file mode 100644 index 00000000000..8c66bcf54cd --- /dev/null +++ b/packages/web-components/fast-element/src/templating/markup.ts @@ -0,0 +1,150 @@ +import { isString } from "../interfaces.js"; +import type { ExecutionContext } from "../observation/observable.js"; +import { bind } from "./binding.js"; +import type { HTMLDirective, InlinableHTMLDirective } from "./html-directive.js"; + +const marker = `fast-${Math.random().toString(36).substring(2, 8)}`; +const interpolationStart = `${marker}{`; +const interpolationEnd = `}${marker}`; +const interpolationEndLength = interpolationEnd.length; + +let id = 0; + +/** @internal */ +export const nextId = (): string => `${marker}-${++id}`; + +/** + * Common APIs related to markup generation. + * @public + */ +export const Markup = Object.freeze({ + /** + * Gets the unique marker used by FAST to annotate templates. + */ + marker, + + /** + * Creates a placeholder string suitable for marking out a location *within* + * an attribute value or HTML content. + * @param index - The directive index to create the placeholder for. + * @remarks + * Used internally by binding directives. + */ + interpolation(index: number): string { + return `${interpolationStart}${index}${interpolationEnd}`; + }, + + /** + * Creates a placeholder that manifests itself as an attribute on an + * element. + * @param attributeName - The name of the custom attribute. + * @param index - The directive index to create the placeholder for. + * @remarks + * Used internally by attribute directives such as `ref`, `slotted`, and `children`. + */ + attribute(index: number): string { + return `${nextId()}="${this.interpolation(index)}"`; + }, + + /** + * Creates a placeholder that manifests itself as a marker within the DOM structure. + * @param index - The directive index to create the placeholder for. + * @remarks + * Used internally by structural directives such as `repeat`. + */ + comment(index: number): string { + return ``; + }, + + /** + * Given a marker node, extract the {@link HTMLDirective} index from the placeholder. + * @param node - The marker node to extract the index from. + */ + indexFromComment(node: Comment): number { + return parseInt(node.data.replace(`${marker}:`, "")); + }, +}); + +/** + * Common APIs related to content parsing. + * @public + */ +export const Parser = Object.freeze({ + /** + * Parses text content or HTML attribute content, separating out the static strings + * from the directives. + * @param value - The content or attribute string to parse. + * @param directives - A list of directives to search for in the string. + * @returns A heterogeneous array of static strings interspersed with + * directives or null if no directives are found in the string. + */ + parse( + value: string, + directives: readonly HTMLDirective[] + ): (string | HTMLDirective)[] | null { + const valueParts = value.split(interpolationStart); + + if (valueParts.length === 1) { + return null; + } + + const bindingParts: (string | HTMLDirective)[] = []; + + for (let i = 0, ii = valueParts.length; i < ii; ++i) { + const current = valueParts[i]; + const index = current.indexOf(interpolationEnd); + let literal: string | HTMLDirective; + + if (index === -1) { + literal = current; + } else { + const directiveIndex = parseInt(current.substring(0, index)); + bindingParts.push(directives[directiveIndex]); + literal = current.substring(index + interpolationEndLength); + } + + if (literal !== "") { + bindingParts.push(literal); + } + } + + return bindingParts; + }, + + /** + * Aggregates an array of strings and directives into a single directive. + * @param parts - A heterogeneous array of static strings interspersed with + * directives. + * @returns A single inline directive that aggregates the behavior of all the parts. + */ + aggregate(parts: (string | HTMLDirective)[]): InlinableHTMLDirective { + if (parts.length === 1) { + return parts[0] as InlinableHTMLDirective; + } + + let aspect: string | undefined; + const partCount = parts.length; + const finalParts = parts.map((x: string | InlinableHTMLDirective) => { + if (isString(x)) { + return (): string => x; + } + + aspect = x.rawAspect || aspect; + return x.binding; + }); + + const binding = (scope: unknown, context: ExecutionContext): string => { + let output = ""; + + for (let i = 0; i < partCount; ++i) { + output += finalParts[i](scope, context); + } + + return output; + }; + + const directive = bind(binding) as InlinableHTMLDirective; + directive.setAspect(aspect!); + return directive; + }, +}); diff --git a/packages/web-components/fast-element/src/templating/repeat.ts b/packages/web-components/fast-element/src/templating/repeat.ts index fce7b46d902..44c9f3237e2 100644 --- a/packages/web-components/fast-element/src/templating/repeat.ts +++ b/packages/web-components/fast-element/src/templating/repeat.ts @@ -1,4 +1,4 @@ -import { DOM } from "../dom.js"; +import { Markup } from "./markup.js"; import { isFunction } from "../interfaces.js"; import type { Splice } from "../observation/array-change-records.js"; import { enableArrayObservation } from "../observation/array-observer.js"; @@ -307,7 +307,7 @@ export class RepeatDirective extends HTMLDirective { * Creates a placeholder string based on the directive's index within the template. * @param index - The index of the directive within the template. */ - public createPlaceholder: (index: number) => string = DOM.createBlockPlaceholder; + public createPlaceholder: (index: number) => string = Markup.comment; /** * Creates an instance of RepeatDirective. diff --git a/packages/web-components/fast-element/src/templating/template.spec.ts b/packages/web-components/fast-element/src/templating/template.spec.ts index f436b932716..01fb18610c1 100644 --- a/packages/web-components/fast-element/src/templating/template.spec.ts +++ b/packages/web-components/fast-element/src/templating/template.spec.ts @@ -1,8 +1,8 @@ import { expect } from "chai"; import { html, ViewTemplate } from "./template"; -import { DOM } from "../dom"; +import { Markup } from "./markup"; import { HTMLBindingDirective } from "./binding"; -import { HTMLDirective, AspectedHTMLDirective } from "./html-directive"; +import { HTMLDirective } from "./html-directive"; import { bind, Binding, InlinableHTMLDirective, ViewBehaviorTargets } from ".."; describe(`The html tag template helper`, () => { @@ -17,7 +17,7 @@ describe(`The html tag template helper`, () => { } createPlaceholder(index: number) { - return DOM.createBlockPlaceholder(index); + return Markup.comment(index); } } @@ -51,40 +51,40 @@ describe(`The html tag template helper`, () => { type: "number", location: "at the beginning", template: html`${numberValue} end`, - result: `${DOM.createInterpolationPlaceholder(0)} end`, + result: `${Markup.interpolation(0)} end`, }, { type: "number", location: "in the middle", template: html`beginning ${numberValue} end`, - result: `beginning ${DOM.createInterpolationPlaceholder(0)} end`, + result: `beginning ${Markup.interpolation(0)} end`, }, { type: "number", location: "at the end", template: html`beginning ${numberValue}`, - result: `beginning ${DOM.createInterpolationPlaceholder(0)}`, + result: `beginning ${Markup.interpolation(0)}`, }, // expression interpolation { type: "expression", location: "at the beginning", template: html`${x => x.value} end`, - result: `${DOM.createInterpolationPlaceholder(0)} end`, + result: `${Markup.interpolation(0)} end`, expectDirectives: [HTMLBindingDirective], }, { type: "expression", location: "in the middle", template: html`beginning ${x => x.value} end`, - result: `beginning ${DOM.createInterpolationPlaceholder(0)} end`, + result: `beginning ${Markup.interpolation(0)} end`, expectDirectives: [HTMLBindingDirective], }, { type: "expression", location: "at the end", template: html`beginning ${x => x.value}`, - result: `beginning ${DOM.createInterpolationPlaceholder(0)}`, + result: `beginning ${Markup.interpolation(0)}`, expectDirectives: [HTMLBindingDirective], }, // directive interpolation @@ -92,21 +92,21 @@ describe(`The html tag template helper`, () => { type: "directive", location: "at the beginning", template: html`${new TestDirective()} end`, - result: `${DOM.createBlockPlaceholder(0)} end`, + result: `${Markup.comment(0)} end`, expectDirectives: [TestDirective], }, { type: "directive", location: "in the middle", template: html`beginning ${new TestDirective()} end`, - result: `beginning ${DOM.createBlockPlaceholder(0)} end`, + result: `beginning ${Markup.comment(0)} end`, expectDirectives: [TestDirective], }, { type: "directive", location: "at the end", template: html`beginning ${new TestDirective()}`, - result: `beginning ${DOM.createBlockPlaceholder(0)}`, + result: `beginning ${Markup.comment(0)}`, expectDirectives: [TestDirective], }, // template interpolation @@ -114,21 +114,21 @@ describe(`The html tag template helper`, () => { type: "template", location: "at the beginning", template: html`${html`sub-template`} end`, - result: `${DOM.createInterpolationPlaceholder(0)} end`, + result: `${Markup.interpolation(0)} end`, expectDirectives: [HTMLBindingDirective], }, { type: "template", location: "in the middle", template: html`beginning ${html`sub-template`} end`, - result: `beginning ${DOM.createInterpolationPlaceholder(0)} end`, + result: `beginning ${Markup.interpolation(0)} end`, expectDirectives: [HTMLBindingDirective], }, { type: "template", location: "at the end", template: html`beginning ${html`sub-template`}`, - result: `beginning ${DOM.createInterpolationPlaceholder(0)}`, + result: `beginning ${Markup.interpolation(0)}`, expectDirectives: [HTMLBindingDirective], }, // mixed interpolation @@ -136,33 +136,33 @@ describe(`The html tag template helper`, () => { type: "mixed, back-to-back string, number, expression, and directive", location: "at the beginning", template: html`${stringValue}${numberValue}${x => x.value}${new TestDirective()} end`, - result: `${stringValue}${DOM.createInterpolationPlaceholder( + result: `${stringValue}${Markup.interpolation( 0 - )}${DOM.createInterpolationPlaceholder( + )}${Markup.interpolation( 1 - )}${DOM.createBlockPlaceholder(2)} end`, + )}${Markup.comment(2)} end`, expectDirectives: [HTMLBindingDirective, HTMLBindingDirective, TestDirective], }, { type: "mixed, back-to-back string, number, expression, and directive", location: "in the middle", template: html`beginning ${stringValue}${numberValue}${x => x.value}${new TestDirective()} end`, - result: `beginning ${stringValue}${DOM.createInterpolationPlaceholder( + result: `beginning ${stringValue}${Markup.interpolation( 0 - )}${DOM.createInterpolationPlaceholder( + )}${Markup.interpolation( 1 - )}${DOM.createBlockPlaceholder(2)} end`, + )}${Markup.comment(2)} end`, expectDirectives: [HTMLBindingDirective, HTMLBindingDirective, TestDirective], }, { type: "mixed, back-to-back string, number, expression, and directive", location: "at the end", template: html`beginning ${stringValue}${numberValue}${x => x.value}${new TestDirective()}`, - result: `beginning ${stringValue}${DOM.createInterpolationPlaceholder( + result: `beginning ${stringValue}${Markup.interpolation( 0 - )}${DOM.createInterpolationPlaceholder( + )}${Markup.interpolation( 1 - )}${DOM.createBlockPlaceholder(2)}`, + )}${Markup.comment(2)}`, expectDirectives: [HTMLBindingDirective, HTMLBindingDirective, TestDirective], }, { @@ -170,11 +170,11 @@ describe(`The html tag template helper`, () => { location: "at the beginning", template: html`${stringValue}separator${numberValue}separator${x => x.value}separator${new TestDirective()} end`, - result: `${stringValue}separator${DOM.createInterpolationPlaceholder( + result: `${stringValue}separator${Markup.interpolation( 0 - )}separator${DOM.createInterpolationPlaceholder( + )}separator${Markup.interpolation( 1 - )}separator${DOM.createBlockPlaceholder(2)} end`, + )}separator${Markup.comment(2)} end`, expectDirectives: [HTMLBindingDirective, HTMLBindingDirective, TestDirective], }, { @@ -182,11 +182,11 @@ describe(`The html tag template helper`, () => { location: "in the middle", template: html`beginning ${stringValue}separator${numberValue}separator${x => x.value}separator${new TestDirective()} end`, - result: `beginning ${stringValue}separator${DOM.createInterpolationPlaceholder( + result: `beginning ${stringValue}separator${Markup.interpolation( 0 - )}separator${DOM.createInterpolationPlaceholder( + )}separator${Markup.interpolation( 1 - )}separator${DOM.createBlockPlaceholder(2)} end`, + )}separator${Markup.comment(2)} end`, expectDirectives: [HTMLBindingDirective, HTMLBindingDirective, TestDirective], }, { @@ -194,11 +194,11 @@ describe(`The html tag template helper`, () => { location: "at the end", template: html`beginning ${stringValue}separator${numberValue}separator${x => x.value}separator${new TestDirective()}`, - result: `beginning ${stringValue}separator${DOM.createInterpolationPlaceholder( + result: `beginning ${stringValue}separator${Markup.interpolation( 0 - )}separator${DOM.createInterpolationPlaceholder( + )}separator${Markup.interpolation( 1 - )}separator${DOM.createBlockPlaceholder(2)}`, + )}separator${Markup.comment(2)}`, expectDirectives: [HTMLBindingDirective, HTMLBindingDirective, TestDirective], }, ]; @@ -218,7 +218,7 @@ describe(`The html tag template helper`, () => { it(`captures a case-sensitive property name when used with an expression`, () => { const template = html` x.value}>`; - const placeholder = DOM.createInterpolationPlaceholder(0); + const placeholder = Markup.interpolation(0); expect(template.html).to.equal( `` @@ -230,7 +230,7 @@ describe(`The html tag template helper`, () => { it(`captures a case-sensitive property name when used with a binding`, () => { const template = html` x.value)}>`; - const placeholder = DOM.createInterpolationPlaceholder(0); + const placeholder = Markup.interpolation(0); expect(template.html).to.equal( `` @@ -255,7 +255,7 @@ describe(`The html tag template helper`, () => { } const template = html``; - const placeholder = DOM.createInterpolationPlaceholder(0); + const placeholder = Markup.interpolation(0); expect(template.html).to.equal( `` From 5f2fe7d2ae1bfa45e22a8a7098d0a643b6935727 Mon Sep 17 00:00:00 2001 From: Jane Chu <7559015+janechu@users.noreply.github.com> Date: Thu, 24 Feb 2022 07:44:12 -0800 Subject: [PATCH 071/135] Fixed an updated export from FAST element 2.0 (#5653) * Fixed an updated export from FAST element 2.0 * Change files --- ...t-fast-router-d8d52093-fa07-417d-a12d-acdab4a96101.json | 7 +++++++ packages/web-components/fast-router/src/contributors.ts | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 change/@microsoft-fast-router-d8d52093-fa07-417d-a12d-acdab4a96101.json diff --git a/change/@microsoft-fast-router-d8d52093-fa07-417d-a12d-acdab4a96101.json b/change/@microsoft-fast-router-d8d52093-fa07-417d-a12d-acdab4a96101.json new file mode 100644 index 00000000000..d6e89d4ac48 --- /dev/null +++ b/change/@microsoft-fast-router-d8d52093-fa07-417d-a12d-acdab4a96101.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Fixed an updated export from FAST element 2.0", + "packageName": "@microsoft/fast-router", + "email": "7559015+janechu@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/packages/web-components/fast-router/src/contributors.ts b/packages/web-components/fast-router/src/contributors.ts index 5ba154b227c..eb9afd35e0d 100644 --- a/packages/web-components/fast-router/src/contributors.ts +++ b/packages/web-components/fast-router/src/contributors.ts @@ -1,7 +1,7 @@ import { Behavior, HTMLDirective, - DOM, + Markup, ViewBehaviorTargets, } from "@microsoft/fast-element"; import { @@ -50,7 +50,7 @@ class NavigationContributorDirective extends HTMLDirective { } createPlaceholder(index: number) { - return DOM.createCustomAttributePlaceholder(index); + return Markup.attribute(index); } createBehavior(targets: ViewBehaviorTargets) { From 5be9fd0679ff4684eaccda6aac509849c115e1e0 Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Thu, 24 Feb 2022 13:39:08 -0500 Subject: [PATCH 072/135] fix: prevent duplicative array observation patch (FE2) (#5656) * fix: prevent duplicative array observation patch * Change files * refactor: re-organize array observer code Co-authored-by: EisenbergEffect --- ...-715828c0-7339-4dde-a445-8a5407f0a3b9.json | 7 + .../src/observation/array-observer.ts | 272 +++++++++--------- 2 files changed, 150 insertions(+), 129 deletions(-) create mode 100644 change/@microsoft-fast-element-715828c0-7339-4dde-a445-8a5407f0a3b9.json diff --git a/change/@microsoft-fast-element-715828c0-7339-4dde-a445-8a5407f0a3b9.json b/change/@microsoft-fast-element-715828c0-7339-4dde-a445-8a5407f0a3b9.json new file mode 100644 index 00000000000..eb4fa3e0eed --- /dev/null +++ b/change/@microsoft-fast-element-715828c0-7339-4dde-a445-8a5407f0a3b9.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "fix: prevent duplicative array observation patch", + "packageName": "@microsoft/fast-element", + "email": "roeisenb@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/web-components/fast-element/src/observation/array-observer.ts b/packages/web-components/fast-element/src/observation/array-observer.ts index 8572c89ee67..172b5a8138c 100644 --- a/packages/web-components/fast-element/src/observation/array-observer.ts +++ b/packages/web-components/fast-element/src/observation/array-observer.ts @@ -5,21 +5,6 @@ import { Subscriber, SubscriberSet } from "./notifier.js"; import type { Notifier } from "./notifier.js"; import { Observable } from "./observable.js"; -function 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; -} - class ArrayObserver extends SubscriberSet { private oldCollection: any[] | undefined = void 0; private splices: Splice[] | undefined = void 0; @@ -74,115 +59,7 @@ class ArrayObserver extends SubscriberSet { } } -const proto = Array.prototype; -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 arrayOverrides = { - 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; - }, -}; +let enabled = false; /* eslint-disable prefer-rest-params */ /* eslint-disable @typescript-eslint/explicit-function-return-type */ @@ -195,11 +72,11 @@ const arrayOverrides = { * @public */ export function enableArrayObservation(): void { - if ((proto as any).$fastObservation) { + if (enabled) { return; } - (proto as any).$fastObservation = true; + enabled = true; Observable.setArrayObserverFactory( (collection: any[]): Notifier => { @@ -207,7 +84,144 @@ export function enableArrayObservation(): void { } ); - Object.assign(proto, arrayOverrides); + const proto = Array.prototype; + + if (!(proto as any).$fastPatch) { + (proto as any).$fastPatch = true; + + 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; + + function 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; + }, + }); + } } -/* eslint-enable prefer-rest-params */ -/* eslint-enable @typescript-eslint/explicit-function-return-type */ From 78bbf3d052b6af3faee3b5d874da611f52edaeca Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Fri, 25 Feb 2022 15:34:57 -0500 Subject: [PATCH 073/135] fix: defend against for/in use on arrays (FE2) (#5662) * fix: defend against for/in use on arrays * Change files * test: added some basic tests for array observers Co-authored-by: EisenbergEffect --- ...-8e5ad334-ac12-466b-8440-415abd011eb0.json | 7 +++ .../src/observation/array-observer.spec.ts | 56 +++++++++++++++++++ .../src/observation/array-observer.ts | 15 +++-- .../src/observation/observable.spec.ts | 16 ++---- 4 files changed, 77 insertions(+), 17 deletions(-) create mode 100644 change/@microsoft-fast-element-8e5ad334-ac12-466b-8440-415abd011eb0.json create mode 100644 packages/web-components/fast-element/src/observation/array-observer.spec.ts diff --git a/change/@microsoft-fast-element-8e5ad334-ac12-466b-8440-415abd011eb0.json b/change/@microsoft-fast-element-8e5ad334-ac12-466b-8440-415abd011eb0.json new file mode 100644 index 00000000000..92598d2f97c --- /dev/null +++ b/change/@microsoft-fast-element-8e5ad334-ac12-466b-8440-415abd011eb0.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "fix: defend against for/in use on arrays", + "packageName": "@microsoft/fast-element", + "email": "roeisenb@microsoft.com", + "dependentChangeType": "patch" +} 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 new file mode 100644 index 00000000000..61411bea639 --- /dev/null +++ b/packages/web-components/fast-element/src/observation/array-observer.spec.ts @@ -0,0 +1,56 @@ +import { expect } from "chai"; +import { Observable } from "./observable"; +import { enableArrayObservation } from "./array-observer"; +import { SubscriberSet } from "./notifier"; + +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"]) + }); +}); diff --git a/packages/web-components/fast-element/src/observation/array-observer.ts b/packages/web-components/fast-element/src/observation/array-observer.ts index 172b5a8138c..4a2a3fb18d1 100644 --- a/packages/web-components/fast-element/src/observation/array-observer.ts +++ b/packages/web-components/fast-element/src/observation/array-observer.ts @@ -5,6 +5,13 @@ 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) { + Reflect.defineProperty(target, property, { + value, + enumerable: false, + }); +} + class ArrayObserver extends SubscriberSet { private oldCollection: any[] | undefined = void 0; private splices: Splice[] | undefined = void 0; @@ -13,7 +20,7 @@ class ArrayObserver extends SubscriberSet { constructor(subject: any[]) { super(subject); - (subject as any).$fastController = this; + setNonEnumerable(subject, "$fastController", this); } public subscribe(subscriber: Subscriber): void { @@ -79,15 +86,13 @@ export function enableArrayObservation(): void { enabled = true; Observable.setArrayObserverFactory( - (collection: any[]): Notifier => { - return new ArrayObserver(collection); - } + (collection: any[]): Notifier => new ArrayObserver(collection) ); const proto = Array.prototype; if (!(proto as any).$fastPatch) { - (proto as any).$fastPatch = true; + setNonEnumerable(proto, "$fastPatch", 1); const pop = proto.pop; const push = proto.push; diff --git a/packages/web-components/fast-element/src/observation/observable.spec.ts b/packages/web-components/fast-element/src/observation/observable.spec.ts index f3e90091ac4..c174b52ab0d 100644 --- a/packages/web-components/fast-element/src/observation/observable.spec.ts +++ b/packages/web-components/fast-element/src/observation/observable.spec.ts @@ -76,19 +76,11 @@ describe("The Observable", () => { expect(notifier).to.equal(notifier2); }); - it("can get a notifier for an array", () => { - enableArrayObservation(); - const array = []; - const notifier = Observable.getNotifier(array); - expect(notifier).to.be.instanceOf(SubscriberSet); - }); + it("gets different notifiers for different objects", () => { + const notifier = Observable.getNotifier(new Model()); + const notifier2 = Observable.getNotifier(new Model()); - it("gets the same notifier for the same array", () => { - enableArrayObservation(); - const array = []; - const notifier = Observable.getNotifier(array); - const notifier2 = Observable.getNotifier(array); - expect(notifier).to.equal(notifier2); + expect(notifier).to.not.equal(notifier2); }); it("can notify a change on an object", () => { From aa36cdac92dcc041ba773ff6d87eadd33476ee9e Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Fri, 25 Feb 2022 17:34:52 -0500 Subject: [PATCH 074/135] docs: add alpha tags to unfinished binding APIs (#5665) * docs: add alpha tags to unfinished binding APIs * Change files Co-authored-by: EisenbergEffect --- ...-d6062145-f143-437a-ae17-b8a923f1208e.json | 7 ++++ .../fast-element/docs/api-report.md | 16 ++++---- .../fast-element/src/templating/binding.ts | 37 ++++++++++++++++++- 3 files changed, 51 insertions(+), 9 deletions(-) create mode 100644 change/@microsoft-fast-element-d6062145-f143-437a-ae17-b8a923f1208e.json diff --git a/change/@microsoft-fast-element-d6062145-f143-437a-ae17-b8a923f1208e.json b/change/@microsoft-fast-element-d6062145-f143-437a-ae17-b8a923f1208e.json new file mode 100644 index 00000000000..8c74cab22c6 --- /dev/null +++ b/change/@microsoft-fast-element-d6062145-f143-437a-ae17-b8a923f1208e.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "docs: add alpha tags to unfinished binding APIs", + "packageName": "@microsoft/fast-element", + "email": "roeisenb@microsoft.com", + "dependentChangeType": "none" +} diff --git a/packages/web-components/fast-element/docs/api-report.md b/packages/web-components/fast-element/docs/api-report.md index e38a9a06d73..7bdd2112ad4 100644 --- a/packages/web-components/fast-element/docs/api-report.md +++ b/packages/web-components/fast-element/docs/api-report.md @@ -60,18 +60,18 @@ export interface Behavior { unbind(source: TSource, context: ExecutionContext): void; } -// @public (undocumented) +// @alpha (undocumented) export function bind(binding: Binding, config?: BindingConfig | DefaultBindingOptions): CaptureType; // @public export type Binding = (source: TSource, context: ExecutionContext) => TReturn; -// @public (undocumented) +// @alpha (undocumented) export type BindingBehaviorFactory = { createBehavior(targets: ViewBehaviorTargets): ViewBehavior; }; -// @public (undocumented) +// @alpha (undocumented) export interface BindingConfig { // (undocumented) mode: BindingMode; @@ -79,7 +79,7 @@ export interface BindingConfig { options: any; } -// @public (undocumented) +// @alpha (undocumented) export interface BindingMode { // (undocumented) attribute: BindingType; @@ -104,7 +104,7 @@ export interface BindingObserver ex // Warning: (ae-forgotten-export) The symbol "HTMLBindingDirective" needs to be exported by the entry point index.d.ts // -// @public (undocumented) +// @alpha (undocumented) export type BindingType = (directive: HTMLBindingDirective) => BindingBehaviorFactory; // @public @@ -196,7 +196,7 @@ export function customElement(nameOrDef: string | PartialFASTElementDefinition): // @public export type DecoratorAttributeConfiguration = Omit; -// @public (undocumented) +// @alpha (undocumented) export type DefaultBindingOptions = { capture?: boolean; }; @@ -415,10 +415,10 @@ export interface ObservationRecord { // Warning: (ae-forgotten-export) The symbol "BindingConfigResolver" needs to be exported by the entry point index.d.ts // -// @public (undocumented) +// @alpha (undocumented) export const onChange: BindingConfig & BindingConfigResolver; -// @public (undocumented) +// @alpha (undocumented) export const oneTime: BindingConfig & BindingConfigResolver; // @public diff --git a/packages/web-components/fast-element/src/templating/binding.ts b/packages/web-components/fast-element/src/templating/binding.ts index cefec33ba7e..a333a04969e 100644 --- a/packages/web-components/fast-element/src/templating/binding.ts +++ b/packages/web-components/fast-element/src/templating/binding.ts @@ -15,15 +15,30 @@ import { import type { CaptureType } from "./template.js"; import type { SyntheticView } from "./view.js"; +// TODO: Fix the code docs in this file. + +/** + * @alpha + */ export type BindingBehaviorFactory = { createBehavior(targets: ViewBehaviorTargets): ViewBehavior; }; +/** + * @alpha + */ export type BindingType = (directive: HTMLBindingDirective) => BindingBehaviorFactory; + +/** + * @alpha + */ export const notSupportedBindingType: BindingType = () => { throw new Error(); }; +/** + * @alpha + */ export interface BindingMode { attribute: BindingType; booleanAttribute: BindingType; @@ -33,7 +48,9 @@ export interface BindingMode { event: BindingType; } -/* eslint-disable-next-line @typescript-eslint/no-unused-vars */ +/** + * @alpha + */ export interface BindingConfig { mode: BindingMode; options: any; @@ -268,6 +285,9 @@ class OneTimeBinding extends TargetUpdateBinding { const signals: Record = Object.create(null); +/** + * @alpha + */ export function sendSignal(signal: string): void { const found = signals[signal]; if (found) { @@ -446,6 +466,9 @@ class OneTimeEventListener extends EventListener { } } +/** + * @alpha + */ export type DefaultBindingOptions = { capture?: boolean; }; @@ -454,11 +477,17 @@ const defaultBindingOptions: DefaultBindingOptions = { capture: false, }; +/** + * @alpha + */ export const onChange = OnChangeBinding.createBindingConfig( defaultBindingOptions, directive => new EventListener(directive) ); +/** + * @alpha + */ export const oneTime = OneTimeBinding.createBindingConfig( defaultBindingOptions, directive => new OneTimeEventListener(directive) @@ -466,6 +495,9 @@ export const oneTime = OneTimeBinding.createBindingConfig( const signalMode: BindingMode = OnSignalBinding.createBindingMode(); +/** + * @alpha + */ export const signal = (options: string | Binding): BindingConfig => { return { mode: signalMode, options }; }; @@ -537,6 +569,9 @@ export class HTMLBindingDirective extends InlinableHTMLDirective { } } +/** + * @alpha + */ export function bind( binding: Binding, config: BindingConfig | DefaultBindingOptions = onChange From ee6aadfffc4d614bb4cead36a882d5e39bd03480 Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Fri, 25 Feb 2022 17:54:36 -0500 Subject: [PATCH 075/135] refactor: move template/style resolution to lazy getter (#5657) * refactor: move template/style resolution to lazy getter Enables access in SSR scenarios to the fully resolved template and styles. * Change files Co-authored-by: EisenbergEffect --- ...-47b69b7f-3b12-4084-b343-e7ee172f5539.json | 7 ++ .../fast-element/docs/api-report.md | 4 +- .../fast-element/src/components/controller.ts | 87 ++++++++++--------- 3 files changed, 54 insertions(+), 44 deletions(-) create mode 100644 change/@microsoft-fast-element-47b69b7f-3b12-4084-b343-e7ee172f5539.json diff --git a/change/@microsoft-fast-element-47b69b7f-3b12-4084-b343-e7ee172f5539.json b/change/@microsoft-fast-element-47b69b7f-3b12-4084-b343-e7ee172f5539.json new file mode 100644 index 00000000000..bc91855aa59 --- /dev/null +++ b/change/@microsoft-fast-element-47b69b7f-3b12-4084-b343-e7ee172f5539.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "refactor: move template/style resolution to lazy getter", + "packageName": "@microsoft/fast-element", + "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 7bdd2112ad4..20d9abd0d0d 100644 --- a/packages/web-components/fast-element/docs/api-report.md +++ b/packages/web-components/fast-element/docs/api-report.md @@ -160,7 +160,7 @@ export class Controller extends PropertyChangeNotifier { // @internal constructor(element: HTMLElement, definition: FASTElementDefinition); addBehaviors(behaviors: ReadonlyArray>): void; - addStyles(styles: ElementStyles | HTMLStyleElement): void; + addStyles(styles: ElementStyles | HTMLStyleElement | null | undefined): void; readonly definition: FASTElementDefinition; readonly element: HTMLElement; emit(type: string, detail?: any, options?: Omit): void | boolean; @@ -170,7 +170,7 @@ export class Controller extends PropertyChangeNotifier { onConnectedCallback(): void; onDisconnectedCallback(): void; removeBehaviors(behaviors: ReadonlyArray>, force?: boolean): void; - removeStyles(styles: ElementStyles | HTMLStyleElement): void; + removeStyles(styles: ElementStyles | HTMLStyleElement | null | undefined): void; get styles(): ElementStyles | null; set styles(value: ElementStyles | null); get template(): ElementViewTemplate | null; diff --git a/packages/web-components/fast-element/src/components/controller.ts b/packages/web-components/fast-element/src/components/controller.ts index 5f48328ed1d..6730134b29f 100644 --- a/packages/web-components/fast-element/src/components/controller.ts +++ b/packages/web-components/fast-element/src/components/controller.ts @@ -77,11 +77,24 @@ export class Controller extends PropertyChangeNotifier { * @remarks * This value can only be accurately read after connect but can be set at any time. */ - get template(): ElementViewTemplate | null { + public get template(): ElementViewTemplate | null { + // 1. Template overrides take top precedence. + if (this._template === null) { + const definition = this.definition; + + if ((this.element as any).resolveTemplate) { + // 2. Allow for element instance overrides next. + this._template = (this.element as any).resolveTemplate(); + } else if (definition.template) { + // 3. Default to the static definition. + this._template = definition.template ?? null; + } + } + return this._template; } - set template(value: ElementViewTemplate | null) { + public set template(value: ElementViewTemplate | null) { if (this._template === value) { return; } @@ -98,11 +111,24 @@ export class Controller extends PropertyChangeNotifier { * @remarks * This value can only be accurately read after connect but can be set at any time. */ - get styles(): ElementStyles | null { + public get styles(): ElementStyles | null { + // 1. Styles overrides take top precedence. + if (this._styles === null) { + const definition = this.definition; + + if ((this.element as any).resolveStyles) { + // 2. Allow for element instance overrides next. + this._styles = (this.element as any).resolveStyles(); + } else if (definition.styles) { + // 3. Default to the static definition. + this._styles = definition.styles ?? null; + } + } + return this._styles; } - set styles(value: ElementStyles | null) { + public set styles(value: ElementStyles | null) { if (this._styles === value) { return; } @@ -113,7 +139,7 @@ export class Controller extends PropertyChangeNotifier { this._styles = value; - if (!this.needsInitialization && value !== null) { + if (!this.needsInitialization) { this.addStyles(value); } } @@ -165,7 +191,11 @@ export class Controller extends PropertyChangeNotifier { * Adds styles to this element. Providing an HTMLStyleElement will attach the element instance to the shadowRoot. * @param styles - The styles to add. */ - public addStyles(styles: ElementStyles | HTMLStyleElement): void { + public addStyles(styles: ElementStyles | HTMLStyleElement | null | undefined): void { + if (!styles) { + return; + } + const target = getShadowRoot(this.element) || ((this.element.getRootNode() as any) as StyleTarget); @@ -186,7 +216,13 @@ export class Controller extends PropertyChangeNotifier { * Removes styles from this element. Providing an HTMLStyleElement will detach the element instance from the shadowRoot. * @param styles - the styles to remove. */ - public removeStyles(styles: ElementStyles | HTMLStyleElement): void { + public removeStyles( + styles: ElementStyles | HTMLStyleElement | null | undefined + ): void { + if (!styles) { + return; + } + const target = getShadowRoot(this.element) || ((this.element.getRootNode() as any) as StyleTarget); @@ -381,41 +417,8 @@ export class Controller extends PropertyChangeNotifier { this.boundObservables = null; } - const definition = this.definition; - - // 1. Template overrides take top precedence. - if (this._template === null) { - if ((this.element as any).resolveTemplate) { - // 2. Allow for element instance overrides next. - this._template = (this.element as any).resolveTemplate(); - } else if (definition.template) { - // 3. Default to the static definition. - this._template = definition.template ?? null; - } - } - - // If we have a template after the above process, render it. - // If there's no template, then the element author has opted into - // custom rendering and they will managed the shadow root's content themselves. - if (this._template !== null) { - this.renderTemplate(this._template); - } - - // 1. Styles overrides take top precedence. - if (this._styles === null) { - if ((this.element as any).resolveStyles) { - // 2. Allow for element instance overrides next. - this._styles = (this.element as any).resolveStyles(); - } else if (definition.styles) { - // 3. Default to the static definition. - this._styles = definition.styles ?? null; - } - } - - // If we have styles after the above process, add them. - if (this._styles !== null) { - this.addStyles(this._styles); - } + this.renderTemplate(this.template); + this.addStyles(this.styles); this.needsInitialization = false; } From d733f2809992fe37bc5ccdcbaa9b4bb968d9e53f Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Tue, 1 Mar 2022 14:39:15 -0500 Subject: [PATCH 076/135] feat: handle existing shadow roots when upgrading (#5679) * feat: handle existing shadow roots when upgrading * Change files Co-authored-by: EisenbergEffect --- ...-19ee7846-01d0-4dc8-babc-29a4537526a5.json | 7 +++++ .../src/components/controller.spec.ts | 27 ++++++++++++++++++- .../fast-element/src/components/controller.ts | 17 +++++++++--- 3 files changed, 46 insertions(+), 5 deletions(-) create mode 100644 change/@microsoft-fast-element-19ee7846-01d0-4dc8-babc-29a4537526a5.json diff --git a/change/@microsoft-fast-element-19ee7846-01d0-4dc8-babc-29a4537526a5.json b/change/@microsoft-fast-element-19ee7846-01d0-4dc8-babc-29a4537526a5.json new file mode 100644 index 00000000000..adaa1c19fae --- /dev/null +++ b/change/@microsoft-fast-element-19ee7846-01d0-4dc8-babc-29a4537526a5.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "feat: handle existing shadow roots when upgrading", + "packageName": "@microsoft/fast-element", + "email": "roeisenb@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/web-components/fast-element/src/components/controller.spec.ts b/packages/web-components/fast-element/src/components/controller.spec.ts index 172d741baee..33bcd7a35da 100644 --- a/packages/web-components/fast-element/src/components/controller.spec.ts +++ b/packages/web-components/fast-element/src/components/controller.spec.ts @@ -388,6 +388,7 @@ describe("The Controller", () => { expect(element.shadowRoot?.contains(style)).to.equal(false); }); + it("should attach and detach the HTMLStyleElement supplied to .addStyles() and .removeStyles() to the shadowRoot", () => { const { controller, element } = createController({ shadowOptions: { @@ -510,5 +511,29 @@ describe("The Controller", () => { controller.removeBehaviors([behavior], true); expect(behavior.bound).to.equal(false); }); - }) + }); + + context("with pre-existing shadow dom on the host", () => { + it("re-renders the view during connect", async () => { + const name = uniqueElementName(); + const element = document.createElement(name); + const root = element.attachShadow({ mode: 'open' }); + root.innerHTML = 'Test 1'; + + document.body.append(element); + + new FASTElementDefinition( + class TestElement extends FASTElement { + static definition = { + name, + template: html`Test 2` + }; + } + ).define(); + + expect(root.innerHTML).to.equal("Test 2"); + + document.body.removeChild(element); + }); + }); }); diff --git a/packages/web-components/fast-element/src/components/controller.ts b/packages/web-components/fast-element/src/components/controller.ts index 6730134b29f..521d2457a9a 100644 --- a/packages/web-components/fast-element/src/components/controller.ts +++ b/packages/web-components/fast-element/src/components/controller.ts @@ -26,6 +26,7 @@ export class Controller extends PropertyChangeNotifier { private boundObservables: Record | null = null; private behaviors: Map, number> | null = null; private needsInitialization: boolean = true; + private hasExistingShadowRoot = false; private _template: ElementViewTemplate | null = null; private _styles: ElementStyles | null = null; private _isConnected: boolean = false; @@ -159,10 +160,16 @@ export class Controller extends PropertyChangeNotifier { const shadowOptions = definition.shadowOptions; if (shadowOptions !== void 0) { - const shadowRoot = element.attachShadow(shadowOptions); + let shadowRoot = element.shadowRoot; - if (shadowOptions.mode === "closed") { - shadowRoots.set(element, shadowRoot); + if (shadowRoot) { + this.hasExistingShadowRoot = true; + } else { + shadowRoot = element.attachShadow(shadowOptions); + + if (shadowOptions.mode === "closed") { + shadowRoots.set(element, shadowRoot); + } } } @@ -434,7 +441,9 @@ export class Controller extends PropertyChangeNotifier { // If there's already a view, we need to unbind and remove through dispose. this.view.dispose(); (this as Mutable).view = null; - } else if (!this.needsInitialization) { + } else if (!this.needsInitialization || this.hasExistingShadowRoot) { + this.hasExistingShadowRoot = false; + // If there was previous custom rendering, we need to clear out the host. for (let child = host.firstChild; child !== null; child = host.firstChild) { host.removeChild(child); From 60b7861f61e545f411c7dbf805116a1f7977d1be Mon Sep 17 00:00:00 2001 From: Nicholas Rice <3213292+nicholasrice@users.noreply.github.com> Date: Tue, 1 Mar 2022 11:54:38 -0800 Subject: [PATCH 077/135] Adds TemplateParser to SSR package (#5645) * adding template-renderer feature and spec files * adding DOM emission configuration to TemplateRenderer * adding render function to TemplateRenderer * adding method description to TemplateRenderer.render * add template parser files and entry functions * parseTemplateToOpCodes should throw when used with an HTMLTemplateElement template * change script structure to allow breakpoints to percist in files from build to build * adding parse5 HTML parser * generate AST from ViewTemplate * implement AST traverser * adding node type checks * implement parser class that acts as a node visitor for node traversal * adding flushTo method to TemplateParser * implement completion method * writing a few test fixtures for pure HTML templates * add directive type and parsing test * move template-parser files to own directory * adding tests for directive ops * fix-up after rebase * emit op-codes for custom elements * adding attribute binding ops * formatting * adding tests for custom element attribute bindings * organize imports * fix processing of interpolated bindings and add test * Change files * Update packages/web-components/fast-ssr/src/template-parser/template-parser.ts Co-authored-by: Jane Chu <7559015+janechu@users.noreply.github.com> * rename ast variable * remove dependency on Markup.marker Co-authored-by: nicholasrice Co-authored-by: Jane Chu <7559015+janechu@users.noreply.github.com> --- ...-3ee397c7-e268-4a7d-b842-0f5cf799d3db.json | 7 + .../fast-element/docs/api-report.md | 9 +- packages/web-components/fast-ssr/package.json | 5 +- .../src/template-parser/attributes.ts | 14 + .../fast-ssr/src/template-parser/op-codes.ts | 91 +++++ .../template-parser/template-parser.spec.ts | 97 ++++++ .../src/template-parser/template-parser.ts | 311 ++++++++++++++++++ .../template-renderer.spec.ts | 19 ++ .../template-renderer/template-renderer.ts | 36 ++ 9 files changed, 583 insertions(+), 6 deletions(-) create mode 100644 change/@microsoft-fast-element-3ee397c7-e268-4a7d-b842-0f5cf799d3db.json create mode 100644 packages/web-components/fast-ssr/src/template-parser/attributes.ts create mode 100644 packages/web-components/fast-ssr/src/template-parser/op-codes.ts create mode 100644 packages/web-components/fast-ssr/src/template-parser/template-parser.spec.ts create mode 100644 packages/web-components/fast-ssr/src/template-parser/template-parser.ts create mode 100644 packages/web-components/fast-ssr/src/template-renderer/template-renderer.spec.ts create mode 100644 packages/web-components/fast-ssr/src/template-renderer/template-renderer.ts diff --git a/change/@microsoft-fast-element-3ee397c7-e268-4a7d-b842-0f5cf799d3db.json b/change/@microsoft-fast-element-3ee397c7-e268-4a7d-b842-0f5cf799d3db.json new file mode 100644 index 00000000000..e9ed860aaf1 --- /dev/null +++ b/change/@microsoft-fast-element-3ee397c7-e268-4a7d-b842-0f5cf799d3db.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "automated update of api-report.md", + "packageName": "@microsoft/fast-element", + "email": "nicholasrice@users.noreply.github.com", + "dependentChangeType": "none" +} diff --git a/packages/web-components/fast-element/docs/api-report.md b/packages/web-components/fast-element/docs/api-report.md index 20d9abd0d0d..31ece83ecda 100644 --- a/packages/web-components/fast-element/docs/api-report.md +++ b/packages/web-components/fast-element/docs/api-report.md @@ -49,7 +49,7 @@ export class AttributeDefinition implements Accessor { onAttributeChangedCallback(element: HTMLElement, value: any): void; readonly Owner: Function; setValue(source: HTMLElement, newValue: any): void; - } +} // @public export type AttributeMode = "reflect" | "boolean" | "fromView"; @@ -467,14 +467,14 @@ export class RepeatBehavior implements Behavior, Subscriber { // @internal (undocumented) handleChange(source: any, args: Splice[]): void; unbind(): void; - } +} // @public export class RepeatDirective extends HTMLDirective { constructor(itemsBinding: Binding, templateBinding: Binding, options: RepeatOptions); createBehavior(targets: ViewBehaviorTargets): RepeatBehavior; createPlaceholder: (index: number) => string; - } +} // @public export interface RepeatOptions { @@ -617,7 +617,7 @@ export class ViewTemplate impl readonly directives: ReadonlyArray; readonly html: string | HTMLTemplateElement; render(source: TSource, host: Node, hostBindingTarget?: Element): HTMLView; - } +} // @public export function volatile(target: {}, name: string | Accessor, descriptor: PropertyDescriptor): PropertyDescriptor; @@ -625,7 +625,6 @@ export function volatile(target: {}, name: string | Accessor, descriptor: Proper // @public export function when(binding: Binding, templateOrTemplateBinding: SyntheticViewTemplate | Binding): CaptureType; - // (No @packageDocumentation comment for this package) ``` diff --git a/packages/web-components/fast-ssr/package.json b/packages/web-components/fast-ssr/package.json index a01f2139ae4..e650709bc65 100644 --- a/packages/web-components/fast-ssr/package.json +++ b/packages/web-components/fast-ssr/package.json @@ -15,7 +15,9 @@ "url": "https://github.com/Microsoft/fast/issues/new/choose" }, "scripts": { - "build": "tsc -b --clean src && tsc -b src", + "clean": "tsc -b --clean src", + "build": "tsc -b src", + "prepare": "yarn run clean && yarn run build", "build-server": "tsc -b server", "eslint": "eslint . --ext .ts", "eslint:fix": "eslint . --ext .ts --fix", @@ -34,6 +36,7 @@ "dependencies": { "@lit-labs/ssr": "^1.0.0-rc.2", "@microsoft/fast-element": "^1.5.0", + "parse5": "^6.0.1", "tslib": "^1.11.1" }, "devDependencies": { diff --git a/packages/web-components/fast-ssr/src/template-parser/attributes.ts b/packages/web-components/fast-ssr/src/template-parser/attributes.ts new file mode 100644 index 00000000000..8e4f322a44a --- /dev/null +++ b/packages/web-components/fast-ssr/src/template-parser/attributes.ts @@ -0,0 +1,14 @@ +/** + * Extracts the attribute type from an attribute name + */ +export const attributeTypeRegExp = /([:?@])?(.*)/; + +/** + * The types of attributes applied in a template + */ +export enum AttributeType { + content, + booleanContent, + idl, + event, +} diff --git a/packages/web-components/fast-ssr/src/template-parser/op-codes.ts b/packages/web-components/fast-ssr/src/template-parser/op-codes.ts new file mode 100644 index 00000000000..78caf4e25fc --- /dev/null +++ b/packages/web-components/fast-ssr/src/template-parser/op-codes.ts @@ -0,0 +1,91 @@ +import { InlinableHTMLDirective } from "@microsoft/fast-element"; +import { AttributeType } from "./attributes.js"; + +/** + * Allows fast identification of operation types + */ +export enum OpType { + customElementOpen, + customElementClose, + customElementAttributes, + customElementShadow, + attributeBinding, + directive, + text, +} + +/** + * Operation to emit static text + */ +export type TextOp = { + type: OpType.text; + value: string; +}; + +/** + * Operation to open a custom element + */ +export type CustomElementOpenOp = { + type: OpType.customElementOpen; + /** + * The tagname of the custom element + */ + tagName: string; + + /** + * The constructor of the custom element + */ + ctor: typeof HTMLElement; + + /** + * Attributes of the custom element, non-inclusive of any attributes + * that are the product of bindings + */ + staticAttributes: Map; +}; + +/** + * Operation to close a custom element + */ +export type CustomElementCloseOp = { + type: OpType.customElementClose; +}; + +export type CustomElementShadowOp = { + type: OpType.customElementShadow; +}; + +/** + * Operation to emit static text + */ +export type DirectiveOp = { + type: OpType.directive; + directive: InlinableHTMLDirective; +}; + +/** + * Operation to emit a bound attribute + */ +export type AttributeBindingOp = { + type: OpType.attributeBinding; + directive: InlinableHTMLDirective; + name: string; + attributeType: AttributeType; + useCustomElementInstance: boolean; +}; + +/** + * Operation to emit to custom-element attributes + */ +export type CustomElementAttributes = { + type: OpType.customElementAttributes; +}; + +export type Op = + | AttributeBindingOp + | TextOp + | CustomElementOpenOp + | CustomElementCloseOp + | DirectiveOp + | CustomElementAttributes + | CustomElementShadowOp; diff --git a/packages/web-components/fast-ssr/src/template-parser/template-parser.spec.ts b/packages/web-components/fast-ssr/src/template-parser/template-parser.spec.ts new file mode 100644 index 00000000000..1156c0f51c4 --- /dev/null +++ b/packages/web-components/fast-ssr/src/template-parser/template-parser.spec.ts @@ -0,0 +1,97 @@ + +import "@lit-labs/ssr/lib/install-global-dom-shim.js"; +import { test, expect } from "@playwright/test"; +import { parseTemplateToOpCodes} from "./template-parser.js"; +import { ViewTemplate, html, FASTElement, customElement, defaultExecutionContext } from "@microsoft/fast-element" +import { Op, OpType, CustomElementOpenOp, AttributeBindingOp, DirectiveOp } from "./op-codes.js"; +import { AttributeType } from "./attributes.js"; + +@customElement("hello-world") +class HelloWorld extends FASTElement {} + +test.describe("parseTemplateToOpCodes", () => { + test("should throw when invoked with a ViewTemplate with a HTMLTemplateElement template", () => { + expect(() => { + parseTemplateToOpCodes(new ViewTemplate(document.createElement("template"), [])); + }).toThrow(); + }); + test("should not throw when invoked with a ViewTemplate with a string template", () => { + expect(() => { + parseTemplateToOpCodes(new ViewTemplate("", [])); + }).not.toThrow(); + }); + + test("should emit a single text op for a template with no bindings or directives", () => { + expect(parseTemplateToOpCodes(html`

Hello world

`)).toEqual([{type: OpType.text, value: "

Hello world

"}]) + }); + test("should emit doctype, html, head, and body elements as part of text op", () => { + expect(parseTemplateToOpCodes(html``)).toEqual([{type: OpType.text, value: ""}]) + }) + test("should emit a directive op from a binding", () => { + const input = html`${() => "hello world"}`; + expect(parseTemplateToOpCodes(input)).toEqual([{ type: OpType.directive, directive: input.directives[0]}]) + }); + test("should emit a directive op from text and a binding ", () => { + const input = html`Hello ${() => "World"}.`; + + const codes = parseTemplateToOpCodes(input); + const code = codes[0] as DirectiveOp; + expect(codes.length).toBe(1); + expect(code.type).toBe(OpType.directive); + expect(code.directive.binding(null, defaultExecutionContext)).toBe("Hello World.") + }); + test("should sandwich directive ops between text ops when binding native element content", () => { + + const input = html`

${() => "hello world"}

`; + expect(parseTemplateToOpCodes(input)).toEqual([ + { type: OpType.text, value: "

"}, + { type: OpType.directive, directive: input.directives[0]}, + { type: OpType.text, value: "

"}, + ]) + }); + test("should emit a custom element as text if it has not been defined", () => { + const input = html``; + expect(parseTemplateToOpCodes(input)).toEqual([{ type: OpType.text, value: ""}]) + }) + + test("should emit custom element open, close, attribute, and shadow ops for a defined custom element", () => { + const input = html``; + expect(parseTemplateToOpCodes(input)).toEqual([ + {type: OpType.customElementOpen, ctor: HelloWorld, tagName: "hello-world", staticAttributes: new Map()}, + {type: OpType.text, value: ""}, + {type: OpType.customElementShadow}, + {type: OpType.customElementClose}, + {type: OpType.text, value: ""} + ]) + }); + test("should emit static attributes of a custom element custom element open, close, attribute, and shadow ops for a defined custom element", () => { + const input = html``; + const code = parseTemplateToOpCodes(input).find((op) => op.type ===OpType.customElementOpen) as CustomElementOpenOp | undefined ; + expect(code).not.toBeUndefined(); + expect(code?.staticAttributes.get("string-value")).toBe("test"); + expect(code?.staticAttributes.get("bool-value")).toBe(""); + expect(code?.staticAttributes.size).toBe(2); + }); + test("should emit attributes binding ops for a native element with attribute bindings", () => { + const input = html`

`; + const codes = parseTemplateToOpCodes(input).filter(x => x.type === OpType.attributeBinding) as AttributeBindingOp[]; + + expect(codes.length).toBe(4); + expect(codes[0].attributeType).toBe(AttributeType.content); + expect(codes[1].attributeType).toBe(AttributeType.booleanContent); + expect(codes[2].attributeType).toBe(AttributeType.idl); + expect(codes[3].attributeType).toBe(AttributeType.event); + }); + test("should emit attributes binding ops for a custom element with attribute bindings", () => { + const input = html``; + const codes = parseTemplateToOpCodes(input).filter(x => x.type === OpType.attributeBinding) as AttributeBindingOp[]; + + expect(codes.length).toBe(4); + expect(codes[0].attributeType).toBe(AttributeType.content); + expect(codes[1].attributeType).toBe(AttributeType.booleanContent); + expect(codes[2].attributeType).toBe(AttributeType.idl); + expect(codes[3].attributeType).toBe(AttributeType.event); + }); +}) diff --git a/packages/web-components/fast-ssr/src/template-parser/template-parser.ts b/packages/web-components/fast-ssr/src/template-parser/template-parser.ts new file mode 100644 index 00000000000..83db03d85ab --- /dev/null +++ b/packages/web-components/fast-ssr/src/template-parser/template-parser.ts @@ -0,0 +1,311 @@ +/** + * This code is largely a fork of lit's rendering implementation: https://github.com/lit/lit/blob/main/packages/labs/ssr/src/lib/render-lit-html.ts + * with changes as necessary to render FAST components. A big thank you to those who contributed to lit's code above. + */ +import { Parser, ViewTemplate } from "@microsoft/fast-element"; +import { + Attribute, + DefaultTreeCommentNode, + DefaultTreeElement, + DefaultTreeNode, + DefaultTreeParentNode, + DefaultTreeTextNode, + parseFragment, +} from "parse5"; +import { AttributeType, attributeTypeRegExp } from "./attributes.js"; +import { Op, OpType } from "./op-codes.js"; + +/** + * Cache the results of template parsing. + */ +const opCache: Map = new Map(); + +interface Visitor { + visit?: (node: DefaultTreeNode) => void; + leave?: (node: DefaultTreeNode) => void; +} + +declare module "parse5" { + interface DefaultTreeElement { + isDefinedCustomElement?: boolean; + } +} + +/** + * Traverses a tree of nodes depth-first, invoking callbacks from visitor for each node as it goes. + * @param node - the node to traverse + * @param visitor - callbacks to be invoked during node traversal + */ +function traverse(node: DefaultTreeNode | DefaultTreeParentNode, visitor: Visitor) { + if (visitor.visit) { + visitor.visit(node); + } + + if ("childNodes" in node) { + const { childNodes } = node; + for (const child of childNodes) { + traverse(child, visitor); + } + } + + if (visitor.leave) { + visitor.leave(node); + } +} + +/** + * Test if a node is a comment node. + * @param node - the node to test + */ +function isCommentNode(node: DefaultTreeNode): node is DefaultTreeCommentNode { + return node.nodeName === "#comment"; +} + +/** + * Test if a node is a text node. + * @param node - the node to test + */ +function isTextNode(node: DefaultTreeNode): node is DefaultTreeTextNode { + return node.nodeName === "#text"; +} + +/** + * Test if a node is an element node + * @param node - the node to test + */ +function isElementNode(node: DefaultTreeNode): node is DefaultTreeElement { + return (node as DefaultTreeElement).tagName !== undefined; +} + +/** + * Determines which type of attribute binding an attribute is + * @param attr - The attribute to inspect + */ +function getAttributeType(attr: Attribute): AttributeType { + const result = attributeTypeRegExp.exec(attr.name); + + if (result === null) { + throw new Error("Failure to determine attribute binding type"); + } + + const prefix = result[1]; + + return prefix === ":" + ? AttributeType.idl + : prefix === "?" + ? AttributeType.booleanContent + : prefix === "@" + ? AttributeType.event + : AttributeType.content; +} + +/** + * Parses a template into a set of operation instructions + * @param template - The template to parse + */ +export function parseTemplateToOpCodes(template: ViewTemplate): Op[] { + const cached: Op[] | undefined = opCache.get(template); + if (cached !== undefined) { + return cached; + } + + const { html } = template; + + if (typeof html !== "string") { + throw new Error( + "@microsoft/fast-ssr only supports rendering a ViewTemplate with a string source." + ); + } + + /** + * Typescript thinks that `html` is a string | HTMLTemplateElement inside the functions defined + * below, so store in a new var that is just a string type + */ + const templateString = html; + const nodeTree = parseFragment(html, { sourceCodeLocationInfo: true }); + + if (!("nodeName" in nodeTree)) { + // I'm not sure when exactly this is encountered but the type system seems to say it's possible. + throw new Error(`Error parsing template:\n${template}`); + } + + /** + * Tracks the offset location in the source template string where the last + * flushing / skip took place. + */ + let lastOffset: number | undefined = 0; + + /** + * Collection of op codes + */ + const opCodes: Op[] = []; + opCache.set(template, opCodes); + + const { directives } = template; + + /** + * Parses an Element node, pushing all op codes for the element into + * the collection of ops for the template + * @param node - The element node to parse + */ + function parseElementNode(node: DefaultTreeElement): void { + // Track whether the opening tag of an element should be augmented. + // All constructable custom elements will need to be augmented, + // as well as any element with attribute bindings + let augmentOpeningTag = false; + const { tagName } = node; + let ctor: typeof HTMLElement | undefined; + + // Sort attributes by whether they're related to a binding or if they have + // static value + const attributes: { + static: Map; + dynamic: Attribute[]; + } = node.attrs.reduce( + (prev, current) => { + if (Parser.parse(current.value, directives)) { + prev.dynamic.push(current); + } else { + prev.static.set(current.name, current.value); + } + + return prev; + }, + { static: new Map(), dynamic: [] as Attribute[] } + ); + + // Special processing for any custom element + if (node.tagName.includes("-")) { + ctor = customElements.get(tagName); + + if (ctor !== undefined) { + augmentOpeningTag = true; + node.isDefinedCustomElement = true; + opCodes.push({ + type: OpType.customElementOpen, + tagName, + ctor, + staticAttributes: attributes.static, + }); + } + } + + // Push attribute binding op codes for any attributes that + // are dynamic + if (attributes.dynamic.length) { + for (const attr of attributes.dynamic) { + const location = node.sourceCodeLocation!.attrs[attr.name]; + flushTo(location.startOffset); + const attributeType = getAttributeType(attr); + const parsed = Parser.parse(attr.value, directives); + + if (parsed !== null) { + augmentOpeningTag = true; + opCodes.push({ + type: OpType.attributeBinding, + name: attr.name, + directive: Parser.aggregate(parsed), + attributeType, + useCustomElementInstance: Boolean(node.isDefinedCustomElement), + }); + skipTo(location.endOffset); + } + } + } + + if (augmentOpeningTag) { + if (ctor) { + flushTo(node.sourceCodeLocation!.startTag.endOffset - 1); + opCodes.push({ type: OpType.customElementAttributes }); + flush(">"); + skipTo(node.sourceCodeLocation!.startTag.endOffset); + } else { + flushTo(node.sourceCodeLocation!.startTag.endOffset); + } + } + + if (ctor !== undefined) { + opCodes.push({ type: OpType.customElementShadow }); + } + } + + /** + * Flushes a string value to op codes + * @param value - The value to flush + */ + function flush(value: string): void { + const last = opCodes[opCodes.length - 1]; + if (last?.type === OpType.text) { + last.value += value; + } else { + opCodes.push({ type: OpType.text, value }); + } + } + + /** + * Flush template content from lastIndex to provided offset + * @param offset - the offset to flush to + */ + function flushTo(offset?: number) { + if (lastOffset === undefined) { + throw new Error( + `Cannot flush template content from a last offset that is ${typeof lastOffset}.` + ); + } + + const prev = lastOffset; + lastOffset = offset; + const value = templateString.substring(prev, offset); + + if (value !== "") { + flush(value); + } + } + + function skipTo(offset: number) { + if (lastOffset === undefined) { + throw new Error("Could not skip from an undefined offset"); + } + if (offset < lastOffset) { + throw new Error(`offset must be greater than lastOffset. + offset: ${offset} + lastOffset: ${lastOffset} + `); + } + + lastOffset = offset; + } + + traverse(nodeTree, { + visit(node: DefaultTreeNode): void { + if (isCommentNode(node) || isTextNode(node)) { + const value = + (node as DefaultTreeCommentNode).data || + (node as DefaultTreeTextNode).value; + const parsed = Parser.parse(value, directives); + + if (parsed) { + flushTo(node.sourceCodeLocation!.startOffset); + opCodes.push({ + type: OpType.directive, + directive: Parser.aggregate(parsed), + }); + skipTo(node.sourceCodeLocation!.endOffset); + } + } else if (isElementNode(node)) { + parseElementNode(node); + } + }, + + leave(node: DefaultTreeNode): void { + if (isElementNode(node) && node.isDefinedCustomElement) { + opCodes.push({ type: OpType.customElementClose }); + } + }, + }); + + // Flush the remaining string content before returning op codes. + flushTo(); + + return opCodes; +} diff --git a/packages/web-components/fast-ssr/src/template-renderer/template-renderer.spec.ts b/packages/web-components/fast-ssr/src/template-renderer/template-renderer.spec.ts new file mode 100644 index 00000000000..2d2ccc77056 --- /dev/null +++ b/packages/web-components/fast-ssr/src/template-renderer/template-renderer.spec.ts @@ -0,0 +1,19 @@ +import { test, expect } from "@playwright/test"; +import { TemplateRenderer } from "./template-renderer.js"; +import { template } from "@babel/core"; + +test.describe("TemplateRenderer", () => { + test.describe("should have an initial configuration", () => { + test("that emits to shadow DOM", () => { + const instance = new TemplateRenderer(); + expect(instance.componentDOMEmissionMode).toBe("shadow") + }); + }); + + test.describe("should allow configuration", () => { + test("that emits to light DOM", () => { + const instance = new TemplateRenderer({componentDOMEmissionMode: "light"}); + expect(instance.componentDOMEmissionMode).toBe("light") + }); + }); +}); diff --git a/packages/web-components/fast-ssr/src/template-renderer/template-renderer.ts b/packages/web-components/fast-ssr/src/template-renderer/template-renderer.ts new file mode 100644 index 00000000000..cc866d94f7c --- /dev/null +++ b/packages/web-components/fast-ssr/src/template-renderer/template-renderer.ts @@ -0,0 +1,36 @@ +import { ViewTemplate } from "@microsoft/fast-element"; +import { RenderInfo } from "@lit-labs/ssr"; + +export type ComponentDOMEmissionMode = "shadow" | "light"; +export interface TemplateRendererConfiguration { + /** + * Controls whether the template renderer should emit component template code to the component's shadow DOM or to its light DOM. + */ + componentDOMEmissionMode: ComponentDOMEmissionMode; +} + +export class TemplateRenderer implements Readonly { + /** + * {@inheritDoc TemplateRendererConfiguration.componentDOMEmissionMode} + */ + public readonly componentDOMEmissionMode: ComponentDOMEmissionMode = "shadow"; + constructor(config?: TemplateRendererConfiguration) { + if (config) { + Object.assign(this, config); + } + } + + /** + * + * @param template - The template to render. + * @param renderInfo - Information about the rendering context. + * @param source - Any source data to render the template and evaluate bindings with. + */ + public *render( + template: ViewTemplate, + renderInfo: RenderInfo, + source?: unknown + ): IterableIterator { + yield ""; + } +} From b29a74818167cd8e2223c625b31a95f7a8a4b6be Mon Sep 17 00:00:00 2001 From: Jane Chu <7559015+janechu@users.noreply.github.com> Date: Tue, 1 Mar 2022 13:38:16 -0800 Subject: [PATCH 078/135] Added specification for a FAST CLI and project initialization package (#5669) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Pull Request ## 📖 Description This specification is based on the need for FAST to have a CLI for accelerating workflows and provide an easy way to initialize a project. ### 🎫 Issues Related to https://github.com/microsoft/fast/issues/5578 ## 👩‍💻 Reviewer Notes I believe some work has been done on this already but the priorities have shifted around, this is my interpretation for what the requirements are and what generally the goals should be. Please give this a thorough read and determine if this captures all requirements, an MVP roadmap is included at the bottom. This is targeting the feature branch including FAST element 2.0 and the server side rendering work as I believe these are related. ## ✅ Checklist ### General - [ ] I have included a change request file using `$ yarn change` - [ ] I have added tests for my changes. - [ ] I have tested my changes. - [x] I have updated the project documentation to reflect my changes. - [x] I have read the [CONTRIBUTING](https://github.com/Microsoft/fast/blob/master/CONTRIBUTING.md) documentation and followed the [standards](https://www.fast.design/docs/community/code-of-conduct/#our-standards) for this project. --- specs/fast-cli.md | 164 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 specs/fast-cli.md diff --git a/specs/fast-cli.md b/specs/fast-cli.md new file mode 100644 index 00000000000..5ff4184cce2 --- /dev/null +++ b/specs/fast-cli.md @@ -0,0 +1,164 @@ +- [Abstract](#abstract) +- [Use Cases](#use-cases) +- [Requirements](#requirements) + - [Configuration](#configuration) + - [Project Type](#project-type) + - [Application](#application) + - [Component Library](#component-library) + - [Non-Specific Project Type Options](#non-specific-project-type-options) + - [Command Line](#command-line) + - [Initialization](#initialization) + - [Adding A Component](#adding-a-component) + - [Adding A Foundation Based Component](#adding-a-foundation-based-component) + - [Structure](#structure) + - [Create FAST Project Package](#create-fast-project-package) + - [FAST CLI Package](#fast-cli-package) + - [Dependencies](#dependencies) +- [Implementation](#implementation) +- [Documentation](#documentation) + - [System Requirements](#system-requirements) + - [Use Of The CLI](#use-of-the-cli) + - [How To Create Templates](#how-to-create-templates) +- [Maintenance](#maintenance) + - [Testing](#testing) +- [Roadmap](#roadmap) + - [MVP](#mvp) + +## Abstract + +This specification describes a CLI used for the creation and running of FAST based applications and component libraries called `@microsoft/fast-cli`, as well as a package leveraging the CLI and allowing `npm` to initialize it called `@microsoft/create-fast-project`. The CLI is intended to accelerate user workflows when using FAST, and the initialization package is intended to assist in a quick setup for projects such as applications and component libraries. + +## Use Cases + +**Creating a prototype**: Bob is a hobbyist illustrator and wants to create a FAST web component based application quickly to prototype a website for his work. He knows how to write HTML, CSS, and some JavaScript, but does not want to get bogged down with testing or type checking. + +**Creating a production application**: April is a developer who has been tasked with creating an application that must run efficiently and is maintainable. She wants a quick setup of an application for a production environment which should include type checking, testing and server side rendering. + +**Creating a component library**: Wilhelm is a UI developer, he wants to create a library of FAST web components for his company to use in their application. His companys current application has mismatched design concepts, so he has also been developing a design system and wants the components he creates to conform to it. + +## Requirements + +### Configuration + +The configuration of the project will occur during the setup using `npm init @microsoft/fast-project ./my-project` see [`npm init` documentation](https://docs.npmjs.com/cli/v6/commands/npm-init). This requires the user has npm >=6.0.0. Certain CLI commands can be used depending on the configuration, such as adding boilerplate components with `fast add --component`. The following configuration options will be determined during initialization. + +#### Project Type + +There will be two project types, `application` and `component-library`. When this is specified, this will allow the CLI to make assumptions about the project. The projects structure will change based on what type of project this is, additionally `webpack` will be added and configured if the project is an application. + +##### Application + +**Server Side Rendering** + +This option will enable SSR (Server Side Rendering). + +**End to End Testing** + +A user may want some end to end tests. These tests will be scaffolded in Playwright. The will conform to a `*.spec.pw.ts` or `*.spec.pw.js` file name to avoid conflict with unit tests. + +An `npm` script will be added `test:e2e`. +The `npm` script `test` will include this script. + +##### Component Library + +**Design Tokens** + +Design tokens can be added, which will initialize an example design token and integrate it with the first example component. + +#### Non-Specific Project Type Options + +**Type Checking** + +The user will have the option of plain JavaScript of TypeScript. This will facilitate production and prototyping experiences. It will also be more friendly to users who may not be familiar with TypeScript. + +Should you use TypeScript, a `npm` script will be added `build:tsc` which will build your project using the TypeScript compiler. If you choose to use JavaScript, further options with `babeljs` will be used to compile the project. + +**Manual Component Testing** + +Components are often manually tested during the process of component creation which can involve a lot of back and forth to check the component in the browser. For this reason there will be an option to add storybook. + +An `npm` script will be added `start:storybook`. + +**Unit Testing** + +This option will set up Mocha and Chai and when new components are added via the CLI they will create a `*.spec.ts` or `*.spec.js` file. + +An `npm` script will be added `test:unit-test`. +The `npm` script `test` will include this script. + +**Linting** + +Another more production level feature is linting. We can offer the user configuration and setup for `eslint`. + +An `npm` script will be added `eslint`. +The `npm` script `test` will include this script. + +### Command Line + +#### Initialization + +The first step in configuration is initializing the project, which will create the configuration file for FAST. This will be done via the `npm init @microsoft/fast-project ./my-project`. The `` in this instance is `fast-project` which transforms if one were to use `npx` into `create-fast-project`, so the `npx` command becomes `npx @microsoft/create-fast-project`. `npm` will assume that `create-` is to be prepended to the unscoped name of the package. + +#### Adding A Component + +Adding a component should be performed using the CLI with `fast add --component` which will create scaffolding of the template, styles, class, and any testing or other files based on the FAST configuration file. + +##### Adding A Foundation Based Component + +Adding a component based on a `@microsoft/fast-foundation` component should be performed using the CLI with `fast add --component --foundation=button`. + +### Structure + +There should be two packages which enable this project, `@microsoft/create-fast-project` and `@microsoft/fast-cli`. One is for setting up the project via `npm init` and templates, the other is a CLI for additional commands. + +#### Create FAST Project Package + +The `@microsoft/create-fast-project` package will interact with `npm init`. It will assume that `npm init` is running it and setup all the options via the `@microsof/fast-cli` as specified in the requirements. + +#### FAST CLI Package + +The `@microsoft/fast-cli` package should be as lightweight as possible, it should also rely on the configuration file for details about how the project has been setup. In this way, if a user were to retro-fit an existing repository to use the CLI as part of their process this would be possible. An example of this is already having a production application, but needing configuration details to use SSR (Server Side Rendering). + +### Dependencies + +Dependencies are necessary for the following requirements: +- Command line arguments +- Colorization of terminal text + +## Documentation + +Documentation will be required for users and for maintainers. Since this model is plug and play with templates, the documentation must also cover this scenario. + +### System Requirements + +There should be a list of system requirements for using `@microsoft/fast-cli` and `@microsoft/create-fast-project`. These should primarily be focused on `npm` and NodeJS versions. + +### Use Of The CLI + +The commands available should be documentated as well as a step-by-step getting started page for all the project types. There should be a dictionary of commands available in a side bar for easy access, though the commands and their documentation may exist on a single page. They may need to be tagged with which project type they can be executed on, additionally documentation may exist in the code, providing users with warnings when they attempt to use commands for a different project type than the one they are configured for. + +### How To Create Templates + +A few templates will be shipped as defaults with the package, however in future there may be other template creators who will want to specify their own templates instead of the defaults. + +**TBD**: Add user stories for template creation. + +## Maintenance + +As with all projects, this one must undergo maintenance when changes occur in any of the dependencies. There should be a document categorizing what dependencies may need to be updated in future as well as a build script which issue warnings when dependencies go out of date for major versions. + +### Testing + +This project should have unit tests for: +- Initialization +- All configuration options +- Running the projects test script after intializing a project to ensure FAST does not break the initialized project + +## Roadmap + +### MVP + +- Ability to generate a new application based on a default template +- One template to start +- TypeScript/JavaScript option +- SSR option From 3622ca02148344a18f9ba4f2126bab2b981250f6 Mon Sep 17 00:00:00 2001 From: William Wagner <44823142+williamw2@users.noreply.github.com> Date: Mon, 7 Mar 2022 10:08:34 -0800 Subject: [PATCH 079/135] Add fast command buffer (#5708) --- .../web-components/fast-ssr/server/server.ts | 89 ++--- .../fast-command-buffer/index.fixture.html | 306 ++++++++++++++++++ .../fast-ssr/src/fast-command-buffer/index.ts | 149 +++++++++ 3 files changed, 502 insertions(+), 42 deletions(-) create mode 100644 packages/web-components/fast-ssr/src/fast-command-buffer/index.fixture.html create mode 100644 packages/web-components/fast-ssr/src/fast-command-buffer/index.ts diff --git a/packages/web-components/fast-ssr/server/server.ts b/packages/web-components/fast-ssr/server/server.ts index 0507d622326..b83fdc77a57 100644 --- a/packages/web-components/fast-ssr/server/server.ts +++ b/packages/web-components/fast-ssr/server/server.ts @@ -23,50 +23,55 @@ function handleRequest(req: Request, res: Response) { }); } -function handleStyleRequest(req: Request, res: Response) { - res.set("Content-Type", "text/html"); - fs.readFile( - path.resolve(__dirname, "./src/fast-style/index.fixture.html"), - { encoding: "utf8" }, - (err, data) => { - const stream = (Readable as any).from(data); - stream.on("readable", function (this: any) { - while ((data = this.read())) { - res.write(data); - } - }); - stream.on("close", () => res.end()); - stream.on("error", (e: Error) => { - console.error(e); - process.exit(1); - }); - } - ); -} - -function handleStyleScriptRequest(req: Request, res: Response) { - res.set("Content-Type", "application/javascript"); - fs.readFile( - path.resolve(__dirname, "./dist/esm/fast-style/index.js"), - { encoding: "utf8" }, - (err, data) => { - const stream = (Readable as any).from(data); - stream.on("readable", function (this: any) { - while ((data = this.read())) { - res.write(data); - } - }); - stream.on("close", () => res.end()); - stream.on("error", (e: Error) => { - console.error(e); - process.exit(1); - }); - } - ); +function handlePathRequest( + mapPath: string, + contentType: string, + req: Request, + res: Response +) { + res.set("Content-Type", contentType); + fs.readFile(path.resolve(__dirname, mapPath), { encoding: "utf8" }, (err, data) => { + const stream = (Readable as any).from(data); + stream.on("readable", function (this: any) { + while ((data = this.read())) { + res.write(data); + } + }); + stream.on("close", () => res.end()); + stream.on("error", (e: Error) => { + console.error(e); + process.exit(1); + }); + }); } const app = express(); app.get("/", handleRequest); -app.get("/fast-style", handleStyleRequest); -app.get("/fast-style.js", handleStyleScriptRequest); +app.get("/fast-style", (req: Request, res: Response) => + handlePathRequest("./src/fast-style/index.fixture.html", "text/html", req, res) +); +app.get("/fast-style.js", (req: Request, res: Response) => + handlePathRequest( + "./dist/esm/fast-style/index.js", + "application/javascript", + req, + res + ) +); +app.get("/fast-command-buffer", (req: Request, res: Response) => + handlePathRequest( + "./src/fast-command-buffer/index.fixture.html", + "text/html", + req, + res + ) +); +app.get("/fast-command-buffer.js", (req: Request, res: Response) => + handlePathRequest( + "./dist/esm/fast-command-buffer/index.js", + "application/javascript", + req, + res + ) +); app.listen(PORT); diff --git a/packages/web-components/fast-ssr/src/fast-command-buffer/index.fixture.html b/packages/web-components/fast-ssr/src/fast-command-buffer/index.fixture.html new file mode 100644 index 00000000000..641b897e4db --- /dev/null +++ b/packages/web-components/fast-ssr/src/fast-command-buffer/index.fixture.html @@ -0,0 +1,306 @@ + + + + + + + + + + + +
+ + + + + + + +
+
+ + + + + diff --git a/packages/web-components/fast-ssr/src/fast-command-buffer/index.ts b/packages/web-components/fast-ssr/src/fast-command-buffer/index.ts new file mode 100644 index 00000000000..a44beeb69f1 --- /dev/null +++ b/packages/web-components/fast-ssr/src/fast-command-buffer/index.ts @@ -0,0 +1,149 @@ +const FASTCommandBufferAttributeName: string = "data-fast-buffer-events"; + +interface CommandCache { + originalNode: HTMLElement; + event: Event; + attachedNode: HTMLElement | null; +} + +/** + * Component for recording user events on SSR rendered FAST components which have not yet hydrated and replaying those + * events when they have. + * During SSR an instance of the component should be placed as a direct child within every template + * that contains children with behaviors bound to user generated events (click, mouseover, focus, etc). When those child element + * tags are rendered FASTCommandBufferAttributeName attribute must be included that lists the events which the element needs to + * have recorded as a comma delimited list (i.e.