Skip to content

Commit

Permalink
exporting ViewBehaviorFactoryRenderer type
Browse files Browse the repository at this point in the history
  • Loading branch information
nicholasrice committed May 12, 2022
1 parent fa25b67 commit a05444c
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 44 deletions.
99 changes: 77 additions & 22 deletions packages/web-components/fast-ssr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,20 @@
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![npm version](https://badge.fury.io/js/%40microsoft%2Ffast-ssr.svg)](https://badge.fury.io/js/%40microsoft%2Ffast-ssr)

The `@microsoft/fast-ssr` package contains a NodeJS solution for emitting templates and components built using FAST to HTML markup strings.
The `@microsoft/fast-ssr` package contains a NodeJS solution for rendering FAST templates and components. While primarily intended for supporting SSR scenarios, it also allows FAST to be used as a general purpose HTML templating solution.

## Requirements
- [NodeJS ^16.0.0](https://nodejs.org)
- [`@microsoft/fast-element@^2.0.0`](https://www.npmjs.com/package/@microsoft/fast-element)

## Important Notes
`@microsoft/fast-ssr` is built using ES modules. This documentation assumes all JavaScript is run as ES module scripts. If that is not the case in your project, you can force ES module format by converting `.js` extensions to `.mjs`. [Click here for more information.](https://nodejs.org/api/packages.html)
`@microsoft/fast-ssr` is built using ES modules. This documentation assumes all JavaScript is run as ES module scripts. If that is not the case in your project, you can force ES module format by converting `.js` extensions to `.mjs`. [Details on this here.](https://nodejs.org/api/packages.html)

## How it Works
`@microsoft/fast-ssr` internally implements `ElementRenderer` interface proposed [here](https://github.com/webcomponents-cg/community-protocols/issues/7#issuecomment-825151215)
nd implemented in [`@lit-labs/ssr`](https://github.com/lit/lit/tree/main/packages/labs/ssr). The `ElementRenderer` interface allows Custom Elements created by different component authoring libraries to be rendered side-by-side, maximizing the portability and interoperability of Web Components.
### Design Philosophy
Performance is a core tenant of this package. To help achieve that, it avoids needing a comprehensive DOM implementation to render components, instead relying on a minimal set of DOM globals provided by the packaged DOM shim. Doing so comes with an important idea to understand; non-custom elements (`<div>`, `<p>`, etc) are never instantiated as JavaScript objects and you cannot gain references to these elements declared in a template. The parts of a template containing these elements are emitted as-is to the SSR string result (with any bindings evaluated). It is for this reason [certain directives are not no-ops](#directive-support-matrix). Additionally, components are rendered using only their template. Imperative code that adds elements or DOM content to a component will be invisible to the template renderer and will not be yielded in the final string result.

- Parses string templates to HTML, depth-first traversal, constructing custom element nodes it finds and yielding out shadow dom.
- Only custom elements are constructed, native elements are yielded directly as strings. This means directives such as `children`, `slotted`, and `ref` are no-ops.
- Elements need to exist in the template to be rendered. Idiomatic operations appending and removing elements will not result in SSR string changes
## Community Alignment
`@microsoft/fast-ssr` internally implements `ElementRenderer` interface proposed [here](https://github.com/webcomponents-cg/community-protocols/issues/7#issuecomment-825151215) and implemented in [`@lit-labs/ssr`](https://github.com/lit/lit/tree/main/packages/labs/ssr). The `ElementRenderer` interface allows Custom Elements created by different component authoring libraries to be rendered side-by-side, maximizing the portability and interoperability of Web Components. For more on how to do that, see the section on [configuring the RenderInfo object](#configuring-the-renderinfo-object).

## Installation
Install `@microsoft/fast-ssr` and `@microsoft/fast-element` using your package manager of choice:
Expand All @@ -37,7 +35,7 @@ import "@microsoft/fast-ssr/install-dom-shim";
Alternatively, a full DOM implementation such as [`jsdom`](https://github.com/jsdom/jsdom) or [`happy-dom`](https://github.com/capricorn86/happy-dom) can be used.

### Construct the Renderer
Import the renderer factory and construct a `TemplateRenderer`:
Import the renderer factory and construct a `TemplateRenderer`. You will also need a `RenderInfo` object and a default is provided, more on this [here](#configuring-the-renderinfo-object).
```js
import fastSSR from "@microsoft/fast-ssr";

Expand All @@ -60,25 +58,31 @@ class MyElement extends FASTElement {
With the `TemplateRenderer` created and a custom element defined, the `TemplateRenderer` can now render a template with that element. The result will be an `iterableIterator<string>`, which allows the result to be streamed to the client before the entire template has been rendered.

```js
const result templateRenderer.render(html`
<!DOCTYPE HTML>
<html>
<body>
<my-element></my-element>
</body>
</html>
const result = templateRenderer.render(html`
<!DOCTYPE HTML>
<html>
<body>
<my-element></my-element>
</body>
</html>
`, defaultRenderInfo);
```

The template being rendered can also be a fragment of HTML, it does not need to be a valid document:

```js
const result = templateRenderer.render(html`<my-element></my-element>`, defaultRenderInfo);
```

#### Rendering Strings
The template renderer can also render `string` types just as it would a template:

```js
const result templateRenderer.render("<!DOCTYPE HTML><html><body><my-element></my-element></body></html>", defaultRenderInfo);
const result = templateRenderer.render("<!DOCTYPE HTML><html><body><my-element></my-element></body></html>", defaultRenderInfo);
```

#### Rendering Templates with Bindings
A template can be rendered with arbitrary source data. When done so, bindings are invoked with that source data:
A template can be rendered with arbitrary source data by providing that source as the third argument to `.render()`. When done so, bindings are invoked with that source data:

```ts
const result = templateRenderer.render(html`
Expand All @@ -87,9 +91,60 @@ const result = templateRenderer.render(html`
```

#### Rendering Templates with Directives
The `TemplateRenderer` can render templates with `@microsoft/fast-element` directives such as `when` and `repeat`.
The `TemplateRenderer` can render templates with directives such as `when` and `repeat`. Some directives are no-ops, see the [directive support matrix](#directive-support-matrix) for details.

```ts
import { when, repeat } from "@microsoft/fast-element";

const result = templateRenderer.render(html`
${when(x => x.shouldRender, html`
<h1>Colors of a pixel</h1>
${repeat(x => x.data, html`<li>${color => color}</li>`)}
`)}
`, defaultRenderInfo, { shouldRender: true, data: ["red", "green", "blue"] });
```

##### Directive Support Matrix

|Directive Name|Support|
|-|-|
|[`children`](https://www.fast.design/docs/fast-element/using-directives#the-children-directive)|[![](https://img.shields.io/badge/-Unsupported-red)]()|
|[`ref`](https://www.fast.design/docs/fast-element/using-directives#the-ref-directive)|[![](https://img.shields.io/badge/-Unsupported-red)]()|
|[`repeat`](https://www.fast.design/docs/fast-element/using-directives#the-repeat-directive)|[![](https://img.shields.io/badge/-Supported-brightgreen)]()|
|[`slotted`](https://www.fast.design/docs/fast-element/using-directives#the-slotted-directive)|[![](https://img.shields.io/badge/-Unsupported-red)]()|
|[`when`](https://www.fast.design/docs/fast-element/using-directives#the-when-directive)|[![](https://img.shields.io/badge/-Supported-brightgreen)]()|

## Testing
This package uses Playwright and a lightweight web server for running tests. You can run the tests by running `npm run test`.
> Unsupported directives are no-ops. To understand more about why, see the [Design Philosophy.](#design-philosophy)
> Playwright may prompt you to install browsers to run the tests. If so, follow the instructions provided or run `npm run install-playwright-browsers`.
### Configuring the RenderInfo Object
`TemplateRenderer.render()` must be invoked with a `RenderInfo` object. It's 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. A pre-generated object is created for you by the factory function, but you can also easily construct your own:

```js
const { templateRenderer, elementRenderer } = fastSSR();
templateRenderer.render(html`<some-fast-element></some-fast-element>`, {
elementRenderers: [elementRenderer],
customElementHostStack: [],
customElementInstanceStack: [],
});
```

To render elements built with another library, that library will need to provide an `ElementRenderer` for the library. For example, to render a Lit element along-side a FAST element, provide the FAST `ElementRenderer` *and* the Lit `ElementRenderer` to the `RenderInfo` object:

```js
import fastSSR from "@microsoft/fast-ssr";
import { html } from "@microsoft/fast-element";
import { LitElementRenderer } from "@lit-labs/ssr/lib/lit-element-renderer.js"

const { templateRenderer, elementRenderer } = fastSSR();
// Some implementation that defines both FAST and Lit components
defineComponents();

const result = templateRenderer.render(html`
<my-fast-element></my-fast-element>
<my-lit-element></my-lit-element>
`, {
elementRenderers: [elementRenderer, LitElementRenderer],
customElementHostStack: [],
customElementInstanceStack: []
});
```
12 changes: 10 additions & 2 deletions packages/web-components/fast-ssr/docs/api-report.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,20 @@ export class TemplateRenderer {
//
// @internal
renderOpCodes(codes: Op[], renderInfo: RenderInfo, source: unknown, context: ExecutionContext): IterableIterator<string>;
// Warning: (ae-forgotten-export) The symbol "ViewBehaviorFactoryRenderer" needs to be exported by the entry point exports.d.ts
//
// @internal
withViewBehaviorFactoryRenderers(...renderers: ViewBehaviorFactoryRenderer<any>[]): void;
}

// @public
export interface ViewBehaviorFactoryRenderer<T extends ViewBehaviorFactory> {
// (undocumented)
matcher: Constructable<T>;
// Warning: (ae-incompatible-release-tags) The symbol "render" is marked as @public, but its signature references "TemplateRenderer" which is marked as @beta
//
// (undocumented)
render(behavior: T, renderInfo: RenderInfo, source: any, renderer: TemplateRenderer, context: ExecutionContext): IterableIterator<string>;
}

// (No @packageDocumentation comment for this package)

```
10 changes: 7 additions & 3 deletions packages/web-components/fast-ssr/src/exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import {
import { FASTElementRenderer } from "./element-renderer/element-renderer.js";
import { FASTSSRStyleStrategy } from "./element-renderer/style-strategy.js";
import { StyleElementStyleRenderer, StyleRenderer } from "./styles/style-renderer.js";
import { defaultViewBehaviorFactoryRenderers } from "./template-renderer/directives.js";
import {
defaultViewBehaviorFactoryRenderers,
ViewBehaviorFactoryRenderer,
} from "./template-renderer/directives.js";
import {
ComponentDOMEmissionMode,
TemplateRenderer,
Expand Down Expand Up @@ -74,8 +77,9 @@ export default function fastSSR(): {
}

export type {
TemplateRenderer,
ComponentDOMEmissionMode,
FASTElementRenderer,
StyleRenderer,
ComponentDOMEmissionMode,
TemplateRenderer,
ViewBehaviorFactoryRenderer,
};
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import {
import { TemplateRenderer } from "./template-renderer.js";

/**
* Describes an implementation that can render a directive
* Describes an implementation that can render a directive.
*
* @public
*/
export interface ViewBehaviorFactoryRenderer<T extends ViewBehaviorFactory> {
render(
Expand All @@ -26,8 +28,8 @@ export interface ViewBehaviorFactoryRenderer<T extends ViewBehaviorFactory> {
matcher: Constructable<T>;
}

export const RepeatDirectiveRenderer: ViewBehaviorFactoryRenderer<RepeatDirective> = Object.freeze(
{
export const RepeatDirectiveRenderer: ViewBehaviorFactoryRenderer<RepeatDirective> =
Object.freeze({
matcher: RepeatDirective,
*render(
directive: RepeatDirective,
Expand Down Expand Up @@ -64,31 +66,27 @@ export const RepeatDirectiveRenderer: ViewBehaviorFactoryRenderer<RepeatDirectiv
throw new Error("Unable to render Repeat Directive template");
}
},
}
);
});

function* noop() {
yield "";
}
export const ChildrenDirectiveRenderer: ViewBehaviorFactoryRenderer<ChildrenDirective> = Object.freeze(
{
export const ChildrenDirectiveRenderer: ViewBehaviorFactoryRenderer<ChildrenDirective> =
Object.freeze({
matcher: ChildrenDirective,
render: noop,
}
);
});

export const RefDirectiveRenderer: ViewBehaviorFactoryRenderer<RefDirective> = Object.freeze(
{
export const RefDirectiveRenderer: ViewBehaviorFactoryRenderer<RefDirective> =
Object.freeze({
matcher: RefDirective,
render: noop,
}
);
export const SlottedDirectiveRenderer: ViewBehaviorFactoryRenderer<SlottedDirective> = Object.freeze(
{
});
export const SlottedDirectiveRenderer: ViewBehaviorFactoryRenderer<SlottedDirective> =
Object.freeze({
matcher: SlottedDirective,
render: noop,
}
);
});

export const defaultViewBehaviorFactoryRenderers: ViewBehaviorFactoryRenderer<any>[] = [
RepeatDirectiveRenderer,
Expand Down

0 comments on commit a05444c

Please sign in to comment.