Skip to content

Commit

Permalink
feat(fast-element): add functioning execution context (microsoft#2935)
Browse files Browse the repository at this point in the history
* feat(fast-element): implement real execution context

* fix(fast-element): property track source and context

* feat(fast-element): fast repeat item index beginnings

* feat(fast-element): add observation of context properties and fix lint

* fix(fast-element): capture contact for trigger bindings

* doc(fast-element): add repeat context docs

* feat(fast-element): some type param improvements

* fix(fast-element): remove decorator usage from core library
  • Loading branch information
EisenbergEffect authored Apr 15, 2020
1 parent 78eb90e commit 707d4e7
Showing 11 changed files with 241 additions and 71 deletions.
26 changes: 25 additions & 1 deletion packages/web-components/fast-element/docs/building-components.md
Original file line number Diff line number Diff line change
@@ -314,7 +314,7 @@ DOM.setHTMLPolicy(myPolicy);
#### Events

Besides rendering content, attributes, and properties, you'll often want to add event listeners and execute code when events fire. To do that, prepend the event name with `@` and provide the expression to be called when that event fires. Within an event expression, you also have access to a special *context* argument from which you can access the event args.
Besides rendering content, attributes, and properties, you'll often want to add event listeners and execute code when events fire. To do that, prepend the event name with `@` and provide the expression to be called when that event fires. Within an event expression, you also have access to a special *context* argument from which you can access the `event` object.

**Example: Basic Events**

@@ -460,6 +460,30 @@ export class FriendList extends FastElement {
}
```

Similar to event handlers, within a `repeat` block you have access to a special context object. Here is a list of the properties that are available on the context:

* `event` - The event object when inside an event handler.
* `parent` - The parent scope when inside a `repeat` block.
* `index` - The index of the current item when inside a `repeat` block (opt in).
* `length` - The length of the array when inside a `repeat` block (opt in).
* `even` - True if the index of the current item is even when inside a `repeat` block (opt in).
* `odd` - True if the index of the current item is odd when inside a `repeat` block (opt in).
* `first` - True if the current item is first in the array inside a `repeat` block (opt in).
* `middle` - True if the current item is somewhere in the middle of the the array inside a `repeat` block (opt in).
* `last` - True if the current item is last in the array inside a `repeat` block (opt in).

Some context properties are opt-in because they are more costly to update. So, for performance reasons, they are not available by default. To opt into the positioning properties, pass options to the repeat directive, with the setting `positioning: true`. For example, here's how we would use the `index` in our friends template from above:

**Example: List Rendering with Item Index**

```html
<ul>
${repeat(x => x.friends, html<string>`
<li>${(x, c) => c.index} ${x => x}</li>
`, { positioning: true })}
</ul>
```

#### Composing Templates

The `HTMLTemplate` returned from the `html` tag helper is also a directive itself. As a result, you can create templates and compose them into other templates.
8 changes: 4 additions & 4 deletions packages/web-components/fast-element/src/controller.ts
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@ import { FastElement, FastElementDefinition } from "./fast-element";
import { Container, InterfaceSymbol, Registry, Resolver } from "./di";
import { ElementView } from "./view";
import { PropertyChangeNotifier } from "./observation/notifier";
import { Observable } from "./observation/observable";
import { defaultExecutionContext, Observable } from "./observation/observable";
import { Behavior } from "./directives/behavior";
import { ElementStyles, StyleTarget } from "./styles";

@@ -109,7 +109,7 @@ export class Controller extends PropertyChangeNotifier implements Container {
const element = this.element;

for (let i = 0; i < length; ++i) {
behaviors[i].bind(element);
behaviors[i].bind(element, defaultExecutionContext);
}
}
}
@@ -163,14 +163,14 @@ export class Controller extends PropertyChangeNotifier implements Container {
const view = this.view;

if (view !== null) {
view.bind(element);
view.bind(element, defaultExecutionContext);
}

const behaviors = this.behaviors;

if (behaviors !== null) {
for (let i = 0, ii = behaviors.length; i < ii; ++i) {
behaviors[i].bind(element);
behaviors[i].bind(element, defaultExecutionContext);
}
}

Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { ExecutionContext } from "../observation/observable";

export interface Behavior {
bind(source: unknown): void;
bind(source: unknown, context: ExecutionContext): void;
unbind(source: unknown): void;
}

32 changes: 23 additions & 9 deletions packages/web-components/fast-element/src/directives/binding.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { Expression } from "../interfaces";
import { ExecutionContext, Expression, setCurrentEvent } from "../observation/observable";
import { ObservableExpression } from "../observation/observable";
import { DOM } from "../dom";
import { Directive } from "./directive";
import { Behavior } from "./behavior";

const context = {} as any;

function normalBind(this: BindingBehavior, source: unknown): void {
function normalBind(
this: BindingBehavior,
source: unknown,
context: ExecutionContext
): void {
this.source = source;
this.context = context;

if (this.observableExpression === null) {
this.observableExpression = new ObservableExpression(this.expression, this);
@@ -16,19 +19,26 @@ function normalBind(this: BindingBehavior, source: unknown): void {
this.updateTarget(this.observableExpression.evaluate(source, context));
}

function triggerBind(this: BindingBehavior, source: unknown): void {
function triggerBind(
this: BindingBehavior,
source: unknown,
context: ExecutionContext
): void {
this.source = source;
this.context = context;
this.target.addEventListener(this.targetName!, this, true);
}

function normalUnbind(this: BindingBehavior): void {
this.observableExpression!.dispose();
this.source = null;
this.context = null;
}

function triggerUnbind(this: BindingBehavior): void {
this.target.removeEventListener(this.targetName!, this, true);
this.source = null;
this.context = null;
}

function updateAttributeTarget(this: BindingBehavior, value: unknown): void {
@@ -120,7 +130,8 @@ export class BindingDirective extends Directive {
}

export class BindingBehavior implements Behavior {
public source: unknown = void 0;
public source: unknown = null;
public context: ExecutionContext | null = null;
public observableExpression: ObservableExpression | null = null;

constructor(
@@ -133,12 +144,15 @@ export class BindingBehavior implements Behavior {
) {}

handleExpressionChange(): void {
this.updateTarget(this.observableExpression!.evaluate(this.source, context));
this.updateTarget(
this.observableExpression!.evaluate(this.source, this.context!)
);
}

handleEvent(event: Event): void {
const context = { event };
const result = this.expression(this.source, context as any);
setCurrentEvent(event);
const result = this.expression(this.source, this.context!);
setCurrentEvent(null);

if (result !== true) {
event.preventDefault();
102 changes: 84 additions & 18 deletions packages/web-components/fast-element/src/directives/repeat.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,77 @@
import { Expression } from "../interfaces";
import { CaptureType, SyntheticViewTemplate } from "../template";
import { CaptureType, SyntheticViewTemplate, ViewTemplate } from "../template";
import { DOM } from "../dom";
import { Observable, ObservableExpression } from "../observation/observable";
import {
ExecutionContext,
Expression,
Observable,
ObservableExpression,
} from "../observation/observable";
import { HTMLView, SyntheticView } from "../view";
import { Subscriber } from "../observation/subscriber-collection";
import { ArrayObserver, enableArrayObservation } from "../observation/array-observer";
import { Splice } from "../observation/array-change-records";
import { Behavior } from "./behavior";
import { Directive } from "./directive";

export interface RepeatOptions {
positioning: boolean;
}

const defaultRepeatOptions: RepeatOptions = Object.freeze({
positioning: false,
});

function bindWithoutPositioning(
view: SyntheticView,
items: any[],
index: number,
context: ExecutionContext
): void {
view.bind(items[index], context);
}

function bindWithPositioning(
view: SyntheticView,
items: any[],
index: number,
context: ExecutionContext
): void {
const childContext = Object.create(context);
childContext.index = index;
childContext.length = items.length;
view.bind(items[index], childContext);
}

export class RepeatBehavior implements Behavior, Subscriber {
private source: unknown = void 0;
private views: SyntheticView[] = [];
private items: any[] | null = null;
private itemsObserver?: ArrayObserver = void 0;
private observableExpression: ObservableExpression;
private originalContext: ExecutionContext | undefined = void 0;
private childContext: ExecutionContext | undefined = void 0;
private bindView: typeof bindWithoutPositioning = bindWithoutPositioning;

constructor(
private location: Node,
expression: Expression,
private template: SyntheticViewTemplate
private template: SyntheticViewTemplate,
private options: RepeatOptions
) {
this.observableExpression = new ObservableExpression(expression, this);

if (options.positioning) {
this.bindView = bindWithPositioning;
}
}

bind(source: unknown): void {
bind(source: unknown, context: ExecutionContext): void {
this.source = source;
this.items = this.observableExpression.evaluate(source, null as any);
this.originalContext = context;
this.childContext = Object.create(context);
this.childContext!.parent = source;

this.items = this.observableExpression.evaluate(source, this.originalContext);
this.observeItems();
this.refreshAllViews();
}
@@ -44,7 +89,10 @@ export class RepeatBehavior implements Behavior, Subscriber {
}

handleExpressionChange(): void {
this.items = this.observableExpression.evaluate(this.source, null as any);
this.items = this.observableExpression.evaluate(
this.source,
this.originalContext!
);
this.observeItems();
this.refreshAllViews();
}
@@ -73,8 +121,10 @@ export class RepeatBehavior implements Behavior, Subscriber {
}

private updateViews(splices: Splice[]): void {
const childContext = this.childContext!;
const views = this.views;
const totalRemoved: SyntheticView[] = [];
const bindView = this.bindView;
let removeDelta = 0;

for (let i = 0, ii = splices.length; i < ii; ++i) {
@@ -103,23 +153,33 @@ export class RepeatBehavior implements Behavior, Subscriber {
totalRemoved.length > 0 ? totalRemoved.shift()! : template.create();

views.splice(addIndex, 0, view);
view.bind(items[addIndex]);
bindView(view, items, addIndex, childContext);
view.insertBefore(location);
}
}

for (let i = 0, ii = totalRemoved.length; i < ii; ++i) {
totalRemoved[i].dispose();
}

if (this.options.positioning) {
for (let i = 0, ii = views.length; i < ii; ++i) {
const currentContext = views[i].context!;
currentContext.length = ii;
currentContext.index = i;
}
}
}

private refreshAllViews(): void {
const items = this.items!;
const childContext = this.childContext!;
let itemsLength = items.length;
let views = this.views;
const viewsLength = views.length;
const template = this.template;
const location = this.location;
const bindView = this.bindView;

if (itemsLength === 0) {
// all views need to be removed
@@ -131,7 +191,7 @@ export class RepeatBehavior implements Behavior, Subscriber {

for (let i = 0; i < itemsLength; ++i) {
const view = template.create();
view.bind(items[i]);
bindView(view, items, i, childContext);
views[i] = view;
view.insertBefore(location);
}
@@ -141,10 +201,11 @@ export class RepeatBehavior implements Behavior, Subscriber {

for (; i < itemsLength; ++i) {
if (i < viewsLength) {
views[i].bind(items[i]);
const view = views[i];
bindView(view, items, i, childContext);
} else {
const view = template.create();
view.bind(items[i]);
bindView(view, items, i, childContext);
views.push(view);
view.insertBefore(location);
}
@@ -170,19 +231,24 @@ export class RepeatBehavior implements Behavior, Subscriber {
export class RepeatDirective extends Directive {
createPlaceholder: (index: number) => string = DOM.createBlockPlaceholder;

constructor(public expression: Expression, public template: SyntheticViewTemplate) {
constructor(
public expression: Expression,
public template: SyntheticViewTemplate,
public options: RepeatOptions
) {
super();
enableArrayObservation();
}

public createBehavior(target: any): RepeatBehavior {
return new RepeatBehavior(target, this.expression, this.template);
return new RepeatBehavior(target, this.expression, this.template, this.options);
}
}

export function repeat<T = any, K = any>(
expression: Expression<T, K[]>,
template: SyntheticViewTemplate
): CaptureType<T> {
return new RepeatDirective(expression, template);
export function repeat<TScope = any, TItem = any>(
expression: Expression<TScope, TItem[]>,
template: ViewTemplate<Partial<TItem>, TScope>,
options: RepeatOptions = defaultRepeatOptions
): CaptureType<TScope> {
return new RepeatDirective(expression, template, options);
}
Loading

0 comments on commit 707d4e7

Please sign in to comment.