Skip to content

Commit

Permalink
feat: enable data binding in CSS (microsoft#6654)
Browse files Browse the repository at this point in the history
* feat: make HostController extend ExpressionController

* feat: add CSSBindingDirective

* feat: integrate css binding with css string template & refactor binding

* fix: foundation updates to binding helpers

* fix: properly update package export paths

* feat: finish css binding variable generation

* test: add tests for css lambda and binding interpolation and fix var bug

* style: apply prettier to new fast-element code

* test: add css binding tests

* Change files

* chore: fix change files

* fix: reset bindings if the styles are overwritten via setAttribute

* refactor: rename BindingSource to BindingDirective
  • Loading branch information
EisenbergEffect authored Mar 4, 2023
1 parent 0535a97 commit 70eb15b
Show file tree
Hide file tree
Showing 32 changed files with 668 additions and 245 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "prerelease",
"comment": "feat: enable data binding in CSS",
"packageName": "@microsoft/fast-element",
"email": "rob@bluespire.com",
"dependentChangeType": "prerelease"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "prerelease",
"comment": "fix: update components to new binding APIs",
"packageName": "@microsoft/fast-foundation",
"email": "rob@bluespire.com",
"dependentChangeType": "prerelease"
}
65 changes: 52 additions & 13 deletions packages/web-components/fast-element/docs/api-report.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,13 +80,10 @@ export class AttributeDefinition implements Accessor {
// @public
export type AttributeMode = typeof reflectMode | typeof booleanMode | "fromView";

// @public
export function bind<T = any>(expression: Expression<T>, policy?: DOMPolicy, isVolatile?: boolean): Binding<T>;

// @public
export abstract class Binding<TSource = any, TReturn = any, TParent = any> {
constructor(evaluate: Expression<TSource, TReturn, TParent>, policy?: DOMPolicy | undefined, isVolatile?: boolean);
abstract createObserver(directive: HTMLDirective, subscriber: Subscriber): ExpressionObserver<TSource, TReturn, TParent>;
abstract createObserver(subscriber: Subscriber, directive: BindingDirective): ExpressionObserver<TSource, TReturn, TParent>;
// (undocumented)
evaluate: Expression<TSource, TReturn, TParent>;
// (undocumented)
Expand All @@ -96,6 +93,13 @@ export abstract class Binding<TSource = any, TReturn = any, TParent = any> {
policy?: DOMPolicy | undefined;
}

// @public
export interface BindingDirective {
readonly aspectType?: DOMAspect;
readonly dataBinding: Binding;
readonly targetAspect?: string;
}

// @public
export const booleanConverter: ValueConverter;

Expand Down Expand Up @@ -178,6 +182,27 @@ export interface ContentView {
// @public
export const css: CSSTemplateTag;

// @public
export class CSSBindingDirective implements HostBehavior, Subscriber, CSSDirective, BindingDirective {
constructor(dataBinding: Binding, targetAspect: string);
addedCallback(controller: HostController<HTMLElement & {
$cssBindings: Map<CSSBindingDirective, CSSBindingEntry>;
}>): void;
connectedCallback(controller: HostController<HTMLElement & {
$cssBindings: Map<CSSBindingDirective, CSSBindingEntry>;
}>): void;
createCSS(add: AddBehavior): ComposableStyles;
// (undocumented)
readonly dataBinding: Binding;
// @internal
handleChange(_: any, observer: ExpressionObserver): void;
removedCallback(controller: HostController<HTMLElement & {
$cssBindings: Map<CSSBindingDirective, CSSBindingEntry>;
}>): void;
// (undocumented)
readonly targetAspect: string;
}

// @public
export interface CSSDirective {
createCSS(add: AddBehavior): ComposableStyles;
Expand All @@ -199,10 +224,13 @@ export interface CSSDirectiveDefinition<TType extends Constructable<CSSDirective
}

// @public
export type CSSTemplateTag = ((strings: TemplateStringsArray, ...values: (ComposableStyles | CSSDirective)[]) => ElementStyles) & {
partial(strings: TemplateStringsArray, ...values: (ComposableStyles | CSSDirective)[]): CSSDirective;
export type CSSTemplateTag = (<TSource = any, TParent = any>(strings: TemplateStringsArray, ...values: CSSValue<TSource, TParent>[]) => ElementStyles) & {
partial<TSource = any, TParent = any>(strings: TemplateStringsArray, ...values: CSSValue<TSource, TParent>[]): CSSDirective;
};

// @public
export type CSSValue<TSource, TParent = any> = Expression<TSource, any, TParent> | Binding<TSource, any, TParent> | ComposableStyles | CSSDirective;

// @public
export function customElement(nameOrDef: string | PartialFASTElementDefinition): (type: Constructable<HTMLElement>) => void;

Expand Down Expand Up @@ -252,18 +280,24 @@ export class ElementController<TElement extends HTMLElement = HTMLElement> exten
addBehavior(behavior: HostBehavior<TElement>): void;
addStyles(styles: ElementStyles | HTMLStyleElement | null | undefined): void;
connect(): void;
get context(): ExecutionContext;
readonly definition: FASTElementDefinition;
disconnect(): void;
emit(type: string, detail?: any, options?: Omit<CustomEventInit, "detail">): void | boolean;
static forCustomElement(element: HTMLElement): ElementController;
get isBound(): boolean;
get isConnected(): boolean;
get mainStyles(): ElementStyles | null;
set mainStyles(value: ElementStyles | null);
onAttributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
onUnbind(behavior: {
unbind(controller: ExpressionController<TElement>): any;
}): void;
removeBehavior(behavior: HostBehavior<TElement>, force?: boolean): void;
removeStyles(styles: ElementStyles | HTMLStyleElement | null | undefined): void;
static setStrategy(strategy: ElementControllerStrategy): void;
readonly source: TElement;
get sourceLifetime(): SourceLifetime | undefined;
get template(): ElementViewTemplate<TElement> | null;
set template(value: ElementViewTemplate<TElement> | null);
readonly view: ElementView<TElement> | null;
Expand Down Expand Up @@ -304,6 +338,10 @@ export class ElementStyles {
// @public
export interface ElementView<TSource = any, TParent = any> extends View<TSource, TParent> {
appendTo(node: Node): void;
onUnbind(behavior: {
unbind(controller: ViewController<TSource, TParent>): any;
}): void;
readonly sourceLifetime?: SourceLifetime;
}

// @public
Expand Down Expand Up @@ -425,21 +463,20 @@ export interface HostBehavior<TSource = any> {
}

// @public
export interface HostController<TSource = any> {
export interface HostController<TSource = any> extends ExpressionController<TSource> {
addBehavior(behavior: HostBehavior<TSource>): void;
addStyles(styles: ElementStyles | HTMLStyleElement | null | undefined): void;
readonly isConnected: boolean;
mainStyles: ElementStyles | null;
removeBehavior(behavior: HostBehavior<TSource>, force?: boolean): void;
removeStyles(styles: ElementStyles | HTMLStyleElement | null | undefined): void;
readonly source: TSource;
}

// @public
export const html: HTMLTemplateTag;

// @public
export class HTMLBindingDirective implements HTMLDirective, ViewBehaviorFactory, ViewBehavior, Aspected {
export class HTMLBindingDirective implements HTMLDirective, ViewBehaviorFactory, ViewBehavior, Aspected, BindingDirective {
constructor(dataBinding: Binding);
aspectType: DOMAspect;
// @internal (undocumented)
Expand Down Expand Up @@ -575,9 +612,6 @@ export abstract class NodeObservationDirective<T extends NodeBehaviorOptions> ex
protected updateTarget(source: any, value: ReadonlyArray<any>): void;
}

// @public
export function normalizeBinding<TSource = any, TReturn = any, TParent = any>(value: Expression<TSource, TReturn, TParent> | Binding<TSource, TReturn, TParent> | {}): Binding<TSource, TReturn, TParent>;

// @public
export interface Notifier {
notify(args: any): void;
Expand Down Expand Up @@ -617,6 +651,9 @@ export interface ObservationRecord {
// @public
export function oneTime<T = any>(expression: Expression<T>, policy?: DOMPolicy): Binding<T>;

// @public
export function oneWay<T = any>(expression: Expression<T>, policy?: DOMPolicy, isVolatile?: boolean): Binding<T>;

// @public
export const Parser: Readonly<{
parse(value: string, factories: Record<string, ViewBehaviorFactory>): (string | ViewBehaviorFactory)[] | null;
Expand Down Expand Up @@ -670,7 +707,7 @@ export class RepeatBehavior<TSource = any> implements ViewBehavior, Subscriber {
}

// @public
export class RepeatDirective<TSource = any> implements HTMLDirective, ViewBehaviorFactory {
export class RepeatDirective<TSource = any> implements HTMLDirective, ViewBehaviorFactory, BindingDirective {
constructor(dataBinding: Binding<TSource>, templateBinding: Binding<TSource, SyntheticViewTemplate>, options: RepeatOptions);
createBehavior(): RepeatBehavior<TSource>;
createHTML(add: AddViewBehaviorFactory): string;
Expand Down Expand Up @@ -850,6 +887,7 @@ export interface ValueConverter {
export interface View<TSource = any, TParent = any> extends Disposable {
bind(source: TSource, context?: ExecutionContext<TParent>): void;
readonly context: ExecutionContext<TParent>;
readonly isBound: boolean;
readonly source: TSource | null;
unbind(): void;
}
Expand Down Expand Up @@ -901,6 +939,7 @@ export function when<TSource = any, TReturn = any, TParent = any>(condition: Exp
// dist/dts/components/fast-element.d.ts:60:5 - (ae-forgotten-export) The symbol "define" needs to be exported by the entry point index.d.ts
// dist/dts/components/fast-element.d.ts:61:5 - (ae-forgotten-export) The symbol "compose" needs to be exported by the entry point index.d.ts
// dist/dts/components/fast-element.d.ts:62:5 - (ae-forgotten-export) The symbol "from" needs to be exported by the entry point index.d.ts
// dist/dts/styles/css-binding-directive.d.ts:35:9 - (ae-forgotten-export) The symbol "CSSBindingEntry" needs to be exported by the entry point index.d.ts

// (No @packageDocumentation comment for this package)

Expand Down
8 changes: 4 additions & 4 deletions packages/web-components/fast-element/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,12 @@
"default": "./dist/esm/debug.js"
},
"./binding/two-way": {
"types": "./dist/dts/templating/binding-two-way.d.ts",
"default": "./dist/esm/templating/binding-two-way.js"
"types": "./dist/dts/binding/two-way.d.ts",
"default": "./dist/esm/binding/two-way.js"
},
"./binding/signal": {
"types": "./dist/dts/templating/binding-signal.d.ts",
"default": "./dist/esm/templating/binding-signal.js"
"types": "./dist/dts/binding/signal.d.ts",
"default": "./dist/esm/binding/signal.js"
},
"./render": {
"types": "./dist/dts/templating/render.d.ts",
Expand Down
59 changes: 59 additions & 0 deletions packages/web-components/fast-element/src/binding/binding.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type { DOMAspect, DOMPolicy } from "../dom.js";
import type { Subscriber } from "../observation/notifier.js";
import type { Expression, ExpressionObserver } from "../observation/observable.js";

/**
* The directive from which a binding originates.
*
* @public
*/
export interface BindingDirective {
/**
* The binding.
*/
readonly dataBinding: Binding;

/**
* The evaluated target aspect.
*/
readonly targetAspect?: string;

/**
* The type of aspect to target.
*/
readonly aspectType?: DOMAspect;
}

/**
* Captures a binding expression along with related information and capabilities.
*
* @public
*/
export abstract class Binding<TSource = any, TReturn = any, TParent = any> {
/**
* Options associated with the binding.
*/
options?: any;

/**
* Creates a binding.
* @param evaluate - Evaluates the binding.
* @param policy - The security policy to associate with this binding.
* @param isVolatile - Indicates whether the binding is volatile.
*/
public constructor(
public evaluate: Expression<TSource, TReturn, TParent>,
public policy?: DOMPolicy,
public isVolatile: boolean = false
) {}

/**
* Creates an observer capable of notifying a subscriber when the output of a binding changes.
* @param subscriber - The subscriber to changes in the binding.
* @param directive - The Binding directive to create the observer for.
*/
abstract createObserver(
subscriber: Subscriber,
directive: BindingDirective
): ExpressionObserver<TSource, TReturn, TParent>;
}
21 changes: 21 additions & 0 deletions packages/web-components/fast-element/src/binding/normalize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { isFunction } from "../interfaces.js";
import type { Expression } from "../observation/observable.js";
import { Binding } from "./binding.js";
import { oneWay } from "./one-way.js";
import { oneTime } from "./one-time.js";

/**
* Normalizes the input value into a binding.
* @param value - The value to create the default binding for.
* @returns A binding configuration for the provided value.
* @public
*/
export function normalizeBinding<TSource = any, TReturn = any, TParent = any>(
value: Expression<TSource, TReturn, TParent> | Binding<TSource, TReturn, TParent> | {}
): Binding<TSource, TReturn, TParent> {
return isFunction(value)
? oneWay(value)
: value instanceof Binding
? value
: oneTime(() => value);
}
36 changes: 36 additions & 0 deletions packages/web-components/fast-element/src/binding/one-time.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { DOMPolicy } from "../dom.js";
import type {
Expression,
ExpressionController,
ExpressionObserver,
} from "../observation/observable.js";
import { makeSerializationNoop } from "../platform.js";
import { Binding } from "./binding.js";

class OneTimeBinding<TSource = any, TReturn = any, TParent = any>
extends Binding<TSource, TReturn, TParent>
implements ExpressionObserver<TSource, TReturn, TParent> {
createObserver(): ExpressionObserver<TSource, TReturn, TParent> {
return this;
}

bind(controller: ExpressionController): TReturn {
return this.evaluate(controller.source, controller.context);
}
}

makeSerializationNoop(OneTimeBinding);

/**
* Creates a one time binding
* @param expression - The binding to refresh when signaled.
* @param policy - The security policy to associate with th binding.
* @returns A binding configuration.
* @public
*/
export function oneTime<T = any>(
expression: Expression<T>,
policy?: DOMPolicy
): Binding<T> {
return new OneTimeBinding(expression, policy);
}
48 changes: 48 additions & 0 deletions packages/web-components/fast-element/src/binding/one-way.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { DOMPolicy } from "../dom.js";
import type { Subscriber } from "../observation/notifier.js";
import { Expression, ExpressionObserver, Observable } from "../observation/observable.js";
import { Binding } from "./binding.js";

class OneWayBinding<TSource = any, TReturn = any, TParent = any> extends Binding<
TSource,
TReturn,
TParent
> {
createObserver(
subscriber: Subscriber
): ExpressionObserver<TSource, TReturn, TParent> {
return Observable.binding(this.evaluate, subscriber, this.isVolatile);
}
}

/**
* Creates an standard binding.
* @param expression - The binding to refresh when changed.
* @param policy - The security policy to associate with th binding.
* @param isVolatile - Indicates whether the binding is volatile or not.
* @returns A binding configuration.
* @public
*/
export function oneWay<T = any>(
expression: Expression<T>,
policy?: DOMPolicy,
isVolatile = Observable.isVolatileBinding(expression)
): Binding<T> {
return new OneWayBinding(expression, policy, isVolatile);
}

/**
* Creates an event listener binding.
* @param expression - The binding to invoke when the event is raised.
* @param options - Event listener options.
* @returns A binding configuration.
* @public
*/
export function listener<T = any>(
expression: Expression<T>,
options?: AddEventListenerOptions
): Binding<T> {
const config = new OneWayBinding(expression);
config.options = options;
return config;
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ import { isString } from "../interfaces.js";
import type { Subscriber } from "../observation/notifier.js";
import type { DOMPolicy } from "../dom.js";
import { makeSerializationNoop } from "../platform.js";
import type { HTMLBindingDirective } from "./binding.js";
import { Binding } from "./html-directive.js";
import { Binding } from "./binding.js";

const subscribers: Record<
string,
Expand Down Expand Up @@ -96,7 +95,6 @@ class SignalBinding<TSource = any, TReturn = any, TParent = any> extends Binding
TParent
> {
createObserver(
directive: HTMLBindingDirective,
subscriber: Subscriber
): ExpressionObserver<TSource, TReturn, TParent> {
return new SignalObserver(this, subscriber);
Expand Down
Loading

0 comments on commit 70eb15b

Please sign in to comment.