From d5e91d1637c521ed13e88dc93fbf01f08e3be5b5 Mon Sep 17 00:00:00 2001 From: Nicholas Rice <3213292+nicholasrice@users.noreply.github.com> Date: Tue, 1 Nov 2022 13:29:53 -0700 Subject: [PATCH] feat: add `defer-hydration` to ssr renderer (#6492) * yield defer-hydration attribute when configured * Make defer-hydration the default behavior, fixing tests * adding disable deferHydration test * update readme * update config comments * fixing documentation * Change files * update API report * make deferHydration false by default Co-authored-by: nicholasrice --- ...-749d9ba6-9033-4475-8641-55e0cd3b14cc.json | 7 ++++ packages/web-components/fast-ssr/README.md | 28 ++++++++++++++ .../fast-ssr/docs/api-report.md | 18 +++++++-- .../fast-ssr/src/configure-fast-element.ts | 1 - .../elemenent-renderer.spec.ts | 3 +- .../element-renderer/fast-element-renderer.ts | 25 ++++++------ .../fast-ssr/src/exports.spec.ts | 25 ++++++++++++ .../web-components/fast-ssr/src/exports.ts | 38 +++++++++++++++++-- .../template-renderer.spec.ts | 2 +- 9 files changed, 123 insertions(+), 24 deletions(-) create mode 100644 change/@microsoft-fast-ssr-749d9ba6-9033-4475-8641-55e0cd3b14cc.json diff --git a/change/@microsoft-fast-ssr-749d9ba6-9033-4475-8641-55e0cd3b14cc.json b/change/@microsoft-fast-ssr-749d9ba6-9033-4475-8641-55e0cd3b14cc.json new file mode 100644 index 00000000000..eea100235c5 --- /dev/null +++ b/change/@microsoft-fast-ssr-749d9ba6-9033-4475-8641-55e0cd3b14cc.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Adds `defer-hydration` attribute emission during rendering", + "packageName": "@microsoft/fast-ssr", + "email": "nicholasrice@users.noreply.github.com", + "dependentChangeType": "prerelease" +} diff --git a/packages/web-components/fast-ssr/README.md b/packages/web-components/fast-ssr/README.md index 6e36ebebfcb..f5ca7c0f724 100644 --- a/packages/web-components/fast-ssr/README.md +++ b/packages/web-components/fast-ssr/README.md @@ -190,6 +190,34 @@ ElementRenderer.disable("my-element"); templateRenderer.render(html``); ``` +### Hydration +#### `defer-hydration` Attribute +The `defer-hydration` attribute is an attribute that indicates to client-side code that the element should not hydrate it's view. When the attribute is removed, the element is free to hydrate itself. + +The SSR renderer can be configured to emit the `defer-hydration` attribute to all FAST custom elements: +```ts +const { templateRenderer } = fastSSR({deferHydration: true}); +``` + +> For more information on this community-protocol, see https://github.com/webcomponents-cg/community-protocols/pull/15 +#### Configuring FAST-Element +`@microsoft/fast-element` must be configured to respect the `defer-hydration` attribute. To do this, simply import the install code into the client-side application before defining the custom elements: +```ts +import "@microsoft/fast-element/install-element-hydration"; + +// Define custom elements +``` + +Alternatively, the `HydratableElementController` can be imported and installed manually: + +```ts +import { HydratableElementController } from "@microsoft/fast-element/element-hydration"; + +HydratableElementController.install(); +``` + +After you do this, `@microsoft/fast-element` will wait until the `defer-hydration` attribute is removed (if present during connection) before doing connection work like rendering templates, applying stylesheets, and binding behaviors. + ### Configuring the RenderInfo Object `TemplateRenderer.render()` must be invoked with a `RenderInfo` object. Its purpose is to provide different element renderers to the process, as well as metadata about the rendering process. It can be used to render custom elements from different templating libraries in the same process. By default, `TemplateRenderer.render()` will create a `RenderInfo` object for you, but you can also easily construct your own using `TemplateRenderer.createRenderInfo()`: diff --git a/packages/web-components/fast-ssr/docs/api-report.md b/packages/web-components/fast-ssr/docs/api-report.md index 28c2c2a8910..122ddb83bc6 100644 --- a/packages/web-components/fast-ssr/docs/api-report.md +++ b/packages/web-components/fast-ssr/docs/api-report.md @@ -78,6 +78,14 @@ function fastSSR(): { ElementRenderer: ConstructableFASTElementRenderer; }; +// Warning: (ae-incompatible-release-tags) The symbol "fastSSR" is marked as @public, but its signature references "SSRConfiguration" which is marked as @beta +// +// @public (undocumented) +function fastSSR(config: Omit): { + templateRenderer: TemplateRenderer; + ElementRenderer: ConstructableFASTElementRenderer; +}; + // @beta (undocumented) function fastSSR(config: SSRConfiguration & Record<"renderMode", "sync">): { templateRenderer: TemplateRenderer; @@ -124,8 +132,8 @@ export const RequestStorageManager: Readonly<{ // @beta export interface SSRConfiguration { - // (undocumented) - renderMode: "sync" | "async"; + deferHydration?: boolean; + renderMode?: "sync" | "async"; } // @beta @@ -160,8 +168,10 @@ export interface ViewBehaviorFactoryRenderer { // Warnings were encountered during analysis: // -// dist/dts/exports.d.ts:18:5 - (ae-forgotten-export) The symbol "SyncFASTElementRenderer" needs to be exported by the entry point exports.d.ts -// dist/dts/exports.d.ts:28:5 - (ae-forgotten-export) The symbol "AsyncFASTElementRenderer" needs to be exported by the entry point exports.d.ts +// dist/dts/exports.d.ts:37:5 - (ae-forgotten-export) The symbol "SyncFASTElementRenderer" needs to be exported by the entry point exports.d.ts +// dist/dts/exports.d.ts:40:5 - (ae-incompatible-release-tags) The symbol "templateRenderer" is marked as @public, but its signature references "TemplateRenderer" which is marked as @beta +// dist/dts/exports.d.ts:41:5 - (ae-incompatible-release-tags) The symbol "ElementRenderer" is marked as @public, but its signature references "ConstructableFASTElementRenderer" which is marked as @beta +// dist/dts/exports.d.ts:51:5 - (ae-forgotten-export) The symbol "AsyncFASTElementRenderer" needs to be exported by the entry point exports.d.ts // dist/dts/request-storage.d.ts:32:5 - (ae-forgotten-export) The symbol "getItem" needs to be exported by the entry point exports.d.ts // (No @packageDocumentation comment for this package) diff --git a/packages/web-components/fast-ssr/src/configure-fast-element.ts b/packages/web-components/fast-ssr/src/configure-fast-element.ts index 9d72177b679..dc2455990c7 100644 --- a/packages/web-components/fast-ssr/src/configure-fast-element.ts +++ b/packages/web-components/fast-ssr/src/configure-fast-element.ts @@ -1,6 +1,5 @@ import { Compiler, - ElementController, ElementStyles, Updates, ViewBehaviorFactory, diff --git a/packages/web-components/fast-ssr/src/element-renderer/elemenent-renderer.spec.ts b/packages/web-components/fast-ssr/src/element-renderer/elemenent-renderer.spec.ts index eb184a8648e..fd66b8e399c 100644 --- a/packages/web-components/fast-ssr/src/element-renderer/elemenent-renderer.spec.ts +++ b/packages/web-components/fast-ssr/src/element-renderer/elemenent-renderer.spec.ts @@ -190,7 +190,7 @@ test.describe("FASTElementRenderer", () => { const { templateRenderer } = fastSSR(); const result = consolidate(templateRenderer.render(html``)); - expect(result).toBe(``) + expect(result).toBe("") }); test("Should bubble events to the document", () => { document.addEventListener("test-event", (e) => { @@ -199,6 +199,7 @@ test.describe("FASTElementRenderer", () => { const { templateRenderer } = fastSSR(); const result = consolidate(templateRenderer.render(html``)); + expect(result).toBe(``); }); test("Should bubble events to the window", () => { diff --git a/packages/web-components/fast-ssr/src/element-renderer/fast-element-renderer.ts b/packages/web-components/fast-ssr/src/element-renderer/fast-element-renderer.ts index 9656b17ceef..b1644e494ff 100644 --- a/packages/web-components/fast-ssr/src/element-renderer/fast-element-renderer.ts +++ b/packages/web-components/fast-ssr/src/element-renderer/fast-element-renderer.ts @@ -20,6 +20,12 @@ abstract class FASTElementRenderer extends DefaultElementRenderer { */ public readonly element!: FASTElement; + /** + * When true, instructs the ElementRenderer to yield the `defer-hydration` attribute for + * rendered elements. + */ + protected abstract deferHydration: boolean; + /** * The template renderer to use when rendering a component template */ @@ -129,19 +135,8 @@ export abstract class AsyncFASTElementRenderer extends FASTElementRenderer if (this.awaiting.size) { yield this.pauseRendering().then(() => ""); } - const { attributes } = this.element; - - for ( - let i = 0, name, value; - i < attributes.length && ({ name, value } = attributes[i]); - i++ - ) { - if (value === "" || value === undefined || value === null) { - yield ` ${name}`; - } else { - yield ` ${name}="${escapeHtml(value)}"`; - } - } + + yield* renderAttributesSync.call(this); } } renderShadow = renderShadow as ( @@ -184,6 +179,10 @@ function* renderAttributesSync(this: FASTElementRenderer): IterableIterator { @@ -8,4 +11,26 @@ test.describe("fastSSR default export", () => { const { templateRenderer, ElementRenderer } = fastSSR(); expect(templateRenderer.createRenderInfo().elementRenderers.includes(ElementRenderer)).toBe(true) }) + + test("should render FAST elements without the `defer-hydration` attribute by default", () => { + const { templateRenderer } = fastSSR(); + const name = uniqueElementName(); + FASTElement.define(name); + + expect(consolidate(templateRenderer.render(`<${name}>`))).toBe(`<${name}>`) + }); + test("should render FAST elements with the `defer-hydration` attribute when deferHydration is configured to be true", () => { + const { templateRenderer } = fastSSR({deferHydration: true}); + const name = uniqueElementName(); + FASTElement.define(name); + + expect(consolidate(templateRenderer.render(`<${name}>`))).toBe(`<${name} defer-hydration>`) + }); + test("should not render FAST elements with the `defer-hydration` attribute when deferHydration is configured to be false", () => { + const { templateRenderer } = fastSSR({deferHydration: false}); + const name = uniqueElementName(); + FASTElement.define(name); + + expect(consolidate(templateRenderer.render(`<${name}>`))).toBe(`<${name}>`) + }); }); diff --git a/packages/web-components/fast-ssr/src/exports.ts b/packages/web-components/fast-ssr/src/exports.ts index 15c0ee1dfa6..472482aabe4 100644 --- a/packages/web-components/fast-ssr/src/exports.ts +++ b/packages/web-components/fast-ssr/src/exports.ts @@ -1,3 +1,4 @@ +import { FASTElement, FASTElementDefinition } from "@microsoft/fast-element"; import { AsyncFASTElementRenderer, SyncFASTElementRenderer, @@ -24,14 +25,33 @@ import { // Perform necessary configuration of FAST-Element library // for rendering in NodeJS import "./configure-fast-element.js"; -import { FASTElement, FASTElementDefinition } from "@microsoft/fast-element"; /** * Configuration for SSR factory. * @beta */ export interface SSRConfiguration { - renderMode: "sync" | "async"; + /** + * When 'async', configures the renderer to support async rendering. + * 'async' rendering will yield 'string | Promise' + * + * Defaults to 'sync'. + */ + renderMode?: "sync" | "async"; + + /** + * Configures the renderer to yield the `defer-hydration` attribute during element rendering. + * The `defer-hydration` attribute can be used to prevent immediate hydration of the element + * by fast-element by importing hydration support in the client bundle. + * + * Defaults to `false` + * @example + * + * ```ts + * import "@microsoft/fast-element/install-element-hydration"; + * ``` + */ + deferHydration?: boolean; } /** @beta */ @@ -39,6 +59,12 @@ function fastSSR(): { templateRenderer: TemplateRenderer; ElementRenderer: ConstructableFASTElementRenderer; }; +function fastSSR( + config: Omit +): { + templateRenderer: TemplateRenderer; + ElementRenderer: ConstructableFASTElementRenderer; +}; /** @beta */ function fastSSR( config: SSRConfiguration & Record<"renderMode", "sync"> @@ -53,6 +79,7 @@ function fastSSR( templateRenderer: AsyncTemplateRenderer; ElementRenderer: ConstructableFASTElementRenderer; }; + /** * Factory for creating SSR rendering assets. * @example @@ -68,10 +95,12 @@ function fastSSR( * @beta */ function fastSSR(config?: SSRConfiguration): any { - const async = config && config.renderMode === "async"; + config = { renderMode: "sync", deferHydration: false, ...config } as Required< + SSRConfiguration + >; const templateRenderer = new DefaultTemplateRenderer(); - const elementRenderer = class extends (!async + const elementRenderer = class extends (config.renderMode !== "async" ? SyncFASTElementRenderer : AsyncFASTElementRenderer) { static #disabledConstructors = new Set(); @@ -103,6 +132,7 @@ function fastSSR(config?: SSRConfiguration): any { } protected templateRenderer: DefaultTemplateRenderer = templateRenderer; protected styleRenderer = new StyleElementStyleRenderer(); + protected deferHydration = config?.deferHydration; }; templateRenderer.withDefaultElementRenderers( diff --git a/packages/web-components/fast-ssr/src/template-renderer/template-renderer.spec.ts b/packages/web-components/fast-ssr/src/template-renderer/template-renderer.spec.ts index 93afcb95b03..5e3cae3a96e 100644 --- a/packages/web-components/fast-ssr/src/template-renderer/template-renderer.spec.ts +++ b/packages/web-components/fast-ssr/src/template-renderer/template-renderer.spec.ts @@ -112,7 +112,7 @@ test.describe("TemplateRenderer", () => { expect(consolidate(result)).toBe(""); }); - test("should a custom element with a static attribute", () => { + test("should render a custom element with a static attribute", () => { const { templateRenderer } = fastSSR(); const result = templateRenderer.render(html``)