Skip to content

Commit 9b13225

Browse files
authored
Add a RenderableFASTElement and default shadow options (#7126)
# Pull Request ## 📖 Description This change: - Requires that using `@microsoft/fast-html` rendered markup include hydratable markup. - Adds default shadowOptions so they do not need to be passed - Adds a `RenderableFASTElement` export which will automatically add and remove the `defer-hydration` attributes ## ✅ Checklist ### General <!--- Review the list and put an x in the boxes that apply. --> - [x] I have included a change request file using `$ npm run change` - [ ] I have added tests for my changes. - [x] I have tested my changes. - [x] I have updated the project documentation to reflect my changes. - [x] I have read the [CONTRIBUTING](https://github.com/microsoft/fast/blob/main/CONTRIBUTING.md) documentation and followed the [standards](https://github.com/microsoft/fast/blob/main/CODE_OF_CONDUCT.md#our-standards) for this project.
1 parent a31632f commit 9b13225

27 files changed

+267
-225
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "prerelease",
3+
"comment": "Add a RenderableFASTElement and default shadow options",
4+
"packageName": "@microsoft/fast-html",
5+
"email": "[email protected]",
6+
"dependentChangeType": "none"
7+
}

packages/web-components/fast-html/README.md

Lines changed: 46 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -30,20 +30,14 @@ MyCustomElement.define({
3030
shadowOptions: null,
3131
});
3232

