Skip to content

Commit

Permalink
feat: add defer-hydration to ssr renderer (#6492)
Browse files Browse the repository at this point in the history
* 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 <nicholasrice@users.noreply.github.com>
  • Loading branch information
2 people authored and janechu committed Jun 10, 2024
1 parent 92c1340 commit d5e91d1
Show file tree
Hide file tree
Showing 9 changed files with 123 additions and 24 deletions.
Original file line number Diff line number Diff line change
@@ -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"
}
28 changes: 28 additions & 0 deletions packages/web-components/fast-ssr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,34 @@ ElementRenderer.disable("my-element");
templateRenderer.render(html`<my-element></my-element>`);
```

### 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()`:

Expand Down
18 changes: 14 additions & 4 deletions packages/web-components/fast-ssr/docs/api-report.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,14 @@ function fastSSR(): {
ElementRenderer: ConstructableFASTElementRenderer<SyncFASTElementRenderer>;
};

// 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<SSRConfiguration, "renderMode">): {
templateRenderer: TemplateRenderer;
ElementRenderer: ConstructableFASTElementRenderer<SyncFASTElementRenderer>;
};

// @beta (undocumented)
function fastSSR(config: SSRConfiguration & Record<"renderMode", "sync">): {
templateRenderer: TemplateRenderer;
Expand Down Expand Up @@ -124,8 +132,8 @@ export const RequestStorageManager: Readonly<{

// @beta
export interface SSRConfiguration {
// (undocumented)
renderMode: "sync" | "async";
deferHydration?: boolean;
renderMode?: "sync" | "async";
}

// @beta
Expand Down Expand Up @@ -160,8 +168,10 @@ export interface ViewBehaviorFactoryRenderer<T extends ViewBehaviorFactory> {

// 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)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import {
Compiler,
ElementController,
ElementStyles,
Updates,
ViewBehaviorFactory,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ test.describe("FASTElementRenderer", () => {
const { templateRenderer } = fastSSR();

const result = consolidate(templateRenderer.render(html`<test-event-listener data="bubble-success"><test-event-dispatch></test-event-dispatch></test-event-listener>`));
expect(result).toBe(`<test-event-listener data=\"bubble-success\"><template shadowroot=\"open\"></template><test-event-dispatch event-detail=\"bubble-success\"><template shadowroot=\"open\"></template></test-event-dispatch></test-event-listener>`)
expect(result).toBe("<test-event-listener data=\"bubble-success\"><template shadowroot=\"open\"></template><test-event-dispatch event-detail=\"bubble-success\"><template shadowroot=\"open\"></template></test-event-dispatch></test-event-listener>")
});
test("Should bubble events to the document", () => {
document.addEventListener("test-event", (e) => {
Expand All @@ -199,6 +199,7 @@ test.describe("FASTElementRenderer", () => {
const { templateRenderer } = fastSSR();

const result = consolidate(templateRenderer.render(html`<test-event-dispatch></test-event-dispatch>`));

expect(result).toBe(`<test-event-dispatch event-detail=\"document-success\"><template shadowroot=\"open\"></template></test-event-dispatch>`);
});
test("Should bubble events to the window", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -184,6 +179,10 @@ function* renderAttributesSync(this: FASTElementRenderer): IterableIterator<stri
yield ` ${name}="${escapeHtml(value)}"`;
}
}

if (this.deferHydration) {
yield " defer-hydration";
}
}
}

Expand Down
25 changes: 25 additions & 0 deletions packages/web-components/fast-ssr/src/exports.spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,36 @@
import "./install-dom-shim.js";
import fastSSR from "./exports.js";
import { test, expect } from "@playwright/test";
import { uniqueElementName } from "@microsoft/fast-element/testing";
import { FASTElement } from "@microsoft/fast-element";
import { consolidate } from "./test-utilities/consolidate.js";


test.describe("fastSSR default export", () => {
test("should return a TemplateRenderer configured to create a RenderInfo object using the returned ElementRenderer", () => {
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}></${name}>`))).toBe(`<${name}><template shadowroot="open"></template></${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}></${name}>`))).toBe(`<${name} defer-hydration><template shadowroot="open"></template></${name}>`)
});
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}></${name}>`))).toBe(`<${name}><template shadowroot="open"></template></${name}>`)
});
});
38 changes: 34 additions & 4 deletions packages/web-components/fast-ssr/src/exports.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { FASTElement, FASTElementDefinition } from "@microsoft/fast-element";
import {
AsyncFASTElementRenderer,
SyncFASTElementRenderer,
Expand All @@ -24,21 +25,46 @@ 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<string>'
*
* 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 */
function fastSSR(): {
templateRenderer: TemplateRenderer;
ElementRenderer: ConstructableFASTElementRenderer<SyncFASTElementRenderer>;
};
function fastSSR(
config: Omit<SSRConfiguration, "renderMode">
): {
templateRenderer: TemplateRenderer;
ElementRenderer: ConstructableFASTElementRenderer<SyncFASTElementRenderer>;
};
/** @beta */
function fastSSR(
config: SSRConfiguration & Record<"renderMode", "sync">
Expand All @@ -53,6 +79,7 @@ function fastSSR(
templateRenderer: AsyncTemplateRenderer;
ElementRenderer: ConstructableFASTElementRenderer<AsyncFASTElementRenderer>;
};

/**
* Factory for creating SSR rendering assets.
* @example
Expand All @@ -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<typeof HTMLElement | string>();
Expand Down Expand Up @@ -103,6 +132,7 @@ function fastSSR(config?: SSRConfiguration): any {
}
protected templateRenderer: DefaultTemplateRenderer = templateRenderer;
protected styleRenderer = new StyleElementStyleRenderer();
protected deferHydration = config?.deferHydration;
};

templateRenderer.withDefaultElementRenderers(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ test.describe("TemplateRenderer", () => {

expect(consolidate(result)).toBe("<hello-world><template shadowroot=\"open\"></template></hello-world>");
});
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`<hello-world id="test"></hello-world>`)

Expand Down

0 comments on commit d5e91d1

Please sign in to comment.