Skip to content

Commit

Permalink
feat(fast-element): style registration and associated behaviors (micr…
Browse files Browse the repository at this point in the history
…osoft#2925)

* feat(fast-element): style registration and behavior association

* feat(fast-element): style registration and behavior association

* fix(fast-element): call correct lifecycle method for behaviors

* doc(fast-element): update roadmap

* fix(fast-element): reduce behaviors when composing styles

* style(fast-element): prettier changes

* feat(fast-element): pass source on behavior unbind

* style(fast-element): prettier modification

* chore(fast-element): prettier fixes

* chore(fast-element): remove unused import

* chore(fast-element): fix lint and prettier issues
  • Loading branch information
EisenbergEffect authored Apr 14, 2020
1 parent 2223e95 commit 70849d7
Show file tree
Hide file tree
Showing 6 changed files with 235 additions and 48 deletions.
2 changes: 0 additions & 2 deletions packages/fast-element/docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

## Short-term

* **Fix**: Improve subscription cleanup on complex observable expressions.
* **Feature**: ElementStyle Registration and Behavior Association.
* **Feature**: Support `class` and/or `style` bindings on the host.
* **Test**: Testing infrastructure and test coverage.

Expand Down
109 changes: 103 additions & 6 deletions packages/fast-element/src/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { Container, InterfaceSymbol, Registry, Resolver } from "./di";
import { ElementView } from "./view";
import { PropertyChangeNotifier } from "./observation/notifier";
import { Observable } from "./observation/observable";
import { Behavior } from "./directives/behavior";
import { ElementStyles, StyleTarget } from "./styles";

const defaultEventOptions: CustomEventInit = {
bubbles: true,
Expand All @@ -14,6 +16,7 @@ export class Controller extends PropertyChangeNotifier implements Container {
public isConnected: boolean = false;
private resolvers: Map<any, Resolver> = new Map<any, Resolver>();
private boundObservables: Record<string, any> | null = null;
private behaviors: Behavior[] | null = null;

public constructor(
public readonly element: HTMLElement,
Expand All @@ -38,8 +41,8 @@ export class Controller extends PropertyChangeNotifier implements Container {
}
}

if (styles !== void 0 && shadowRoot !== void 0) {
styles.applyTo(shadowRoot);
if (styles !== void 0) {
this.addStyles(styles, shadowRoot);
}

definition.dependencies.forEach((x: Registry) => x.register(this));
Expand All @@ -65,6 +68,78 @@ export class Controller extends PropertyChangeNotifier implements Container {
}
}

public addStyles(
styles: ElementStyles,
target: StyleTarget | null = this.element.shadowRoot
): void {
if (target !== null) {
styles.addStylesTo(target);
}

const sourceBehaviors = styles.behaviors;

if (sourceBehaviors !== null) {
this.addBehaviors(sourceBehaviors);
}
}

public removeStyles(styles: ElementStyles): void {
const target = this.element.shadowRoot;

if (target !== null) {
styles.removeStylesFrom(target);
}

const sourceBehaviors = styles.behaviors;

if (sourceBehaviors !== null) {
this.removeBehaviors(sourceBehaviors);
}
}

public addBehaviors(behaviors: ReadonlyArray<Behavior>): void {
const targetBehaviors = this.behaviors || (this.behaviors = []);
const length = behaviors.length;

for (let i = 0; i < length; ++i) {
targetBehaviors.push(behaviors[i]);
}

if (this.isConnected) {
const element = this.element;

for (let i = 0; i < length; ++i) {
behaviors[i].bind(element);
}
}
}

public removeBehaviors(behaviors: ReadonlyArray<Behavior>): void {
const targetBehaviors = this.behaviors;

if (targetBehaviors === null) {
return;
}

const length = behaviors.length;

for (let i = 0; i < length; ++i) {
const index = targetBehaviors.indexOf(behaviors[i]);

if (index !== -1) {
targetBehaviors.splice(index, 1);
}
}

if (this.isConnected) {
const element = this.element;

for (let i = 0; i < length; ++i) {
behaviors[i].unbind(element);
}
}
}

public onConnectedCallback(): void {
if (this.isConnected) {
return;
Expand All @@ -85,8 +160,18 @@ export class Controller extends PropertyChangeNotifier implements Container {
this.boundObservables = null;
}

if (this.view !== null) {
this.view.bind(element);
const view = this.view;

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

const behaviors = this.behaviors;

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

this.isConnected = true;
Expand All @@ -99,8 +184,20 @@ export class Controller extends PropertyChangeNotifier implements Container {

this.isConnected = false;

if (this.view !== null) {
this.view.unbind();
const view = this.view;

if (view !== null) {
view.unbind();
}

const behaviors = this.behaviors;

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

for (let i = 0, ii = behaviors.length; i < ii; ++i) {
behaviors[i].unbind(element);
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion packages/fast-element/src/directives/behavior.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export interface Behavior {
bind(source: unknown): void;
unbind(): void;
unbind(source: unknown): void;
}

export interface BehaviorFactory {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ export function calcSplices(
old: any[],
oldStart: number,
oldEnd: number
): readonly never[] | Splice[] {
): ReadonlyArray<never> | Splice[] {
let prefixCount = 0;
let suffixCount = 0;

Expand Down
138 changes: 110 additions & 28 deletions packages/fast-element/src/styles.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,86 @@
const elementStylesBrand = Symbol();
import { Behavior } from "./directives/behavior";

export interface ElementStyles {
readonly styles: InjectableStyles[];
applyTo(shadowRoot: ShadowRoot): void;
export interface StyleTarget {
adoptedStyleSheets?: CSSStyleSheet[];

prepend(node: Node): void;
removeChild(node: Node): void;
querySelectorAll<E extends Element = Element>(selectors: string): NodeListOf<E>;
}

type InjectableStyles = string | ElementStyles;
type ElementStyleFactory = (styles: InjectableStyles[]) => ElementStyles;
const styleLookup = new Map<string, ElementStyles>();

export abstract class ElementStyles {
/** @internal */
public abstract readonly styles: ReadonlyArray<InjectableStyles>;

/** @internal */
public abstract readonly behaviors: ReadonlyArray<Behavior> | null = null;

/** @internal */
public abstract addStylesTo(target: StyleTarget): void;

/** @internal */
public abstract removeStylesFrom(target: StyleTarget): void;

public withBehaviors(...behaviors: Behavior[]): this {
(this.behaviors as any) =
this.behaviors === null ? behaviors : this.behaviors.concat(behaviors);

return this;
}

function isElementStyles(object: any): object is ElementStyles {
return object.brand === elementStylesBrand;
public withKey(key: string): this {
styleLookup.set(key, this);
return this;
}

public static find(key: string): ElementStyles | null {
return styleLookup.get(key) || null;
}
}

function reduceStyles(styles: InjectableStyles[]): string[] {
type InjectableStyles = string | ElementStyles;
type ElementStyleFactory = (styles: ReadonlyArray<InjectableStyles>) => ElementStyles;

function reduceStyles(styles: ReadonlyArray<InjectableStyles>): string[] {
return styles
.map((x: InjectableStyles) => (isElementStyles(x) ? reduceStyles(x.styles) : [x]))
.map((x: InjectableStyles) =>
x instanceof ElementStyles ? reduceStyles(x.styles) : [x]
)
.reduce((prev: string[], curr: string[]) => prev.concat(curr), []);
}

type HasAdoptedStyleSheets = ShadowRoot & {
adoptedStyleSheets: CSSStyleSheet[];
};
function reduceBehaviors(
styles: ReadonlyArray<InjectableStyles>
): ReadonlyArray<Behavior> | null {
return styles
.map((x: InjectableStyles) => (x instanceof ElementStyles ? x.behaviors : null))
.reduce((prev: Behavior[] | null, curr: Behavior[] | null) => {
if (curr === null) {
return prev;
}

if (prev === null) {
prev = [];
}

return prev.concat(curr);
}, null as Behavior[] | null);
}

export class AdoptedStyleSheetsStyles implements ElementStyles {
public readonly brand: symbol = elementStylesBrand;
// https://wicg.github.io/construct-stylesheets/
// https://developers.google.com/web/updates/2019/02/constructable-stylesheets
export class AdoptedStyleSheetsStyles extends ElementStyles {
private readonly styleSheets: CSSStyleSheet[];
public readonly behaviors: ReadonlyArray<Behavior> | null = null;

public constructor(
public styles: InjectableStyles[],
styleSheetCache: Map<string, CSSStyleSheet>
) {
super();
this.behaviors = reduceBehaviors(styles);
this.styleSheets = reduceStyles(styles).map((x: string) => {
let sheet = styleSheetCache.get(x);

Expand All @@ -43,31 +94,54 @@ export class AdoptedStyleSheetsStyles implements ElementStyles {
});
}

public applyTo(shadowRoot: HasAdoptedStyleSheets): void {
// https://wicg.github.io/construct-stylesheets/
// https://developers.google.com/web/updates/2019/02/constructable-stylesheets
shadowRoot.adoptedStyleSheets = [
...shadowRoot.adoptedStyleSheets,
...this.styleSheets,
];
public addStylesTo(target: StyleTarget): void {
target.adoptedStyleSheets = [...target.adoptedStyleSheets!, ...this.styleSheets];
}

public removeStylesFrom(target: StyleTarget): void {
const sourceSheets = this.styleSheets;
target.adoptedStyleSheets = target.adoptedStyleSheets!.filter(
(x: CSSStyleSheet) => !sourceSheets.includes(x)
);
}
}

export class StyleElementStyles implements ElementStyles {
public readonly brand: symbol = elementStylesBrand;
let styleClassId = 0;

function getNextStyleClass(): string {
return `fast-style-class-${++styleClassId}`;
}

export class StyleElementStyles extends ElementStyles {
public readonly behaviors: ReadonlyArray<Behavior> | null = null;

private styleSheets: string[];
private styleClass: string;

public constructor(public styles: InjectableStyles[]) {
super();
this.behaviors = reduceBehaviors(styles);
this.styleSheets = reduceStyles(styles);
this.styleClass = getNextStyleClass();
}

public applyTo(shadowRoot: ShadowRoot): void {
public addStylesTo(target: StyleTarget): void {
const styleSheets = this.styleSheets;
const styleClass = this.styleClass;

for (let i = styleSheets.length - 1; i > -1; --i) {
const element = document.createElement("style");
element.innerHTML = styleSheets[i];
shadowRoot.prepend(element);
element.className = styleClass;
target.prepend(element);
}
}

public removeStylesFrom(target: StyleTarget): void {
const styles = target.querySelectorAll(`.${this.styleClass}`);

for (const style of styles) {
target.removeChild(style);
}
}
}
Expand Down Expand Up @@ -95,15 +169,23 @@ export function css(
cssString += strings[i];
const value = values[i];

if (isElementStyles(value)) {
if (value instanceof ElementStyles) {
if (cssString.trim() !== "") {
styles.push(cssString);
cssString = "";
}

styles.push(value);
} else {
cssString += value;
}
}

cssString += strings[strings.length - 1];
styles.push(cssString);

if (cssString.trim() !== "") {
styles.push(cssString);
}

return createStyles(styles);
}
Loading

0 comments on commit 70849d7

Please sign in to comment.