Skip to content

Commit

Permalink
feat(fast-element): enable CSSStyleSheet and string to be used as sty…
Browse files Browse the repository at this point in the history
…les (#3345)

* feat(fast-element): enable CSSStyleSheet and string to be used as styles

* docs(fast-element): add note about css with strings and CSSStyleSheet

* Update packages/web-components/fast-element/docs/guide/leveraging-css.md

Co-authored-by: Nicholas Rice <[email protected]>

Co-authored-by: Nicholas Rice <[email protected]>
  • Loading branch information
EisenbergEffect and nicholasrice authored Jun 22, 2020
1 parent 53e7380 commit 70e2f7f
Show file tree
Hide file tree
Showing 5 changed files with 48 additions and 27 deletions.
10 changes: 5 additions & 5 deletions packages/web-components/fast-element/docs/api-report.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,10 +184,10 @@ export class Controller extends PropertyChangeNotifier {
readonly view: ElementView | null;
}

// Warning: (ae-forgotten-export) The symbol "InjectableStyles" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "ComposableStyles" needs to be exported by the entry point index.d.ts
//
// @public
export function css(strings: TemplateStringsArray, ...values: InjectableStyles[]): ElementStyles;
export function css(strings: TemplateStringsArray, ...values: ComposableStyles[]): ElementStyles;

// @public
export function customElement(nameOrDef: string | PartialFASTElementDefinition): (type: Function) => void;
Expand Down Expand Up @@ -234,7 +234,7 @@ export abstract class ElementStyles {
// @internal (undocumented)
abstract removeStylesFrom(target: StyleTarget): void;
// @internal (undocumented)
abstract readonly styles: ReadonlyArray<InjectableStyles>;
abstract readonly styles: ReadonlyArray<ComposableStyles>;
withBehaviors(...behaviors: Behavior[]): this;
withKey(key: string): this;
}
Expand Down Expand Up @@ -288,7 +288,7 @@ export const FASTElement: (new () => HTMLElement & FASTElement) & {

// @public
export class FASTElementDefinition {
constructor(name: string, attributes: ReadonlyArray<AttributeDefinition>, propertyLookup: Record<string, AttributeDefinition>, attributeLookup: Record<string, AttributeDefinition>, template?: ElementViewTemplate, styles?: ElementStyles, shadowOptions?: ShadowRootInit, elementOptions?: ElementDefinitionOptions);
constructor(name: string, attributes: ReadonlyArray<AttributeDefinition>, propertyLookup: Record<string, AttributeDefinition>, attributeLookup: Record<string, AttributeDefinition>, template?: ElementViewTemplate, styles?: ComposableStyles, shadowOptions?: ShadowRootInit, elementOptions?: ElementDefinitionOptions);
readonly attributeLookup: Record<string, AttributeDefinition>;
readonly attributes: ReadonlyArray<AttributeDefinition>;
readonly elementOptions?: ElementDefinitionOptions;
Expand Down Expand Up @@ -356,7 +356,7 @@ export interface PartialFASTElementDefinition {
readonly elementOptions?: ElementDefinitionOptions;
readonly name: string;
readonly shadowOptions?: Partial<ShadowRootInit> | null;
readonly styles?: ElementStyles;
readonly styles?: ComposableStyles;
readonly template?: ElementViewTemplate;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@ const styles = css`

Rather than simply concatenating CSS strings, the `css` helper understands that `normalize` is `ElementStyles` and is able to re-use the same Constructable StyleSheet instance as any other component that uses `normalize`.

:::note
You can also pass a CSS string or a [CSSStyleSheet](https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet) instance directly to the element definition, without needing to use the `css` helper and it will automatically be converted into `ElementStyles`. The advantage of using the `css` helper is that it enables the rich composition and reuse of styles described above, with automatic runtime caching for memory efficiency and performance.
:::

## Shadow DOM Styling

You may have noticed the `:host` selector we used in our `name-tag` styles. This selector allows us to apply styles directly to our custom element. Here are a few things to consider always configuring for your host element:
Expand Down Expand Up @@ -172,4 +176,4 @@ Custom Elements that have not been [upgraded](https://developers.google.com/web/

:::important
The consuming application must apply this, as the components themselves do not.
:::
:::
3 changes: 2 additions & 1 deletion packages/web-components/fast-element/docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

## Short-term

* **Feature** Enable simple function converters for `@attr`
* **Feature**: Enable event delegation through a syntax like `@click.delegate=...`
* **Feature**: Enable event capture through a syntax like `@click.capture=...`

## Medium-term

Expand All @@ -15,5 +17,4 @@

## Long-term

* **Feature:** Support interpolating `StyleSheet` instances into the `css` string (prepare for CSS Modules)
* **Feature:** Support interpolating `Document` instances into the `html` string (prepare for HTML modules).
13 changes: 9 additions & 4 deletions packages/web-components/fast-element/src/fast-definitions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ElementViewTemplate } from "./template";
import { ElementStyles } from "./styles";
import { ElementStyles, css, ComposableStyles } from "./styles";
import { AttributeConfiguration, AttributeDefinition } from "./attributes";

/**
Expand Down Expand Up @@ -64,7 +64,7 @@ export class FASTElementDefinition {
propertyLookup: Record<string, AttributeDefinition>,
attributeLookup: Record<string, AttributeDefinition>,
template?: ElementViewTemplate,
styles?: ElementStyles,
styles?: ComposableStyles,
shadowOptions?: ShadowRootInit,
elementOptions?: ElementDefinitionOptions
) {
Expand All @@ -73,9 +73,14 @@ export class FASTElementDefinition {
this.propertyLookup = propertyLookup;
this.attributeLookup = attributeLookup;
this.template = template;
this.styles = styles;
this.shadowOptions = shadowOptions;
this.elementOptions = elementOptions;
this.styles =
styles !== void 0 && !(styles instanceof ElementStyles)
? css`
${styles}
`
: styles;
}
}

Expand All @@ -97,7 +102,7 @@ export interface PartialFASTElementDefinition {
/**
* The styles to associated with the custom element.
*/
readonly styles?: ElementStyles;
readonly styles?: ComposableStyles;

/**
* The custom attributes of the custom element.
Expand Down
43 changes: 27 additions & 16 deletions packages/web-components/fast-element/src/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const styleLookup = new Map<string, ElementStyles>();
*/
export abstract class ElementStyles {
/** @internal */
public abstract readonly styles: ReadonlyArray<InjectableStyles>;
public abstract readonly styles: ReadonlyArray<ComposableStyles>;

/** @internal */
public abstract readonly behaviors: ReadonlyArray<Behavior> | null = null;
Expand Down Expand Up @@ -78,22 +78,29 @@ export abstract class ElementStyles {
}
}

type InjectableStyles = string | ElementStyles;
type ElementStyleFactory = (styles: ReadonlyArray<InjectableStyles>) => ElementStyles;
/**
* Represents styles that can be composed into the ShadowDOM of a custom element.
* @public
*/
export type ComposableStyles = string | ElementStyles | CSSStyleSheet;

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

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

function reduceBehaviors(
styles: ReadonlyArray<InjectableStyles>
styles: ReadonlyArray<ComposableStyles>
): ReadonlyArray<Behavior> | null {
return styles
.map((x: InjectableStyles) => (x instanceof ElementStyles ? x.behaviors : null))
.map((x: ComposableStyles) => (x instanceof ElementStyles ? x.behaviors : null))
.reduce((prev: Behavior[] | null, curr: Behavior[] | null) => {
if (curr === null) {
return prev;
Expand All @@ -118,12 +125,16 @@ export class AdoptedStyleSheetsStyles extends ElementStyles {
public readonly behaviors: ReadonlyArray<Behavior> | null = null;

public constructor(
public styles: InjectableStyles[],
public styles: ComposableStyles[],
styleSheetCache: Map<string, CSSStyleSheet>
) {
super();
this.behaviors = reduceBehaviors(styles);
this.styleSheets = reduceStyles(styles).map((x: string) => {
this.styleSheets = reduceStyles(styles).map((x: string | CSSStyleSheet) => {
if (x instanceof CSSStyleSheet) {
return x;
}

let sheet = styleSheetCache.get(x);

if (sheet === void 0) {
Expand Down Expand Up @@ -159,10 +170,10 @@ class StyleElementStyles extends ElementStyles {
private readonly styleClass: string;
public readonly behaviors: ReadonlyArray<Behavior> | null = null;

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

Expand Down Expand Up @@ -193,11 +204,11 @@ class StyleElementStyles extends ElementStyles {
const createStyles: ElementStyleFactory = (() => {
if (DOM.supportsAdoptedStyleSheets) {
const styleSheetCache = new Map();
return (styles: InjectableStyles[]) =>
return (styles: ComposableStyles[]) =>
new AdoptedStyleSheetsStyles(styles, styleSheetCache);
}

return (styles: InjectableStyles[]) => new StyleElementStyles(styles);
return (styles: ComposableStyles[]) => new StyleElementStyles(styles);
})();
/* eslint-enable @typescript-eslint/explicit-function-return-type */

Expand All @@ -211,16 +222,16 @@ const createStyles: ElementStyleFactory = (() => {
*/
export function css(
strings: TemplateStringsArray,
...values: InjectableStyles[]
...values: ComposableStyles[]
): ElementStyles {
const styles: InjectableStyles[] = [];
const styles: ComposableStyles[] = [];
let cssString = "";

for (let i = 0, ii = strings.length - 1; i < ii; ++i) {
cssString += strings[i];
const value = values[i];

if (value instanceof ElementStyles) {
if (value instanceof ElementStyles || value instanceof CSSStyleSheet) {
if (cssString.trim() !== "") {
styles.push(cssString);
cssString = "";
Expand Down

0 comments on commit 70e2f7f

Please sign in to comment.