@microsoft/fast-element 2.0.0-beta.9 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/.eslintrc.json +1 -1
  2. package/CHANGELOG.json +518 -0
  3. package/CHANGELOG.md +181 -1
  4. package/README.md +1 -9
  5. package/api-extractor.context.json +14 -0
  6. package/api-extractor.di.json +14 -0
  7. package/dist/context/context.api.json +1068 -0
  8. package/dist/di/di.api.json +4929 -0
  9. package/dist/dts/binding/binding.d.ts +49 -0
  10. package/dist/dts/binding/normalize.d.ts +9 -0
  11. package/dist/dts/binding/one-time.d.ts +11 -0
  12. package/dist/dts/binding/one-way.d.ts +20 -0
  13. package/dist/dts/{templating/binding-signal.d.ts → binding/signal.d.ts} +19 -4
  14. package/dist/dts/{templating/binding-two-way.d.ts → binding/two-way.d.ts} +9 -5
  15. package/dist/dts/components/attributes.d.ts +7 -1
  16. package/dist/dts/components/element-controller.d.ts +104 -8
  17. package/dist/dts/components/element-hydration.d.ts +2 -0
  18. package/dist/dts/components/fast-definitions.d.ts +6 -0
  19. package/dist/dts/components/hydration.d.ts +56 -0
  20. package/dist/dts/components/install-hydration.d.ts +1 -0
  21. package/dist/dts/context.d.ts +29 -15
  22. package/dist/dts/di/di.d.ts +0 -5
  23. package/dist/dts/dom-policy.d.ts +83 -0
  24. package/dist/dts/dom.d.ts +100 -0
  25. package/dist/dts/hydration/target-builder.d.ts +63 -0
  26. package/dist/dts/index.d.ts +33 -26
  27. package/dist/dts/index.rollup.d.ts +0 -1
  28. package/dist/dts/index.rollup.debug.d.ts +0 -1
  29. package/dist/dts/interfaces.d.ts +32 -82
  30. package/dist/dts/metadata.d.ts +6 -5
  31. package/dist/dts/observation/arrays.d.ts +1 -1
  32. package/dist/dts/observation/observable.bench.d.ts +18 -0
  33. package/dist/dts/observation/observable.d.ts +5 -5
  34. package/dist/dts/pending-task.d.ts +19 -7
  35. package/dist/dts/platform.d.ts +11 -2
  36. package/dist/dts/polyfills.d.ts +0 -8
  37. package/dist/dts/styles/css-binding-directive.d.ts +60 -0
  38. package/dist/dts/styles/css.d.ts +9 -7
  39. package/dist/dts/styles/element-styles.d.ts +1 -14
  40. package/dist/dts/styles/host.d.ts +2 -5
  41. package/dist/dts/styles/style-strategy.d.ts +42 -0
  42. package/dist/dts/templating/compiler.d.ts +11 -13
  43. package/dist/dts/templating/{binding.d.ts → html-binding-directive.d.ts} +21 -41
  44. package/dist/dts/templating/html-directive.d.ts +44 -140
  45. package/dist/dts/templating/install-hydratable-view-templates.d.ts +1 -0
  46. package/dist/dts/templating/node-observation.d.ts +11 -1
  47. package/dist/dts/templating/ref.d.ts +4 -0
  48. package/dist/dts/templating/render.bench.d.ts +3 -0
  49. package/dist/dts/templating/render.d.ts +49 -9
  50. package/dist/dts/templating/repeat-basic-reverse.bench.d.ts +3 -0
  51. package/dist/dts/templating/repeat-basic-shift.bench.d.ts +3 -0
  52. package/dist/dts/templating/repeat.d.ts +31 -9
  53. package/dist/dts/templating/template.d.ts +97 -12
  54. package/dist/dts/templating/view.d.ts +146 -29
  55. package/dist/dts/templating/when-basic.bench.d.ts +3 -0
  56. package/dist/dts/templating/when-conditional.bench.d.ts +3 -0
  57. package/dist/dts/templating/when-switch.bench.d.ts +3 -0
  58. package/dist/dts/templating/when.d.ts +3 -1
  59. package/dist/dts/testing/fakes.d.ts +12 -1
  60. package/dist/dts/tsdoc-metadata.json +1 -1
  61. package/dist/dts/utilities.d.ts +55 -1
  62. package/dist/esm/binding/binding.js +18 -0
  63. package/dist/esm/binding/normalize.js +17 -0
  64. package/dist/esm/binding/one-time.js +21 -0
  65. package/dist/esm/binding/one-way.js +30 -0
  66. package/dist/esm/{templating/binding-signal.js → binding/signal.js} +22 -6
  67. package/dist/esm/{templating/binding-two-way.js → binding/two-way.js} +18 -12
  68. package/dist/esm/components/attributes.js +19 -6
  69. package/dist/esm/components/element-controller.js +319 -49
  70. package/dist/esm/components/element-hydration.js +2 -0
  71. package/dist/esm/components/fast-definitions.js +12 -4
  72. package/dist/esm/components/fast-element.js +3 -1
  73. package/dist/esm/components/hydration.js +104 -0
  74. package/dist/esm/components/install-hydration.js +3 -0
  75. package/dist/esm/context.js +26 -4
  76. package/dist/esm/debug.js +8 -2
  77. package/dist/esm/di/di.js +9 -12
  78. package/dist/esm/dom-policy.js +345 -0
  79. package/dist/esm/dom.js +101 -0
  80. package/dist/esm/hydration/target-builder.js +175 -0
  81. package/dist/esm/index.js +34 -25
  82. package/dist/esm/index.rollup.debug.js +3 -1
  83. package/dist/esm/index.rollup.js +3 -1
  84. package/dist/esm/interfaces.js +51 -3
  85. package/dist/esm/metadata.js +11 -8
  86. package/dist/esm/observation/arrays.js +1 -1
  87. package/dist/esm/observation/observable.bench.js +79 -0
  88. package/dist/esm/observation/observable.js +20 -15
  89. package/dist/esm/observation/update-queue.js +2 -2
  90. package/dist/esm/pending-task.js +13 -1
  91. package/dist/esm/platform.js +12 -2
  92. package/dist/esm/polyfills.js +3 -61
  93. package/dist/esm/styles/css-binding-directive.js +76 -0
  94. package/dist/esm/styles/css.js +14 -7
  95. package/dist/esm/styles/element-styles.js +0 -33
  96. package/dist/esm/styles/style-strategy.js +1 -0
  97. package/dist/esm/templating/children.js +8 -4
  98. package/dist/esm/templating/compiler.js +37 -44
  99. package/dist/esm/templating/html-binding-directive.js +218 -0
  100. package/dist/esm/templating/html-directive.js +25 -152
  101. package/dist/esm/templating/install-hydratable-view-templates.js +17 -0
  102. package/dist/esm/templating/node-observation.js +14 -8
  103. package/dist/esm/templating/ref.js +1 -1
  104. package/dist/esm/templating/render.bench.js +56 -0
  105. package/dist/esm/templating/render.js +74 -30
  106. package/dist/esm/templating/repeat-basic-reverse.bench.js +43 -0
  107. package/dist/esm/templating/repeat-basic-shift.bench.js +43 -0
  108. package/dist/esm/templating/repeat.js +116 -17
  109. package/dist/esm/templating/template.js +135 -60
  110. package/dist/esm/templating/view.js +254 -34
  111. package/dist/esm/templating/when-basic.bench.js +36 -0
  112. package/dist/esm/templating/when-conditional.bench.js +39 -0
  113. package/dist/esm/templating/when-switch.bench.js +68 -0
  114. package/dist/esm/templating/when.js +12 -5
  115. package/dist/esm/testing/fakes.js +32 -1
  116. package/dist/esm/testing/fixture.js +1 -1
  117. package/dist/esm/utilities.js +97 -1
  118. package/dist/fast-element.api.json +9789 -5667
  119. package/dist/fast-element.d.ts +813 -2392
  120. package/dist/fast-element.debug.js +2788 -974
  121. package/dist/fast-element.debug.min.js +3 -1
  122. package/dist/fast-element.js +2641 -833
  123. package/dist/fast-element.min.js +3 -1
  124. package/dist/fast-element.untrimmed.d.ts +662 -314
  125. package/docs/{api-report.md → api-report.api.md} +238 -151
  126. package/docs/context/api-report.api.md +69 -0
  127. package/docs/di/api-report.api.md +315 -0
  128. package/karma.conf.cjs +2 -1
  129. package/package.json +59 -47
  130. package/scripts/run-api-extractor.js +51 -0
  131. package/scripts/run-benchmarks.js +46 -0
  132. package/tensile.config.js +12 -0
  133. package/dist/dts/templating/dom.d.ts +0 -41
  134. package/dist/esm/templating/binding.js +0 -282
  135. package/dist/esm/templating/dom.js +0 -49
  136. package/docs/guide/declaring-templates.md +0 -230
  137. package/docs/guide/defining-elements.md +0 -214
  138. package/docs/guide/leveraging-css.md +0 -253
  139. package/docs/guide/next-steps.md +0 -13
  140. package/docs/guide/observables-and-state.md +0 -213
  141. package/docs/guide/using-directives.md +0 -576
  142. package/docs/guide/working-with-shadow-dom.md +0 -296
