Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(fast-element): enable children and slotted filters #3323

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/web-components/fast-element/docs/api-report.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ export function children<T = any>(propertyOrOptions: (keyof T & string) | Childr
//
// @public
export class ChildrenBehavior extends NodeObservationBehavior<ChildrenBehaviorOptions> {
constructor(target: HTMLSlotElement, options: ChildrenBehaviorOptions);
constructor(target: HTMLElement, options: ChildrenBehaviorOptions);
disconnect(): void;
protected getNodes(): ChildNode[];
observe(): void;
Expand Down
3 changes: 1 addition & 2 deletions packages/web-components/fast-element/docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@

## Short-term

* **Feature**: Allow `repeat` to accept an expression that returns the template they generate from.
* **Feature**: Enable event delegation through a syntax like `@click.delegate=...`

## Medium-term

* **Experiment**: See if it's possible combine template instantiate and bind, and if that improves initial render time.
* **Feature**: Improve `when` to enable if/else scenarios.
* **Feature**: Dependency injection infrastructure, including simple decorator-based property injection for `FASTElement`.
* **Refactor:** Create abstraction for `ElementInternals`.
* **Test:** Include perf benchmarks in the automated build process and track changes over time.
Expand Down
139 changes: 139 additions & 0 deletions packages/web-components/fast-element/src/directives/children.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { expect } from "chai";
import { children, ChildrenBehavior } from "./children";
import { AttachedBehaviorDirective } from "./directive";
import { observable } from "../observation/observable";
import { elements } from "./node-observation";
import { DOM } from "../dom";

describe("The children", () => {
context("template function", () => {
it("returns an AttachedBehaviorDirective", () => {
const directive = children("test");
expect(directive).to.be.instanceOf(AttachedBehaviorDirective);
});
});

context("directive", () => {
it("creates a ChildrenBehavior", () => {
const directive = children("test") as AttachedBehaviorDirective;
const target = document.createElement("div");
const behavior = directive.createBehavior(target);

expect(behavior).to.be.instanceOf(ChildrenBehavior);
});
});

context("behavior", () => {
class Model {
@observable nodes;
}

function createAndAppendChildren(host: HTMLElement, elementName = "div") {
const children = new Array(10);

for (let i = 0, ii = children.length; i < ii; ++i) {
const child = document.createElement(i % 1 === 0 ? elementName : "div");
children[i] = child;
host.appendChild(child);
}

return children;
}

function createDOM(elementName: string = "div") {
const host = document.createElement("div");
const children = createAndAppendChildren(host, elementName);

return { host, children };
}

it("gathers child nodes", () => {
const { host, children } = createDOM();
const behavior = new ChildrenBehavior(host, {
property: "nodes",
childList: true,
});
const model = new Model();

behavior.bind(model);

expect(model.nodes).members(children);
});

it("gathers child nodes with a filter", () => {
const { host, children } = createDOM("foo-bar");
const behavior = new ChildrenBehavior(host, {
property: "nodes",
childList: true,
filter: elements("foo-bar"),
});
const model = new Model();

behavior.bind(model);

expect(model.nodes).members(children.filter(elements("foo-bar")));
});

it("updates child nodes when they change", async () => {
const { host, children } = createDOM("foo-bar");
const behavior = new ChildrenBehavior(host, {
property: "nodes",
childList: true,
});
const model = new Model();

behavior.bind(model);

expect(model.nodes).members(children);

const updatedChildren = children.concat(createAndAppendChildren(host));

await DOM.nextUpdate();

expect(model.nodes).members(updatedChildren);
});

it("updates child nodes when they change with a filter", async () => {
const { host, children } = createDOM("foo-bar");
const behavior = new ChildrenBehavior(host, {
property: "nodes",
childList: true,
filter: elements("foo-bar"),
});
const model = new Model();

behavior.bind(model);

expect(model.nodes).members(children);

const updatedChildren = children.concat(createAndAppendChildren(host));

await DOM.nextUpdate();

expect(model.nodes).members(updatedChildren.filter(elements("foo-bar")));
});

it("clears and unwatches when unbound", async () => {
const { host, children } = createDOM("foo-bar");
const behavior = new ChildrenBehavior(host, {
property: "nodes",
childList: true,
});
const model = new Model();

behavior.bind(model);

expect(model.nodes).members(children);

behavior.unbind();

expect(model.nodes).members([]);

host.appendChild(document.createElement("div"));

await DOM.nextUpdate();

expect(model.nodes).members([]);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export class ChildrenBehavior extends NodeObservationBehavior<ChildrenBehaviorOp
* @param target - The element target to observe children on.
* @param options - The options to use when observing the element children.
*/
public constructor(target: HTMLSlotElement, options: ChildrenBehaviorOptions) {
public constructor(target: HTMLElement, options: ChildrenBehaviorOptions) {
super(target, options);
}

Expand Down Expand Up @@ -66,6 +66,8 @@ export function children<T = any>(
property: propertyOrOptions,
childList: true,
};
} else {
(propertyOrOptions as MutationObserverInit).childList = true;
}

return new AttachedBehaviorDirective(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,33 @@ export interface NodeBehaviorBehaviorOptions<T = any> {
* The property to assign the observed nodes to.
*/
property: T;

/**
* Filters nodes that are synced with the property.
* Called one time for each element in the array.
* @param value - The Node that is being inspected.
* @param index - The index of the node within the array.
* @param array - The Node array that is being filtered.
*/
filter?(value: Node, index: number, array: Node[]): boolean;
}

/**
* Filters an array of nodes to only elements.
* @param tagName - An optional tag name to restrict the filter to.
*/
export function elements(tagName?: string) {
if (tagName) {
tagName = tagName.toUpperCase();

return function (value: Node, index: number, array: Node[]): boolean {
return value.nodeType === 1 && (value as HTMLElement).tagName === tagName;
};
}

return function (value: Node, index: number, array: Node[]): boolean {
return value.nodeType === 1;
};
}

/**
Expand All @@ -27,7 +54,7 @@ export abstract class NodeObservationBehavior<T extends NodeBehaviorBehaviorOpti
* @param target - The target to assign the nodes property on.
* @param options - The options to use in configuring node observation.
*/
constructor(protected target: HTMLSlotElement, protected options: T) {}
constructor(protected target: HTMLElement, protected options: T) {}

/**
* Begins observation of the nodes.
Expand Down Expand Up @@ -55,7 +82,7 @@ export abstract class NodeObservationBehavior<T extends NodeBehaviorBehaviorOpti
(x: Accessor) => x.name === name
);
this.source = source;
this.updateTarget(this.getNodes());
this.updateTarget(this.computeNodes());

if (this.shouldUpdate) {
this.observe();
Expand All @@ -77,7 +104,17 @@ export abstract class NodeObservationBehavior<T extends NodeBehaviorBehaviorOpti

/** @internal */
public handleEvent(): void {
this.updateTarget(this.getNodes());
this.updateTarget(this.computeNodes());
}

private computeNodes() {
let nodes = this.getNodes();

if (this.options.filter !== void 0) {
nodes = nodes.filter(this.options.filter!);
}

return nodes;
}

private updateTarget(value: ReadonlyArray<any>): void {
Expand Down
132 changes: 132 additions & 0 deletions packages/web-components/fast-element/src/directives/slotted.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { expect } from "chai";
import { slotted, SlottedBehavior } from "./slotted";
import { AttachedBehaviorDirective } from "./directive";
import { observable } from "../observation/observable";
import { elements } from "./node-observation";
import { DOM } from "../dom";

describe("The slotted", () => {
context("template function", () => {
it("returns an AttachedBehaviorDirective", () => {
const directive = slotted("test");
expect(directive).to.be.instanceOf(AttachedBehaviorDirective);
});
});

context("directive", () => {
it("creates a SlottedBehavior", () => {
const directive = slotted("test") as AttachedBehaviorDirective;
const target = document.createElement("slot");
const behavior = directive.createBehavior(target);

expect(behavior).to.be.instanceOf(SlottedBehavior);
});
});

context("behavior", () => {
class Model {
@observable nodes;
}

function createAndAppendChildren(host: HTMLElement, elementName = "div") {
const children = new Array(10);

for (let i = 0, ii = children.length; i < ii; ++i) {
const child = document.createElement(i % 1 === 0 ? elementName : "div");
children[i] = child;
host.appendChild(child);
}

return children;
}

function createDOM(elementName: string = "div") {
const host = document.createElement("div");
const slot = document.createElement("slot");
const shadowRoot = host.attachShadow({ mode: "open" });
const children = createAndAppendChildren(host, elementName);

shadowRoot.appendChild(slot);

return { host, slot, children };
}

it("gathers nodes from a slot", () => {
const { host, slot, children } = createDOM();
const behavior = new SlottedBehavior(slot, { property: "nodes" });
const model = new Model();

behavior.bind(model);

expect(model.nodes).members(children);
});

it("gathers nodes from a slot with a filter", () => {
const { host, slot, children } = createDOM("foo-bar");
const behavior = new SlottedBehavior(slot, {
property: "nodes",
filter: elements("foo-bar"),
});
const model = new Model();

behavior.bind(model);

expect(model.nodes).members(children.filter(elements("foo-bar")));
});

it("updates when slotted nodes change", async () => {
const { host, slot, children } = createDOM("foo-bar");
const behavior = new SlottedBehavior(slot, { property: "nodes" });
const model = new Model();

behavior.bind(model);

expect(model.nodes).members(children);

const updatedChildren = children.concat(createAndAppendChildren(host));

await DOM.nextUpdate();

expect(model.nodes).members(updatedChildren);
});

it("updates when slotted nodes change with a filter", async () => {
const { host, slot, children } = createDOM("foo-bar");
const behavior = new SlottedBehavior(slot, {
property: "nodes",
filter: elements("foo-bar"),
});
const model = new Model();

behavior.bind(model);

expect(model.nodes).members(children);

const updatedChildren = children.concat(createAndAppendChildren(host));

await DOM.nextUpdate();

expect(model.nodes).members(updatedChildren.filter(elements("foo-bar")));
});

it("clears and unwatches when unbound", async () => {
const { host, slot, children } = createDOM("foo-bar");
const behavior = new SlottedBehavior(slot, { property: "nodes" });
const model = new Model();

behavior.bind(model);

expect(model.nodes).members(children);

behavior.unbind();

expect(model.nodes).members([]);

host.appendChild(document.createElement("div"));

await DOM.nextUpdate();

expect(model.nodes).members([]);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export class SlottedBehavior extends NodeObservationBehavior<SlottedBehaviorOpti
* Retrieves the nodes that should be assigned to the target.
*/
protected getNodes(): Node[] {
return this.target.assignedNodes(this.options);
return (this.target as HTMLSlotElement).assignedNodes(this.options);
}
}

Expand Down