33-
TemplateElement.options({
34-
"my-custom-element": {
35-
shadowOptions: {
36-
mode: "closed",
37-
}
38-
},
39-
}).define({
33+
TemplateElement.define({
4034
name: "f-template",
4135
});
4236
```
4337

4438
This will include the `<f-template>` custom element and all logic for interpreting the declarative HTML syntax for a FAST web component as well as the `shadowOptions` for any element an `<f-template>` has been used to define.
4539

46-
It is necessary to set the initial `shadowOptions` of your custom elements to `null` otherwise a shadowRoot will be attached and cause a FOUC (Flash Of Unstyled Content). For more information about how this affects hydration, check out our [document](./RENDERING.md#setting-shadow-options) on rendering DOM from non-browser.
40+
It is necessary to set the initial `shadowOptions` of your custom elements to `null` otherwise a shadowRoot will be attached and cause a FOUC (Flash Of Unstyled Content). For more information about how this affects hydration, check out our [document](./RENDERING.md#setting-shadow-options) on rendering DOM from a non-browser environment.
4741

4842
The template must be wrapped in `<f-template name="[custom-element-name]"><template>[template logic]</template></f-template>` with a `name` attribute for the custom elements name, and the template logic inside.
4943

@@ -59,6 +53,50 @@ Example:
5953
</f-template>
6054
```
6155

56+
#### Non-browser HTML rendering
57+
58+
One of the benefits of FAST declarative HTML templates is that the server can be stack agnostic as JavaScript does not need to be interpreted. By default `@microsoft/fast-html` will expect hydratable content and uses comments and datasets for tracking the binding logic. For more information on what that markup should look like, as well as an example of how initial state may be applied, read our [documentation](./RENDERING.md) to understand what markup should be generated for a hydratable experience. For the sake of brevity hydratable markup will be excluded from the README.
59+
60+
#### Adding shadowOptions
61+
62+
By default `shadowOptions` via the `TemplateElement` will be with `"mode": "open"` once the template has been set. To set each components `shadowOptions` you can pass an `options` object.
63+
64+
Example:
65+
66+
```typescript
67+
TemplateElement.options({
68+
"my-custom-element": {
69+
shadowOptions: {
70+
mode: "closed",
71+
}
72+
},
73+
}).define({
74+
name: "f-template",
75+
});
76+
```
77+
78+
#### Using the RenderableFASTElement
79+
80+
The exported abstract class `RenderableFASTElement` is available for automatic addition and removal of `defer-hydration` and `needs-hydration`. If you use `FASTElement` you will need to add `defer-hydration` and `needs-hydration` attributes to your rendered markup and remove them via your component.
81+
82+
Example:
83+
```typescript
84+
import { RenderableFASTElement, TemplateElement } from "@microsoft/fast-html";
85+
86+
class MyCustomElement extends RenderableFASTElement {
87+
// component logic
88+
}
89+
90+
MyCustomElement.define({
91+
name: "my-custom-element",
92+
shadowOptions: null,
93+
});
94+
95+
TemplateElement.define({
96+
name: "f-template",
97+
});
98+
```
99+
62100
### Syntax
63101

64102
All bindings use a handlebars-like syntax.
@@ -217,10 +255,6 @@ If your template includes JavaScript specific logic that does not conform to tho
217255
- `@microsoft/fast-html/rules/member-expression.yml`
218256
- `@microsoft/fast-html/rules/tag-function-to-template-literal.yml`
219257

220-
### Non-browser HTML rendering
221-
222-
One of the benefits of FAST declarative HTML templates is that the server can be stack agnostic as JavaScript does not need to be interpreted. FASTElement will expect hydratable content however and uses comments and datasets for tracking the binding logic. For more information on what that markup should look like, as well as an example of how initial state may be applied, read our [documentation](./RENDERING.md) to understand what markup should be generated for a hydratable experience.
223-
224258
## Acknowledgements
225259

226260
This project has been heavily inspired by [Handlebars](https://handlebarsjs.com/) and [Vue.js](https://vuejs.org/).

packages/web-components/fast-html/RENDERING.md

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -160,13 +160,7 @@ TemplateElement.options({
160160

161161
## Hydration Comments and Datasets
162162

163-
When hydrating the HTML FAST uses the `HydratableElementController` which can be included in your bundle like so:
164-
165-
```typescript
166-
import "@microsoft/fast-element/install-element-hydration.js";
167-
```
168-
169-
The `HydratableElementController` will take over from the `ElementController` to use hydration "markers" such as comments and dataset attributes to rationalize bindings with existing HTML.
163+
When hydrating the HTML, FAST uses the `HydratableElementController` which will take over from the `ElementController` to use hydration "markers" such as comments and dataset attributes to rationalize bindings with existing HTML. By default the `@microsoft/fast-html` package will assume that component hydration will occur so rendering hydratable markup is required.
170164

171165
### Bindings
172166

packages/web-components/fast-html/docs/api-report.api.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,25 @@
77
import { FASTElement } from '@microsoft/fast-element';
88
import { ShadowRootOptions } from '@microsoft/fast-element';
99

10+
// @public (undocumented)
11+
export abstract class RenderableFASTElement extends FASTElement {
12+
constructor();
13+
// (undocumented)
14+
deferHydration: boolean;
15+
// (undocumented)
16+
needsHydration: boolean;
17+
}
18+
1019
// @public
1120
export class TemplateElement extends FASTElement {
21+
constructor();
1222
// (undocumented)
1323
connectedCallback(): void;
14-
// Warning: (ae-forgotten-export) The symbol "ElementOptions" needs to be exported by the entry point index.d.ts
15-
static elementOptions: ElementOptions;
24+
// Warning: (ae-forgotten-export) The symbol "ElementOptionsDictionary" needs to be exported by the entry point index.d.ts
25+
static elementOptions: ElementOptionsDictionary;
1626
name?: string;
1727
// (undocumented)
18-
static options(elementOptions?: ElementOptions): typeof TemplateElement;
28+
static options(elementOptions?: ElementOptionsDictionary): typeof TemplateElement;
1929
}
2030

2131
// (No @packageDocumentation comment for this package)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { attr, FASTElement, Observable } from "@microsoft/fast-element";
2+
3+
export abstract class RenderableFASTElement extends FASTElement {
4+
@attr({ mode: "boolean", attribute: "defer-hydration" })
5+
deferHydration: boolean = true;
6+
7+
@attr({ mode: "boolean", attribute: "needs-hydration" })
8+
needsHydration: boolean = true;
9+
10+
constructor() {
11+
super();
12+
13+
this.setAttribute("defer-hydration", "");
14+
this.setAttribute("needs-hydration", "");
15+
16+
Observable.defineProperty(this.$fastController.definition, "shadowOptions");
17+
18+
Observable.getNotifier(this.$fastController.definition).subscribe(
19+
{
20+
handleChange: () => {
21+
this.deferHydration = false;
22+
},
23+
},
24+
"shadowOptions"
25+
);
26+
}
27+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export { TemplateElement } from "./template.js";
2+
export { RenderableFASTElement } from "./element.js";

packages/web-components/fast-html/src/components/template.ts

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ import {
66
FASTElement,
77
FASTElementDefinition,
88
fastElementRegistry,
9+
HydratableElementController,
910
ShadowRootOptions,
1011
ViewTemplate,
1112
} from "@microsoft/fast-element";
13+
import "@microsoft/fast-element/install-hydratable-view-templates.js";
1214
import { DOMPolicy } from "@microsoft/fast-element/dom-policy.js";
1315
import { Message } from "../interfaces.js";
1416
import {
@@ -40,13 +42,15 @@ function allow(
4042
};
4143
}
4244

45+
export interface ElementOptions {
46+
shadowOptions?: ShadowRootOptions | undefined;
47+
}
48+
4349
/**
4450
* A dictionary of element options the TemplateElement will use to update the registered element
4551
*/
46-
interface ElementOptions {
47-
[key: string]: {
48-
shadowOptions: ShadowRootOptions | undefined;
49-
};
52+
export interface ElementOptionsDictionary<ElementOptionsType = ElementOptions> {
53+
[key: string]: ElementOptionsType;
5054
}
5155

5256
/**
@@ -62,23 +66,64 @@ class TemplateElement extends FASTElement {
6266
/**
6367
* A dictionary of custom element options
6468
*/
65-
public static elementOptions: ElementOptions = {};
69+
public static elementOptions: ElementOptionsDictionary = {};
6670

6771
private partials: { [key: string]: ViewTemplate } = {};
6872

69-
public static options(elementOptions: ElementOptions = {}) {
70-
this.elementOptions = elementOptions;
73+
private static defaultElementOptions: ElementOptions = {
74+
shadowOptions: {
75+
mode: "open",
76+
},
77+
};
78+
79+
public static options(elementOptions: ElementOptionsDictionary = {}) {
80+
const result: ElementOptionsDictionary = {};
81+
82+
for (const key in elementOptions) {
83+
const value = elementOptions[key];
84+
result[key] = {
85+
shadowOptions:
86+
value.shadowOptions ??
87+
TemplateElement.defaultElementOptions.shadowOptions,
88+
};
89+
}
90+
91+
this.elementOptions = result;
92+
93+
HydratableElementController.install();
7194

7295
return this;
7396
}
7497

98+
constructor() {
99+
super();
100+
101+
if (!!TemplateElement.elementOptions) {
102+
TemplateElement.options();
103+
}
104+
}
105+
106+
/**
107+
* Set options for a custom element
108+
* @param name - The name of the custom element to set options for.
109+
*/
110+
private static setOptions(name: string): void {
111+
if (!!!TemplateElement.elementOptions[name]) {
112+
TemplateElement.elementOptions[name] = TemplateElement.defaultElementOptions;
113+
}
114+
}
115+
75116
connectedCallback(): void {
76117
super.connectedCallback();
77118

78119
if (this.name) {
79120
this.$fastController.definition.registry
80121
.whenDefined(this.name)
81122
.then(async value => {
123+
if (this.name && !!!TemplateElement.elementOptions?.[this.name]) {
124+
TemplateElement.setOptions(this.name);
125+
}
126+
82127
const registeredFastElement: FASTElementDefinition | undefined =
83128
fastElementRegistry.getByType(value);
84129
const template = this.getElementsByTagName("template").item(0);

packages/web-components/fast-html/src/fixtures/attribute/attribute.fixture.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
</head>
77
<body>
88
<test-element type="checkbox">
9-
<template shadowrootmode="open"><input type="checkbox" disabled></template>
9+
<template shadowrootmode="open">
10+
<input disabled data-fe-b-0 type="checkbox">
11+
</template>
1012
</test-element>
1113
<f-template name="test-element">
1214
<template><input type="{{type}}" disabled></template>
Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { TemplateElement } from "@microsoft/fast-html";
2-
import { attr, FASTElement } from "@microsoft/fast-element";
1+
import { RenderableFASTElement, TemplateElement } from "@microsoft/fast-html";
2+
import { attr } from "@microsoft/fast-element";
33

4-
class TestElement extends FASTElement {
4+
class TestElement extends RenderableFASTElement {
55
@attr
66
type: string = "radio";
77
}
@@ -10,12 +10,6 @@ TestElement.define({
1010
shadowOptions: null,
1111
});
1212

13-
TemplateElement.options({
14-
"test-element": {
15-
shadowOptions: {
16-
mode: "closed",
17-
},
18-
},
19-
}).define({
13+
TemplateElement.define({
2014
name: "f-template",
2115
});

packages/web-components/fast-html/src/fixtures/binding/binding.fixture.html

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,14 @@
66
</head>
77
<body>
88
<test-element text="Hello world">
9-
<template shadowrootmode="open">Hello world</template>
9+
<template shadowrootmode="open">
10+
<!--fe-b$$start$$0$$ZJEYduCZlM$$fe-b-->Hello world<!--fe-b$$end$$0$$ZJEYduCZlM$$fe-b-->
11+
</template>
1012
</test-element>
1113
<test-element-unescaped>
12-
<template shadowrootmode="open"><p>Hello world</p></template>
14+
<template shadowrootmode="open">
15+
<div><p><!--fe-b$$start$$0$$ZJEYduCZlM$$fe-b-->Hello world<!--fe-b$$end$$0$$ZJEYduCZlM$$fe-b--></p></div>
16+
</template>
1317
</test-element-unescaped>
1418
<f-template name="test-element">
1519
<template>{{text}}</template>

0 commit comments

Comments
 (0)