Skip to content

Commit

Permalink
feat(fast-element): introduce NamedTargetDirective for extensibility (m…
Browse files Browse the repository at this point in the history
…icrosoft#4079)

* feat(fast-element): introduce NamedTargetDirective for extensibility

* fix(fast-element): typo in binding directive for volatility detect

* fix: prettier messing up tests

Co-authored-by: EisenbergEffect <roeisenb@microsoft.com>
  • Loading branch information
EisenbergEffect and EisenbergEffect authored Oct 27, 2020
1 parent cacfefe commit c93bc26
Show file tree
Hide file tree
Showing 5 changed files with 89 additions and 24 deletions.
9 changes: 7 additions & 2 deletions packages/web-components/fast-element/docs/api-report.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,12 +108,11 @@ export class BindingBehavior implements Behavior {
}

// @public
export class BindingDirective extends Directive {
export class BindingDirective extends NamedTargetDirective {
constructor(binding: Binding);
// (undocumented)
binding: Binding;
createBehavior(target: Node): BindingBehavior;
createPlaceholder: (index: number) => string;
targetAtContent(): void;
get targetName(): string | undefined;
set targetName(value: string | undefined);
Expand Down Expand Up @@ -351,6 +350,12 @@ export type Mutable<T> = {
-readonly [P in keyof T]: T[P];
};

// @public
export abstract class NamedTargetDirective extends Directive {
createPlaceholder: (index: number) => string;
abstract targetName: string | undefined;
}

// @public
export interface NodeBehaviorOptions<T = any> {
filter?(value: Node, index: number, array: Node[]): boolean;
Expand Down
13 changes: 3 additions & 10 deletions packages/web-components/fast-element/src/directives/binding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
import { Observable } from "../observation/observable";
import { DOM } from "../dom";
import { SyntheticView } from "../view";
import { Directive } from "./directive";
import { NamedTargetDirective } from "./directive";
import { Behavior } from "./behavior";

function normalBind(
Expand Down Expand Up @@ -187,28 +187,21 @@ function updateClassTarget(this: BindingBehavior, value: string): void {
* A directive that configures data binding to element content and attributes.
* @public
*/
export class BindingDirective extends Directive {
export class BindingDirective extends NamedTargetDirective {
private cleanedTargetName?: string;
private originalTargetName?: string;
private bind: typeof normalBind = normalBind;
private unbind: typeof normalUnbind = normalUnbind;
private updateTarget: typeof updateAttributeTarget = updateAttributeTarget;
private isBindingVolatile: boolean;

/**
* 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;

/**
* Creates an instance of BindingDirective.
* @param binding - A binding that returns the data used to update the DOM.
*/
public constructor(public binding: Binding) {
super();
this.isBindingVolatile = Observable.isVolatileBinding(this.bind);
this.isBindingVolatile = Observable.isVolatileBinding(this.binding);
}

/**
Expand Down
19 changes: 19 additions & 0 deletions packages/web-components/fast-element/src/directives/directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,25 @@ export abstract class Directive implements BehaviorFactory {
public abstract createBehavior(target: Node): Behavior;
}

/**
* A {@link Directive} that targets a named attribute or property on a node or object.
* @public
*/
export abstract class NamedTargetDirective extends Directive {
/**
* Gets/sets the name of the attribute or property that this
* directive is targeting on the associated node or object.
*/
public abstract targetName: string | undefined;

/**
* 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;
}

/**
* Describes the shape of a behavior constructor that can be created by
* an {@link AttachedBehaviorDirective}.
Expand Down
66 changes: 56 additions & 10 deletions packages/web-components/fast-element/src/template.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { expect } from "chai";
import { html, ViewTemplate } from "./template";
import { DOM } from "./dom";
import { BindingDirective } from "./directives/binding";
import { Directive } from "./directives/directive";
import { Directive, NamedTargetDirective } from "./directives/directive";

describe(`The html tag template helper`, () => {
it(`transforms a string into a ViewTemplate.`, () => {
Expand Down Expand Up @@ -135,42 +135,57 @@ describe(`The html tag template helper`, () => {
type: "mixed, back-to-back string, number, expression, and directive",
location: "at the beginning",
template: html<Model>`${stringValue}${numberValue}${x => x.value}${new TestDirective()} end`,
result: `${stringValue}${numberValue}${DOM.createInterpolationPlaceholder(0)}${DOM.createBlockPlaceholder(1)} end`,
result: `${stringValue}${numberValue}${DOM.createInterpolationPlaceholder(
0
)}${DOM.createBlockPlaceholder(1)} end`,
expectDirectives: [BindingDirective, TestDirective],
},
{
type: "mixed, back-to-back string, number, expression, and directive",
location: "in the middle",
template: html<Model>`beginning ${stringValue}${numberValue}${x => x.value}${new TestDirective()} end`,
result: `beginning ${stringValue}${numberValue}${DOM.createInterpolationPlaceholder(0)}${DOM.createBlockPlaceholder(1)} end`,
result: `beginning ${stringValue}${numberValue}${DOM.createInterpolationPlaceholder(
0
)}${DOM.createBlockPlaceholder(1)} end`,
expectDirectives: [BindingDirective, TestDirective],
},
{
type: "mixed, back-to-back string, number, expression, and directive",
location: "at the end",
template: html<Model>`beginning ${stringValue}${numberValue}${x => x.value}${new TestDirective()}`,
result: `beginning ${stringValue}${numberValue}${DOM.createInterpolationPlaceholder(0)}${DOM.createBlockPlaceholder(1)}`,
result: `beginning ${stringValue}${numberValue}${DOM.createInterpolationPlaceholder(
0
)}${DOM.createBlockPlaceholder(1)}`,
expectDirectives: [BindingDirective, TestDirective],
},
{
type: "mixed, separated string, number, expression, and directive",
location: "at the beginning",
template: html<Model>`${stringValue}separator${numberValue}separator${x => x.value}separator${new TestDirective()} end`,
result: `${stringValue}separator${numberValue}separator${DOM.createInterpolationPlaceholder(0)}separator${DOM.createBlockPlaceholder(1)} end`,
template: html<Model>`${stringValue}separator${numberValue}separator${x =>
x.value}separator${new TestDirective()} end`,
result: `${stringValue}separator${numberValue}separator${DOM.createInterpolationPlaceholder(
0
)}separator${DOM.createBlockPlaceholder(1)} end`,
expectDirectives: [BindingDirective, TestDirective],
},
{
type: "mixed, separated string, number, expression, and directive",
location: "in the middle",
template: html<Model>`beginning ${stringValue}separator${numberValue}separator${x => x.value}separator${new TestDirective()} end`,
result: `beginning ${stringValue}separator${numberValue}separator${DOM.createInterpolationPlaceholder(0)}separator${DOM.createBlockPlaceholder(1)} end`,
template: html<Model>`beginning ${stringValue}separator${numberValue}separator${x =>
x.value}separator${new TestDirective()} end`,
result: `beginning ${stringValue}separator${numberValue}separator${DOM.createInterpolationPlaceholder(
0
)}separator${DOM.createBlockPlaceholder(1)} end`,
expectDirectives: [BindingDirective, TestDirective],
},
{
type: "mixed, separated string, number, expression, and directive",
location: "at the end",
template: html<Model>`beginning ${stringValue}separator${numberValue}separator${x => x.value}separator${new TestDirective()}`,
result: `beginning ${stringValue}separator${numberValue}separator${DOM.createInterpolationPlaceholder(0)}separator${DOM.createBlockPlaceholder(1)}`,
template: html<Model>`beginning ${stringValue}separator${numberValue}separator${x =>
x.value}separator${new TestDirective()}`,
result: `beginning ${stringValue}separator${numberValue}separator${DOM.createInterpolationPlaceholder(
0
)}separator${DOM.createBlockPlaceholder(1)}`,
expectDirectives: [BindingDirective, TestDirective],
},
];
Expand Down Expand Up @@ -199,4 +214,35 @@ describe(`The html tag template helper`, () => {
":someAttribute"
);
});

it(`captures a case-sensitive property name when used with a binding`, () => {
const template = html<Model>`<my-element :someAttribute=${new BindingDirective(x => x.value)}></my-element>`;
const placeholder = DOM.createInterpolationPlaceholder(0);

expect(template.html).to.equal(
`<my-element :someAttribute=${placeholder}></my-element>`
);
expect((template.directives[0] as NamedTargetDirective).targetName).to.equal(
":someAttribute"
);
});

it(`captures a case-sensitive property name when used with a named target directive`, () => {
class TestDirective extends NamedTargetDirective {
targetName: string | undefined;
createBehavior(target: Node) {
return { bind() {}, unbind() {} };
}
}

const template = html<Model>`<my-element :someAttribute=${new TestDirective()}></my-element>`;
const placeholder = DOM.createInterpolationPlaceholder(0);

expect(template.html).to.equal(
`<my-element :someAttribute=${placeholder}></my-element>`
);
expect((template.directives[0] as NamedTargetDirective).targetName).to.equal(
":someAttribute"
);
});
});
6 changes: 4 additions & 2 deletions packages/web-components/fast-element/src/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { compileTemplate } from "./template-compiler";
import { ElementView, HTMLView, SyntheticView } from "./view";
import { DOM } from "./dom";
import { Behavior, BehaviorFactory } from "./directives/behavior";
import { Directive } from "./directives/directive";
import { Directive, NamedTargetDirective } from "./directives/directive";
import { BindingDirective } from "./directives/binding";
import { defaultExecutionContext, Binding } from "./observation/observable";

Expand Down Expand Up @@ -226,10 +226,12 @@ export function html<TSource = any, TParent = any>(

if (typeof value === "function") {
value = new BindingDirective(value as Binding);
}

if (value instanceof NamedTargetDirective) {
const match = lastAttributeNameRegex.exec(currentString);
if (match !== null) {
(value as BindingDirective).targetName = match[2];
value.targetName = match[2];
}
}

Expand Down

0 comments on commit c93bc26

Please sign in to comment.