Skip to content

Commit

Permalink
fix: ssr bug fixes (#5850)
Browse files Browse the repository at this point in the history
* expose dom shim from package root

* ensure DOM updates are made synchronously

* upgrade lit to use latest dom shim

* fleshing out dom-shim with foundation-element requirements

* fix root <template> behavior to be more durable to formatting

* implement classList binding for native elements

* enables style retrevial for a custom element through SSR style-strategy

* clean up exports

* add tagName to elements created by ElementRenderer to be used by componentPresentation

* prettier

Co-authored-by: nicholasrice <[email protected]>
  • Loading branch information
nicholasrice and nicholasrice committed May 9, 2022
1 parent 09e53b7 commit a55feb1
Show file tree
Hide file tree
Showing 10 changed files with 260 additions and 39 deletions.
6 changes: 5 additions & 1 deletion packages/web-components/fast-ssr/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,13 @@
"description": "A package for rendering FAST components outside the browser.",
"main": "./dist/esm/exports.js",
"types": "./dist/dts/exports.d.ts",
"exports": {
"." : "./dist/esm/exports.js",
"./install-dom-shim": "./dist/esm/dom-shim.js"
},
"private": true,
"dependencies": {
"@lit-labs/ssr": "^1.0.0-rc.2",
"@lit-labs/ssr": "^2.1.0",
"@microsoft/fast-element": "^1.5.0",
"parse5": "^6.0.1",
"tslib": "^1.11.1"
Expand Down
155 changes: 141 additions & 14 deletions packages/web-components/fast-ssr/src/dom-shim.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,151 @@
import { installWindowOnGlobal } from "@lit-labs/ssr/lib/dom-shim.js";

installWindowOnGlobal();

// Implement shadowRoot getter on HTMLElement.
// Can be removed if https://github.com/lit/lit/issues/2652
// is integrated.
class PatchedHTMLElement extends HTMLElement {
#shadowRoot: ShadowRoot | null = null;
get shadowRoot() {
class DOMTokenList {
#tokens = new Set<string>();

public add(value: string) {
this.#tokens.add(value);
}

public remove(value: string) {
this.#tokens.delete(value);
}

public contains(value: string) {
return this.#tokens.has(value);
}

public toggle(value: string, force?: boolean) {
const add = force === undefined ? !this.contains(value) : force;

if (add) {
this.add(value);
return true;
} else {
this.remove(value);
return false;
}
}

public toString() {
return Array.from(this.#tokens).join(" ");
}

*[Symbol.iterator]() {
yield* this.#tokens.values();
}
}
class Node {
appendChild() {}
removeChild() {}
}

class Element extends Node {}

abstract class HTMLElement extends Element {
#attributes = new Map<string, string | DOMTokenList>();
#shadowRoot: null | ShadowRoot = null;

public readonly classList = new DOMTokenList();

public get attributes(): { name: string; value: string }[] {
return Array.from(this.#attributes).map(([name, value]) => {
if (typeof value === "string") {
return { name, value };
} else {
return { name, value: value.toString() };
}
});
}

public get shadowRoot() {
return this.#shadowRoot;
}
attachShadow(init: ShadowRootInit) {
const root = super.attachShadow(init);

if (init.mode === "open") {
this.#shadowRoot = root;
public abstract attributeChangedCallback?(
name: string,
old: string | null,
value: string | null
): void;

public setAttribute(name: string, value: string) {
let _value: string | DOMTokenList = value;
if (name === "class") {
_value = new DOMTokenList();
value.split(" ").forEach(className => {
(_value as DOMTokenList).add(className);
});
}
this.#attributes.set(name, _value);
}

public removeAttribute(name: string) {
this.#attributes.delete(name);
}

public hasAttribute(name: string) {
return this.#attributes.has(name);
}

public attachShadow(init: ShadowRootInit) {
const shadowRoot = ({ host: this } as unknown) as ShadowRoot;
if (init && init.mode === "open") {
this.#shadowRoot = shadowRoot;
}
return shadowRoot;
}

public getAttribute(name: string) {
const value = this.#attributes.get(name);
return value === undefined
? null
: value instanceof DOMTokenList
? value.toString()
: value;
}
}

return root;
class Document {
head = new Node();
adoptedStyleSheets = [];
createTreeWalker() {
return {};
}
createTextNode() {
return {};
}
createElement(tagName: string) {
return { tagName };
}
querySelector() {
return undefined;
}
addEventListener() {}
}

class CSSStyleDeclaration {
setProperty() {}
}

window.HTMLElement = PatchedHTMLElement;
class CSSStyleSheet {
get cssRules() {
return [{ style: new CSSStyleDeclaration() }];
}
replace() {}
insertRule() {
return 0;
}
}

class MediaQueryList {
addListener() {}
matches = false;
}
installWindowOnGlobal({
matchMedia: () => new MediaQueryList(),
HTMLElement,
Document,
document: new Document(),
Node,
CSSStyleSheet,
});
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@ test.describe("FASTElementRenderer", () => {
test(`should render stylesheets as 'style' elements by default`, () => {
const { templateRenderer, defaultRenderInfo} = fastSSR();
const result = consolidate(templateRenderer.render(html`<styled-element></styled-element>`, defaultRenderInfo));
expect(result).toBe("<styled-element><template shadowroot=\"open\"><style>:host { display: block; }:host { color: red; }</style></template></styled-element>");
expect(result).toBe("<styled-element><template shadowroot=\"open\"><style>:host { display: block; }</style><style>:host { color: red; }</style></template></styled-element>");
});
test(`should render stylesheets as 'fast-style' elements when configured`, () => {
const { templateRenderer, defaultRenderInfo} = fastSSR({useFASTStyle: true});
const result = consolidate(templateRenderer.render(html`<styled-element></styled-element>`, defaultRenderInfo));
expect(result).toBe(`<styled-element><template shadowroot=\"open\"><fast-style style-id="fast-style-0" css=":host { display: block; }:host { color: red; }"></fast-style></template></styled-element>`);
expect(result).toBe(`<styled-element><template shadowroot=\"open\"><fast-style style-id="fast-style-0" css=":host { display: block; }\"></fast-style><fast-style style-id=\"fast-style-1\" css=\":host { color: red; }"></fast-style></template></styled-element>`);
});
});
});
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { ElementRenderer, RenderInfo } from "@lit-labs/ssr";
import { Aspect, DOM, ExecutionContext, FASTElement } from "@microsoft/fast-element";
import { StyleRenderer } from "../styles/style-renderer.js";
import { TemplateRenderer } from "../template-renderer/template-renderer.js";
import { SSRView } from "../view.js";
import { StyleRenderer } from "../styles/style-renderer.js";
import { FASTSSRStyleStrategy } from "./style-strategy.js";

export abstract class FASTElementRenderer extends ElementRenderer {
/**
Expand Down Expand Up @@ -88,6 +89,7 @@ export abstract class FASTElementRenderer extends ElementRenderer {

if (ctor) {
this.element = new ctor() as FASTElement;
(this.element as any).tagName = tagName;
} else {
throw new Error(
`FASTElementRenderer was unable to find a constructor for a custom element with the tag name '${tagName}'.`
Expand All @@ -110,10 +112,12 @@ export abstract class FASTElementRenderer extends ElementRenderer {
*/
public *renderShadow(renderInfo: RenderInfo): IterableIterator<string> {
const view = this.element.$fastController.view;
const styles = this.element.$fastController.styles;
const styles = FASTSSRStyleStrategy.getStylesFor(this.element);

if (styles) {
yield this.styleRenderer.render(styles);
for (const style of styles) {
yield this.styleRenderer.render(style);
}
}

if (view !== null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,37 @@
import { StyleStrategy } from "@microsoft/fast-element";
import { StyleStrategy, StyleTarget } from "@microsoft/fast-element";

const sheetsForElement = new WeakMap<Element, Set<string | CSSStyleSheet>>();

function getOrCreateFor(target: Element): Set<string | CSSStyleSheet> {
let set = sheetsForElement.get(target);
if (set) {
return set;
}

set = new Set<string | CSSStyleSheet>();
sheetsForElement.set(target, set);

return set;
}

function isShadowRoot(target: any): target is ShadowRoot {
return !!target.host;
}

export class FASTSSRStyleStrategy implements StyleStrategy {
addStylesTo() {}
addStylesTo(target: StyleTarget) {
if (isShadowRoot(target)) {
const cache = getOrCreateFor(target.host);

this.styles.forEach(style => cache?.add(style));
}
}

removeStylesFrom() {}

constructor(private styles: (string | CSSStyleSheet)[]) {}

public static getStylesFor(target: Element): Set<string | CSSStyleSheet> | null {
return sheetsForElement.get(target) || null;
}
}
12 changes: 9 additions & 3 deletions packages/web-components/fast-ssr/src/exports.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { RenderInfo } from "@lit-labs/ssr";
import { Compiler, ElementStyles, ViewBehaviorFactory } from "@microsoft/fast-element";
import {
Compiler,
DOM,
ElementStyles,
ViewBehaviorFactory,
} from "@microsoft/fast-element";
import { FASTElementRenderer } from "./element-renderer/element-renderer.js";
import { FASTSSRStyleStrategy } from "./element-renderer/style-strategy.js";
import {
Expand Down Expand Up @@ -31,16 +36,17 @@ Compiler.setDefaultStrategy(
);

ElementStyles.setDefaultStrategy(FASTSSRStyleStrategy);
DOM.setUpdateMode(false);

/**
* Factory for creating SSR rendering assets.
* @param config - FAST SSR configuration
*
* @example
* ```ts
* import "@lit-labs/ssr/lib/install-global-dom-shim.js";
* import { html } from "@microsoft/fast-element";
* import "@microsoft/install-dom-shim";
* import fastSSR from "@microsoft/fast-ssr";
* import { html } from "@microsoft/fast-element";
* const { templateRenderer, defaultRenderInfo } = fastSSR();
*
* const streamableSSRResult = templateRenderer.render(html`...`, defaultRenderInfo);
Expand Down
8 changes: 4 additions & 4 deletions packages/web-components/fast-ssr/src/styles/style-renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,18 @@ function collectStyles(style: ComposableStyles): string {
}

export interface StyleRenderer {
render(styles: ElementStyles): string;
render(styles: ComposableStyles): string;
}

export class FASTStyleStyleRenderer implements StyleRenderer {
private static stylesheetCache = new Map<ElementStyles, string>();
private static stylesheetCache = new Map<ComposableStyles, string>();
private static nextId = (() => {
let id = 0;
const prefix = "fast-style";
return () => `${prefix}-${id++}`;
})();

public render(styles: ElementStyles): string {
public render(styles: ComposableStyles): string {
let id = FASTStyleStyleRenderer.stylesheetCache.get(styles);
const content = id === undefined ? collectStyles(styles) : null;
let contentAttr = "";
Expand All @@ -47,7 +47,7 @@ export class FASTStyleStyleRenderer implements StyleRenderer {
}

export class StyleElementStyleRenderer implements StyleRenderer {
public render(styles: ElementStyles): string {
public render(styles: ComposableStyles): string {
return `<style>${collectStyles(styles)}</style>`;
}
}
Loading

0 comments on commit a55feb1

Please sign in to comment.