@@ -0,0 +1,46 @@
1
+ import fs from 'fs/promises';
2
+ import { fileURLToPath } from 'url';
3
+ import path from 'path';
4
+ import { execSync } from 'child_process';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+ const rootDir = path.join(__dirname, '..');
9
+ const tensileConfig = 'tensile.config.js';
10
+
11
+ try {
12
+ const esmOutput = path.join(rootDir, 'dist', 'esm');
13
+ const items = await fs.readdir(esmOutput);
14
+
15
+ // Collect all component folders
16
+ const folders = [];
17
+ for (const item of items) {
18
+ const itemPath = path.join(esmOutput, item);
19
+ const stats = await fs.lstat(itemPath);
20
+ if (stats.isDirectory()) {
21
+ folders.push(item);
22
+ }
23
+ }
24
+
25
+ // Collect all .bench.js files
26
+ const benchFiles = [];
27
+ for (const folder of folders) {
28
+ const folderPath = path.join(esmOutput, folder);
29
+ const files = await fs.readdir(folderPath);
30
+ const filteredFiles = files.filter(file => file.endsWith('.bench.js'));
31
+ benchFiles.push(...filteredFiles.map(file => path.relative(rootDir, path.join(folderPath, file))));
32
+ }
33
+
34
+ // Execute tensile for each .bench.js file
35
+ for (const file of benchFiles) {
36
+ try {
37
+ // eslint-disable-next-line no-undef
38
+ execSync(`tensile --file ./${file} --config ${tensileConfig} ${process.argv[2]}`, { stdio: 'inherit' });
39
+ } catch (error) {
40
+ console.error(`Error executing command for file ${file}: ${error.message}`);
41
+ }
42
+ }
43
+
44
+ } catch (error) {
45
+ console.error(`Error reading directory: ${error.message}`);
46
+ }
@@ -0,0 +1,12 @@
1
+ const config = {
2
+ // Browsers to test against
3
+ browsers: ['chrome'],
4
+
5
+ // Importmaps for your test.
6
+ // See: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap
7
+ imports: {
8
+ '@tensile-perf/web-components': '/node_modules/@tensile-perf/web-components/lib/index.js',
9
+ }
10
+ };
11
+
12
+ export default config;
@@ -1,41 +0,0 @@
1
- import type { Callable } from "../interfaces.js";
2
- /**
3
- * Common DOM APIs.
4
- * @public
5
- */
6
- export declare const DOM: Readonly<{
7
- /**
8
- * @deprecated
9
- * Use Updates.enqueue().
10
- */
11
- queueUpdate: (callable: Callable) => void;
12
- /**
13
- * @deprecated
14
- * Use Updates.next()
15
- */
16
- nextUpdate: () => Promise<void>;
17
- /**
18
- * @deprecated
19
- * Use Updates.process()
20
- */
21
- processUpdates: () => void;
22
- /**
23
- * Sets an attribute value on an element.
24
- * @param element - The element to set the attribute value on.
25
- * @param attributeName - The attribute name to set.
26
- * @param value - The value of the attribute to set.
27
- * @remarks
28
- * If the value is `null` or `undefined`, the attribute is removed, otherwise
29
- * it is set to the provided value using the standard `setAttribute` API.
30
- */
31
- setAttribute(element: HTMLElement, attributeName: string, value: any): void;
32
- /**
33
- * Sets a boolean attribute value.
34
- * @param element - The element to set the boolean attribute value on.
35
- * @param attributeName - The attribute name to set.
36
- * @param value - The value of the attribute to set.
37
- * @remarks
38
- * If the value is true, the attribute is added; otherwise it is removed.
39
- */
40
- setBooleanAttribute(element: HTMLElement, attributeName: string, value: boolean): void;
41
- }>;
@@ -1,282 +0,0 @@
1
- import { isFunction } from "../interfaces.js";
2
- import { ExecutionContext, Observable, } from "../observation/observable.js";
3
- import { FAST } from "../platform.js";
4
- import { DOM } from "./dom.js";
5
- import { Aspect, Binding, HTMLDirective, } from "./html-directive.js";
6
- import { Markup, nextId } from "./markup.js";
7
- const createInnerHTMLBinding = globalThis.TrustedHTML
8
- ? (binding) => (s, c) => {
9
- const value = binding(s, c);
10
- if (value instanceof TrustedHTML) {
11
- return value;
12
- }
13
- throw FAST.error(1202 /* Message.bindingInnerHTMLRequiresTrustedTypes */);
14
- }
15
- : (binding) => binding;
16
- class OnChangeBinding extends Binding {
17
- createObserver(_, subscriber) {
18
- return Observable.binding(this.evaluate, subscriber, this.isVolatile);
19
- }
20
- }
21
- class OneTimeBinding extends Binding {
22
- createObserver() {
23
- return this;
24
- }
25
- bind(controller) {
26
- return this.evaluate(controller.source, controller.context);
27
- }
28
- }
29
- function updateContent(target, aspect, value, controller) {
30
- // If there's no actual value, then this equates to the
31
- // empty string for the purposes of content bindings.
32
- if (value === null || value === undefined) {
33
- value = "";
34
- }
35
- // If the value has a "create" method, then it's a ContentTemplate.
36
- if (value.create) {
37
- target.textContent = "";
38
- let view = target.$fastView;
39
- // If there's no previous view that we might be able to
40
- // reuse then create a new view from the template.
41
- if (view === void 0) {
42
- view = value.create();
43
- }
44
- else {
45
- // If there is a previous view, but it wasn't created
46
- // from the same template as the new value, then we
47
- // need to remove the old view if it's still in the DOM
48
- // and create a new view from the template.
49
- if (target.$fastTemplate !== value) {
50
- if (view.isComposed) {
51
- view.remove();
52
- view.unbind();
53
- }
54
- view = value.create();
55
- }
56
- }
57
- // It's possible that the value is the same as the previous template
58
- // and that there's actually no need to compose it.
59
- if (!view.isComposed) {
60
- view.isComposed = true;
61
- view.bind(controller.source, controller.context);
62
- view.insertBefore(target);
63
- target.$fastView = view;
64
- target.$fastTemplate = value;
65
- }
66
- else if (view.needsBindOnly) {
67
- view.needsBindOnly = false;
68
- view.bind(controller.source, controller.context);
69
- }
70
- }
71
- else {
72
- const view = target.$fastView;
73
- // If there is a view and it's currently composed into
74
- // the DOM, then we need to remove it.
75
- if (view !== void 0 && view.isComposed) {
76
- view.isComposed = false;
77
- view.remove();
78
- if (view.needsBindOnly) {
79
- view.needsBindOnly = false;
80
- }
81
- else {
82
- view.unbind();
83
- }
84
- }
85
- target.textContent = value;
86
- }
87
- }
88
- function updateTokenList(target, aspect, value) {
89
- var _a;
90
- const lookup = `${this.id}-t`;
91
- const state = (_a = target[lookup]) !== null && _a !== void 0 ? _a : (target[lookup] = { c: 0, v: Object.create(null) });
92
- const versions = state.v;
93
- let currentVersion = state.c;
94
- const tokenList = target[aspect];
95
- // Add the classes, tracking the version at which they were added.
96
- if (value !== null && value !== undefined && value.length) {
97
- const names = value.split(/\s+/);
98
- for (let i = 0, ii = names.length; i < ii; ++i) {
99
- const currentName = names[i];
100
- if (currentName === "") {
101
- continue;
102
- }
103
- versions[currentName] = currentVersion;
104
- tokenList.add(currentName);
105
- }
106
- }
107
- state.v = currentVersion + 1;
108
- // If this is the first call to add classes, there's no need to remove old ones.
109
- if (currentVersion === 0) {
110
- return;
111
- }
112
- // Remove classes from the previous version.
113
- currentVersion -= 1;
114
- for (const name in versions) {
115
- if (versions[name] === currentVersion) {
116
- tokenList.remove(name);
117
- }
118
- }
119
- }
120
- const setProperty = (t, a, v) => (t[a] = v);
121
- const eventTarget = () => void 0;
122
- /**
123
- * A directive that applies bindings.
124
- * @public
125
- */
126
- export class HTMLBindingDirective {
127
- /**
128
- * Creates an instance of HTMLBindingDirective.
129
- * @param dataBinding - The binding configuration to apply.
130
- */
131
- constructor(dataBinding) {
132
- this.dataBinding = dataBinding;
133
- this.updateTarget = null;
134
- /**
135
- * The unique id of the factory.
136
- */
137
- this.id = nextId();
138
- /**
139
- * The type of aspect to target.
140
- */
141
- this.aspectType = Aspect.content;
142
- /** @internal */
143
- this.bind = this.bindDefault;
144
- this.data = `${this.id}-d`;
145
- }
146
- /**
147
- * Creates HTML to be used within a template.
148
- * @param add - Can be used to add behavior factories to a template.
149
- */
150
- createHTML(add) {
151
- return Markup.interpolation(add(this));
152
- }
153
- /**
154
- * Creates a behavior.
155
- */
156
- createBehavior() {
157
- if (this.updateTarget === null) {
158
- if (this.targetAspect === "innerHTML") {
159
- this.dataBinding.evaluate = createInnerHTMLBinding(this.dataBinding.evaluate);
160
- }
161
- switch (this.aspectType) {
162
- case 1:
163
- this.updateTarget = DOM.setAttribute;
164
- break;
165
- case 2:
166
- this.updateTarget = DOM.setBooleanAttribute;
167
- break;
168
- case 3:
169
- this.updateTarget = setProperty;
170
- break;
171
- case 4:
172
- this.bind = this.bindContent;
173
- this.updateTarget = updateContent;
174
- break;
175
- case 5:
176
- this.updateTarget = updateTokenList;
177
- break;
178
- case 6:
179
- this.bind = this.bindEvent;
180
- this.updateTarget = eventTarget;
181
- break;
182
- default:
183
- throw FAST.error(1205 /* Message.unsupportedBindingBehavior */);
184
- }
185
- }
186
- return this;
187
- }
188
- /** @internal */
189
- bindDefault(controller) {
190
- var _a;
191
- const target = controller.targets[this.nodeId];
192
- const observer = (_a = target[this.data]) !== null && _a !== void 0 ? _a : (target[this.data] = this.dataBinding.createObserver(this, this));
193
- observer.target = target;
194
- observer.controller = controller;
195
- this.updateTarget(target, this.targetAspect, observer.bind(controller), controller);
196
- if (this.updateTarget === updateContent) {
197
- controller.onUnbind(this);
198
- }
199
- }
200
- /** @internal */
201
- bindContent(controller) {
202
- this.bindDefault(controller);
203
- controller.onUnbind(this);
204
- }
205
- /** @internal */
206
- bindEvent(controller) {
207
- const target = controller.targets[this.nodeId];
208
- target[this.data] = controller;
209
- target.addEventListener(this.targetAspect, this, this.dataBinding.options);
210
- }
211
- /** @internal */
212
- unbind(controller) {
213
- const target = controller.targets[this.nodeId];
214
- const view = target.$fastView;
215
- if (view !== void 0 && view.isComposed) {
216
- view.unbind();
217
- view.needsBindOnly = true;
218
- }
219
- }
220
- /** @internal */
221
- handleEvent(event) {
222
- const target = event.currentTarget;
223
- ExecutionContext.setEvent(event);
224
- const controller = target[this.data];
225
- const result = this.dataBinding.evaluate(controller.source, controller.context);
226
- ExecutionContext.setEvent(null);
227
- if (result !== true) {
228
- event.preventDefault();
229
- }
230
- }
231
- /** @internal */
232
- handleChange(binding, observer) {
233
- const target = observer.target;
234
- const controller = observer.controller;
235
- this.updateTarget(target, this.targetAspect, observer.bind(controller), controller);
236
- }
237
- }
238
- HTMLDirective.define(HTMLBindingDirective, { aspected: true });
239
- /**
240
- * Creates an standard binding.
241
- * @param binding - The binding to refresh when changed.
242
- * @param isVolatile - Indicates whether the binding is volatile or not.
243
- * @returns A binding configuration.
244
- * @public
245
- */
246
- export function bind(binding, isVolatile = Observable.isVolatileBinding(binding)) {
247
- return new OnChangeBinding(binding, isVolatile);
248
- }
249
- /**
250
- * Creates a one time binding
251
- * @param binding - The binding to refresh when signaled.
252
- * @returns A binding configuration.
253
- * @public
254
- */
255
- export function oneTime(binding) {
256
- return new OneTimeBinding(binding);
257
- }
258
- /**
259
- * Creates an event listener binding.
260
- * @param binding - The binding to invoke when the event is raised.
261
- * @param options - Event listener options.
262
- * @returns A binding configuration.
263
- * @public
264
- */
265
- export function listener(binding, options) {
266
- const config = new OnChangeBinding(binding, false);
267
- config.options = options;
268
- return config;
269
- }
270
- /**
271
- * Normalizes the input value into a binding.
272
- * @param value - The value to create the default binding for.
273
- * @returns A binding configuration for the provided value.
274
- * @public
275
- */
276
- export function normalizeBinding(value) {
277
- return isFunction(value)
278
- ? bind(value)
279
- : value instanceof Binding
280
- ? value
281
- : oneTime(() => value);
282
- }
@@ -1,49 +0,0 @@
1
- import { Updates } from "../observation/update-queue.js";
2
- /**
3
- * Common DOM APIs.
4
- * @public
5
- */
6
- export const DOM = Object.freeze({
7
- /**
8
- * @deprecated
9
- * Use Updates.enqueue().
10
- */
11
- queueUpdate: Updates.enqueue,
12
- /**
13
- * @deprecated
14
- * Use Updates.next()
15
- */
16
- nextUpdate: Updates.next,
17
- /**
18
- * @deprecated
19
- * Use Updates.process()
20
- */
21
- processUpdates: Updates.process,
22
- /**
23
- * Sets an attribute value on an element.
24
- * @param element - The element to set the attribute value on.
25
- * @param attributeName - The attribute name to set.
26
- * @param value - The value of the attribute to set.
27
- * @remarks
28
- * If the value is `null` or `undefined`, the attribute is removed, otherwise
29
- * it is set to the provided value using the standard `setAttribute` API.
30
- */
31
- setAttribute(element, attributeName, value) {
32
- value === null || value === undefined
33
- ? element.removeAttribute(attributeName)
34
- : element.setAttribute(attributeName, value);
35
- },
36
- /**
37
- * Sets a boolean attribute value.
38
- * @param element - The element to set the boolean attribute value on.
39
- * @param attributeName - The attribute name to set.
40
- * @param value - The value of the attribute to set.
41
- * @remarks
42
- * If the value is true, the attribute is added; otherwise it is removed.
43
- */
44
- setBooleanAttribute(element, attributeName, value) {
45
- value
46
- ? element.setAttribute(attributeName, "")
47
- : element.removeAttribute(attributeName);
48
- },
49
- });
@@ -1,230 +0,0 @@
1
- ---
2
- id: declaring-templates
3
- title: Declaring Templates
4
- sidebar_label: Declaring Templates
5
- custom_edit_url: https://github.com/microsoft/fast/edit/master/packages/web-components/fast-element/docs/guide/declaring-templates.md
6
- description: While you can create and update nodes in the Shadow DOM manually, FASTElement provides a streamlined templating system for the most common rendering scenarios.
7
- ---
8
-
9
- ## Basic templates
10
-
11
- While you can create and update nodes in the Shadow DOM manually, `FASTElement` provides a streamlined templating system for the most common rendering scenarios. To create an HTML template for an element, import and use the `html` tagged template helper and pass the template to the `@customElement` decorator.
12
-
13
- Here's how we would add a template for our `name-tag` component that renders some basic structure as well as our `greeting`:
14
-
15
- **Example: Adding a Template to a `FASTElement`**
16
-
17
- ```ts
18
- import { FASTElement, customElement, attr, html } from '@microsoft/fast-element';
19
-
20
- const template = html<NameTag>`
21
- <div class="header">
22
- <h3>${x => x.greeting.toUpperCase()}</h3>
23
- <h4>my name is</h4>
24
- </div>
25
-
26
- <div class="body">TODO: Name Here</div>
27
-
28
- <div class="footer"></div>
29
- `;
30
-
31
- @customElement({
32
- name: 'name-tag',
33
- template
34
- })
35
- export class NameTag extends FASTElement {
36
- @attr greeting: string = 'Hello';
37
- }
38
- ```
39
-
40
- There are several important details in the above example, so let's break them down one-by-one.
41
-
42
- First, we create a template by using a [tagged template literal](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals). The tag, `html`, provides special processing for the HTML string that follows, returning an instance of `ViewTemplate`.
43
-
44
- Within a template, we provide *bindings* that declare the *dynamic parts* of our template. These bindings are declared with [arrow functions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions). Because the template is typed, the input to your arrow function will be an instance of the data model you declared in your `html` tag. When the `html` tag processes your template, it identifies these dynamic expressions and builds up an optimized model, capable of high-performance rendering, and efficient, incremental batched updates.
45
-
46
- Finally, we associate the template with our custom element by using a new form of the `@customElement` decorator, which allows us to pass more options. In this configuration, we pass an options object specifying the `name` and the `template`.
47
-
48
- With this in place, we now have a `name-tag` element that will render its template into the Shadow DOM and automatically update the `h3` content whenever the name tag's `greeting` attribute changes. Give it a try!
49
-
50
- ### Typed Templates
51
-
52
- Your templates can be *typed* to the data model that they are rendering over. In TypeScript, we provide the type as part of the tag: `html<NameTag>`. For TypeScript 3.8 or higher, you can import `ViewTemplate` as a type:
53
-
54
- ```ts
55
- import type { ViewTemplate } from '@microsoft/fast-element';
56
-
57
- const template: ViewTemplate<NameTag> = html`
58
- <div>${x => x.greeting}</div>
59
- `;
60
- ```
61
-
62
- ## Understanding bindings
63
-
64
- We've seen how arrow functions can be used to declare dynamic parts of templates. Let's look at a few more examples to see the breadth of what is available to you.
65
-
66
- ### Content
67
-
68
- To bind the content of an element, simply provide the expression within the start and end tags of the element. It can be the sole content of the element or interwoven with other elements and text.
69
-
70
- **Example: Basic Text Content**
71
-
72
- ```html
73
- <h3>${x => x.greeting.toUpperCase()}</h3>
74
- ```
75
-
76
- **Example: Interpolated Text Content**
77
-
78
- ```html
79
- <h3>${x => x.greeting}, my name is ${x => x.name}.</h3>
80
- ```
81
-
82
- **Example: Heterogeneous Content**
83
-
84
- ```html
85
- <h3>
86
- ${x => x.greeting}, my name is
87
- <span class="name">${x => x.name}</span>.
88
- </h3>
89
- ```
90
-
91
- :::note
92
- Dynamic content is set via the `textContent` HTML property for security reasons. You *cannot* set HTML content this way. See below for the explicit, opt-in mechanism for setting HTML.
93
- :::
94
-
95
- ### Attributes
96
-
97
- You can also use an expression to set an attribute value on an HTML Element. Simply place the expression where the value of the HTML attribute would go. The template engine will then use your expression to set the value using `setAttribute(...)`, whenever it needs to be updated. Additionally, some attributes are known as [boolean attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes#Boolean_Attributes) (e.g. required, readonly, disabled). These attributes behave differently from normal attributes and need special value handling. The templating engine will handle this for you if you prepend the attribute name with a `?`.
98
-
99
- **Example: Basic Attribute Values**
100
-
101
- ```html
102
- <a href="${x => x.aboutLink}">About</a>
103
- ```
104
-
105
- **Example: Interpolated Attribute Values**
106
-
107
- ```html
108
- <a href="products/${x => x.id}">
109
- ${x => x.name}
110
- </a>
111
- ```
112
-
113
- ```html
114
- <li class="list-item ${x => x.type}">
115
- ...
116
- </li>
117
- ```
118
-
119
- :::tip
120
- When binding to `class`, the underlying engine will not over-write classes added to the element via other mechanisms. It only adds and removes classes that result directly from the binding. This "safe by default" behavior does come at a slight performance cost. To opt-out of this feature and squeeze out every ounce of performance by always overwriting all classes, use a property binding (see below) on the `className` property. e.g. `:className="list-item ${x => x.type}"`.
121
- :::
122
-
123
- ```html
124
- <span style="text-decoration: ${x => x.done ? 'line-through' : ''}">
125
- ${x => x.description}
126
- </span>
127
- ```
128
-
129
- **Example: ARIA Attributes**
130
-
131
- ```html
132
- <div role="progressbar"
133
- aria-valuenow="${x => x.value}"
134
- aria-valuemin="${x => x.min}"
135
- aria-valuemax="${x => x.max}">
136
- </div>
137
- ```
138
-
139
- **Example: Boolean Attributes**
140
-
141
- ```html
142
- <button type="submit" ?disabled="${x => !x.enabled}">Submit</button>
143
- ```
144
-
145
- **Example: Nullish value**
146
-
147
- Some cases may occur where an attribute needs to have a value when used however not present if unused. These are different than boolean attributes by where their presence alone triggers their effect. In this situation, a nullish value (`null` or `undefined`) may be provided so the attribute won't exist in that condition.
148
-
149
- ```html
150
- <div aria-hidden="${x => x.isViewable ? 'true' : null}"></div>
151
- ```
152
-
153
- ### Properties
154
-
155
- Properties can also be set directly on an HTML element. To do so, prepend the property name with `:` to indicate a property binding. The template engine will then use your expression to assign the element's property value.
156
-
157
- **Example: Basic Property Values**
158
-
159
- ```html
160
- <my-element :myCustomProperty="${x => x.mySpecialData}">
161
- ...
162
- </my-element>
163
- ```
164
-
165
- **Example: Inner HTML**
166
-
167
- ```html
168
- <div :innerHTML="${x => x.someDangerousHTMLContent}"></div>
169
- ```
170
-
171
- :::warning
172
- Avoid scenarios that require you to directly set HTML, especially when the content is coming from an external source. If you must do this, you should always sanitize the HTML.
173
-
174
- The best way to accomplish HTML sanitization is to configure [a trusted types policy](https://w3c.github.io/webappsec-trusted-types/dist/spec/) with FASTElement's template engine. FASTElement ensures that all HTML strings pass through the configured policy. Also, by leveraging the platform's trusted types capabilities, you get native enforcement of the policy through CSP headers. Here's an example of how to configure a custom policy to sanitize HTML:
175
-
176
- ```ts
177
- import { DOM } from '@microsoft/fast-element';
178
-
179
- const myPolicy = trustedTypes.createPolicy('my-policy', {
180
- createHTML(html) {
181
- // TODO: invoke a sanitization library on the html before returning it
182
- return html;
183
- }
184
- });
185
-
186
- DOM.setHTMLPolicy(myPolicy);
187
- ```
188
-
189
- For security reasons, the HTML Policy can only be set once. For this reason, it should be set by application developers and not by component authors, and it should be done immediately during the startup sequence of the application.
190
- :::
191
-
192
- ### Events
193
-
194
- Besides rendering content, attributes, and properties, you'll often want to add event listeners and execute code when events fire. To do that, prepend the event name with `@` and provide the expression to be called when that event fires. Within an event binding, you also have access to a special *context* argument from which you can access the `event` object.
195
-
196
- **Example: Basic Events**
197
-
198
- ```html
199
- <button @click="${x => x.remove()}">Remove</button>
200
- ```
201
-
202
- **Example: Accessing Event Details**
203
-
204
- ```html
205
- <input type="text"
206
- :value="${x => x.description}"
207
- @input="${(x, c) => x.handleDescriptionChange(c.event)}">
208
- ```
209
-
210
- :::important
211
- In both examples above, after your event handler is executed, `preventDefault()` will be called on the event object by default. You can return `true` from your handler to opt-out of this behavior.
212
- :::
213
-
214
- The second example demonstrates an important characteristic of the templating engine: it only supports *unidirectional data flow* (model => view). It does not support *two-way data binding* (model <=> view). As shown above, pushing data from the view back to the model should be handled with explicit events that call into your model's API.
215
-
216
- ## Templating and the element lifecycle
217
-
218
- It is during the `connectedCallback` phase of the Custom Element lifecycle that `FASTElement` creates templates and binds the resulting view. The creation of the template only occurs the first time the element is connected, while binding happens every time the element is connected (with unbinding happening during the `disconnectedCallback` for symmetry).
219
-
220
- :::note
221
- In the future, we're planning new optimizations that will enable us to safely determine when we do not need to unbind/rebind certain views.
222
- :::
223
-
224
- In most cases, the template that `FASTElement` renders is determined by the `template` property of the Custom Element's configuration. However, you can also implement a method on your Custom Element class named `resolveTemplate()` that returns a template instance. If this method is present, it will be called during `connectedCallback` to obtain the template to use. This allows the element author to dynamically select completely different templates based on the state of the element at the time of connection.
225
-
226
- In addition to dynamic template selection during the `connectedCallback`, the `$fastController` property of `FASTElement` enables dynamically changing the template at any time by setting the controller's `template` property to any valid template.
227
-
228
- :::tip
229
- Check out [our Cheat Sheet](../resources/cheat-sheet.md#bindings) for a quick guide on bindings.
230
- :::