Skip to content

Commit

Permalink
feat: new execution context design (#5800)
Browse files Browse the repository at this point in the history
* refactor: new design for execution context

* feat: add two new event helpers to the execution context and tests

* fix: wip update types to match new context apis

* fix: update foundation and components template types

* Change files

* fix: update template type in fast-website

* fix: update site components for new template types

* fix: add missing api updates

Co-authored-by: EisenbergEffect <[email protected]>
  • Loading branch information
2 people authored and nicholasrice committed May 9, 2022
1 parent d57a864 commit bfa540d
Show file tree
Hide file tree
Showing 43 changed files with 989 additions and 416 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "major",
"comment": "fix: update foundation and components template types",
"packageName": "@microsoft/fast-components",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "major",
"comment": "refactor: new design for execution context",
"packageName": "@microsoft/fast-element",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "major",
"comment": "fix: update foundation and components template types",
"packageName": "@microsoft/fast-foundation",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "major",
"comment": "refactor: new design for execution context",
"packageName": "@microsoft/fast-router",
"email": "[email protected]",
"dependentChangeType": "patch"
}
197 changes: 138 additions & 59 deletions packages/web-components/fast-element/docs/api-report.md

Large diffs are not rendered by default.

44 changes: 26 additions & 18 deletions packages/web-components/fast-element/src/components/controller.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Message, Mutable, StyleTarget } from "../interfaces.js";
import type { Behavior } from "../observation/behavior.js";
import { PropertyChangeNotifier } from "../observation/notifier.js";
import { defaultExecutionContext, Observable } from "../observation/observable.js";
import { ExecutionContext, Observable } from "../observation/observable.js";
import { FAST } from "../platform.js";
import type { ElementStyles } from "../styles/element-styles.js";
import type { ElementViewTemplate } from "../templating/template.js";
Expand All @@ -25,12 +25,14 @@ const isConnectedPropertyName = "isConnected";
* Controls the lifecycle and rendering of a `FASTElement`.
* @public
*/
export class Controller extends PropertyChangeNotifier {
export class Controller<
TElement extends HTMLElement = HTMLElement
> extends PropertyChangeNotifier {
private boundObservables: Record<string, any> | null = null;
private behaviors: Map<Behavior<HTMLElement>, number> | null = null;
private behaviors: Map<Behavior<TElement>, number> | null = null;
private needsInitialization: boolean = true;
private hasExistingShadowRoot = false;
private _template: ElementViewTemplate | null = null;
private _template: ElementViewTemplate<TElement> | null = null;
private _styles: ElementStyles | null = null;
private _isConnected: boolean = false;

Expand All @@ -47,7 +49,7 @@ export class Controller extends PropertyChangeNotifier {
/**
* The element being controlled by this controller.
*/
public readonly element: HTMLElement;
public readonly element: TElement;

/**
* The element definition that instructs this controller
Expand All @@ -60,7 +62,7 @@ export class Controller extends PropertyChangeNotifier {
* @remarks
* If `null` then the element is managing its own rendering.
*/
public readonly view: ElementView | null = null;
public readonly view: ElementView<TElement> | null = null;

/**
* Indicates whether or not the custom element has been
Expand All @@ -81,7 +83,7 @@ export class Controller extends PropertyChangeNotifier {
* @remarks
* This value can only be accurately read after connect but can be set at any time.
*/
public get template(): ElementViewTemplate | null {
public get template(): ElementViewTemplate<TElement> | null {
// 1. Template overrides take top precedence.
if (this._template === null) {
const definition = this.definition;
Expand All @@ -98,7 +100,7 @@ export class Controller extends PropertyChangeNotifier {
return this._template;
}

public set template(value: ElementViewTemplate | null) {
public set template(value: ElementViewTemplate<TElement> | null) {
if (this._template === value) {
return;
}
Expand Down Expand Up @@ -155,8 +157,9 @@ export class Controller extends PropertyChangeNotifier {
* controller in how to handle rendering and other platform integrations.
* @internal
*/
public constructor(element: HTMLElement, definition: FASTElementDefinition) {
public constructor(element: TElement, definition: FASTElementDefinition) {
super(element);

this.element = element;
this.definition = definition;

Expand Down Expand Up @@ -254,10 +257,10 @@ export class Controller extends PropertyChangeNotifier {
* Adds behaviors to this element.
* @param behaviors - The behaviors to add.
*/
public addBehaviors(behaviors: ReadonlyArray<Behavior<HTMLElement>>): void {
public addBehaviors(behaviors: ReadonlyArray<Behavior<TElement>>): void {
const targetBehaviors = this.behaviors ?? (this.behaviors = new Map());
const length = behaviors.length;
const behaviorsToBind: Behavior<HTMLElement>[] = [];
const behaviorsToBind: Behavior<TElement>[] = [];

for (let i = 0; i < length; ++i) {
const behavior = behaviors[i];
Expand All @@ -272,9 +275,10 @@ export class Controller extends PropertyChangeNotifier {

if (this._isConnected) {
const element = this.element;
const context = ExecutionContext.default;

for (let i = 0; i < behaviorsToBind.length; ++i) {
behaviorsToBind[i].bind(element, defaultExecutionContext);
behaviorsToBind[i].bind(element, context);
}
}
}
Expand All @@ -285,7 +289,7 @@ export class Controller extends PropertyChangeNotifier {
* @param force - Forces unbinding of behaviors.
*/
public removeBehaviors(
behaviors: ReadonlyArray<Behavior<HTMLElement>>,
behaviors: ReadonlyArray<Behavior<TElement>>,
force: boolean = false
): void {
const targetBehaviors = this.behaviors;
Expand All @@ -295,7 +299,7 @@ export class Controller extends PropertyChangeNotifier {
}

const length = behaviors.length;
const behaviorsToUnbind: Behavior<HTMLElement>[] = [];
const behaviorsToUnbind: Behavior<TElement>[] = [];

for (let i = 0; i < length; ++i) {
const behavior = behaviors[i];
Expand All @@ -311,9 +315,10 @@ export class Controller extends PropertyChangeNotifier {

if (this._isConnected) {
const element = this.element;
const context = ExecutionContext.default;

for (let i = 0; i < behaviorsToUnbind.length; ++i) {
behaviorsToUnbind[i].unbind(element, defaultExecutionContext);
behaviorsToUnbind[i].unbind(element, context);
}
}
}
Expand All @@ -327,18 +332,19 @@ export class Controller extends PropertyChangeNotifier {
}

const element = this.element;
const context = ExecutionContext.default;

if (this.needsInitialization) {
this.finishInitialization();
} else if (this.view !== null) {
this.view.bind(element, defaultExecutionContext);
this.view.bind(element, context);
}

const behaviors = this.behaviors;

if (behaviors !== null) {
for (const behavior of behaviors.keys()) {
behavior.bind(element, defaultExecutionContext);
behavior.bind(element, context);
}
}

Expand All @@ -365,8 +371,10 @@ export class Controller extends PropertyChangeNotifier {

if (behaviors !== null) {
const element = this.element;
const context = ExecutionContext.default;

for (const behavior of behaviors.keys()) {
behavior.unbind(element, defaultExecutionContext);
behavior.unbind(element, context);
}
}
}
Expand Down
10 changes: 10 additions & 0 deletions packages/web-components/fast-element/src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@ export type Mutable<T> = {
-readonly [P in keyof T]: T[P];
};

/**
* Extracts the item type from an array.
* @public
*/
export type ArrayItem<T> = T extends ReadonlyArray<infer TItem>
? TItem
: T extends Array<infer TItem>
? TItem
: any;

/**
* A policy for use with the standard trustedTypes platform API.
* @public
Expand Down
12 changes: 8 additions & 4 deletions packages/web-components/fast-element/src/observation/behavior.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
import type { ExecutionContext } from "./observable.js";
import type { ExecutionContext, RootContext } from "./observable.js";

/**
* Represents an object that can contribute behavior to a view or
* element's bind/unbind operations.
* @public
*/
export interface Behavior<TSource = any, TParent = any, TGrandparent = any> {
export interface Behavior<
TSource = any,
TParent = any,
TContext extends ExecutionContext<TParent> = RootContext
> {
/**
* 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: TSource, context: ExecutionContext<TParent, TGrandparent>): void;
bind(source: TSource, context: TContext): void;

/**
* Unbinds this behavior from the source.
* @param source - The source to unbind from.
*/
unbind(source: TSource, context: ExecutionContext<TParent, TGrandparent>): void;
unbind(source: TSource, context: TContext): void;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { expect } from "chai";
import { ExecutionContext, ItemContext } from "./observable";

describe("The ExecutionContext", () => {
it("has a default", () => {
const defaultContext = ExecutionContext.default;
const newContext = ExecutionContext.create();

expect(defaultContext.constructor).equals(newContext.constructor);
});

function createEvent() {
const detail = { hello: "world" };
const event = new CustomEvent('my-event', { detail });

return { event, detail };
}

it("can get the current event", () => {
const { event } = createEvent();

ExecutionContext.setEvent(event);
const context = ExecutionContext.create();

expect(context.event).equals(event);

ExecutionContext.setEvent(null);
});

it("can get the current event detail", () => {
const { event, detail } = createEvent();

ExecutionContext.setEvent(event);
const context = ExecutionContext.create();

expect(context.eventDetail()).equals(detail);
expect(context.eventDetail<typeof detail>().hello).equals(detail.hello);

ExecutionContext.setEvent(null);
});

it("can create a child context for a parent source", () => {
const parentSource = {};
const parentContext = ExecutionContext.create();
const childContext = parentContext.createChildContext(parentSource);

expect(childContext.parent).equals(parentSource);
expect(childContext.parentContext).equals(parentContext);
});

it("can create an item context from a child context", () => {
const parentSource = {};
const parentContext = ExecutionContext.create();
const childContext = parentContext.createChildContext(parentSource);
const itemContext = childContext.createItemContext(7, 42);

expect(itemContext.parent).equals(parentSource);
expect(itemContext.parentContext).equals(parentContext);
expect(itemContext.index).equals(7);
expect(itemContext.length).equals(42);
});

context("item context", () => {
const scenarios = [
{
name: "even is first",
index: 0,
length: 42,
isEven: true,
isOdd: false,
isFirst: true,
isMiddle: false,
isLast: false
},
{
name: "odd in middle",
index: 7,
length: 42,
isEven: false,
isOdd: true,
isFirst: false,
isMiddle: true,
isLast: false
},
{
name: "even in middle",
index: 8,
length: 42,
isEven: true,
isOdd: false,
isFirst: false,
isMiddle: true,
isLast: false
},
{
name: "odd at end",
index: 41,
length: 42,
isEven: false,
isOdd: true,
isFirst: false,
isMiddle: false,
isLast: true
},
{
name: "even at end",
index: 40,
length: 41,
isEven: true,
isOdd: false,
isFirst: false,
isMiddle: false,
isLast: true
}
];

function assert(itemContext: ItemContext, scenario: typeof scenarios[0]) {
expect(itemContext.index).equals(scenario.index);
expect(itemContext.length).equals(scenario.length);
expect(itemContext.isEven).equals(scenario.isEven);
expect(itemContext.isOdd).equals(scenario.isOdd);
expect(itemContext.isFirst).equals(scenario.isFirst);
expect(itemContext.isInMiddle).equals(scenario.isMiddle);
expect(itemContext.isLast).equals(scenario.isLast);
}

for (const scenario of scenarios) {
it(`has correct position when ${scenario.name}`, () => {
const parentSource = {};
const parentContext = ExecutionContext.create();
const childContext = parentContext.createChildContext(parentSource);
const itemContext = childContext.createItemContext(scenario.index, scenario.length);

assert(itemContext, scenario);
});
}

it ("can update its index and length", () => {
const scenario1 = scenarios[0];
const scenario2 = scenarios[1];

const parentSource = {};
const parentContext = ExecutionContext.create();
const childContext = parentContext.createChildContext(parentSource);
const itemContext = childContext.createItemContext(scenario1.index, scenario1.length);

assert(itemContext, scenario1);

itemContext.updatePosition(scenario2.index, scenario2.length);

assert(itemContext, scenario2);
});
});
});
Loading

0 comments on commit bfa540d

Please sign in to comment.