@microsoft/fast-html 1.0.0-alpha.7 → 1.0.0-alpha.9

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.
package/README.md CHANGED
@@ -23,13 +23,27 @@ In your JS bundle you will need to include the `@microsoft/fast-html` package:
23
23
 
24
24
  ```typescript
25
25
  import { TemplateElement } from "@microsoft/fast-html";
26
+ import { MyCustomElement } from "./my-custom-element";
26
27
 
27
- TemplateElement.define({
28
+ MyCustomElement.define({
29
+ name: "my-custom-element",
30
+ shadowOptions: null,
31
+ });
32
+
33
+ TemplateElement.options({
34
+ "my-custom-element": {
35
+ shadowOptions: {
36
+ mode: "closed",
37
+ }
38
+ },
39
+ }).define({
28
40
  name: "f-template",
29
41
  });
30
42
  ```
31
43
 
32
- This will include the `<f-template>` custom element and all logic for interpreting the declarative HTML syntax for a FAST web component.
44
+ 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.
45
+
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).
33
47
 
34
48
  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.
35
49
 
@@ -162,10 +176,47 @@ Where the right operand can be either a reference to a value (string e.g. `{{foo
162
176
  <f-apply partial="test" value="{{items}}"></f-apply>
163
177
  ```
164
178
 
179
+ #### Unescaped HTML
180
+
181
+ You can add unescaped HTML using triple braces, this will create an additional `div` element as the HTML needs an element to bind to. Where possible it is advisable to not use unescaped HTML and instead use other binding techniques as well as partials.
182
+
183
+ Example:
184
+ ```html
185
+ {{{html}}}
186
+ ```
187
+
165
188
  ### Writing Components
166
189
 
167
190
  When writing components with the intention of using the declarative HTML syntax, it is imperative that components are written with styling and rendering of the component to be less reliant on any JavaScript state management. An example of this is relying on `elementInterals` state to style a component.
168
191
 
192
+ ### Converting Components
193
+
194
+ FAST Components written using the `html` tag template literal can be partially converted via the supplied `.yml` rules made for use with [ast-grep](https://ast-grep.github.io/).
195
+
196
+ Example:
197
+
198
+ ```ts
199
+ // before
200
+ export const template = html`
201
+ <slot ${slotted("slottedNodes")}></slot>
202
+ `;
203
+ // after
204
+ export const template = `
205
+ <slot f-slotted="{slottedNodes}"></slot>
206
+ `;
207
+ ```
208
+
209
+ Which creates a starting point for converting the tag template literals to the declarative HTML syntax.
210
+
211
+ If your template includes JavaScript specific logic that does not conform to those rules, the fix may not be applied or may apply incorrectly. It is therefore suggested that complex logic instead leverages the custom elements JavaScript class.
212
+
213
+ #### Available Rules
214
+
215
+ - `@microsoft/fast-html/rules/attribute-directive.yml`
216
+ - `@microsoft/fast-html/rules/call-expression-with-event-argument.yml`
217
+ - `@microsoft/fast-html/rules/member-expression.yml`
218
+ - `@microsoft/fast-html/rules/tag-function-to-template-literal.yml`
219
+
169
220
  ## Acknowledgements
170
221
 
171
222
  This project has been heavily inspired by [Handlebars](https://handlebarsjs.com/) and [Vue.js](https://vuejs.org/).
@@ -1,4 +1,12 @@
1
- import { FASTElement } from "@microsoft/fast-element";
1
+ import { FASTElement, ShadowRootOptions } from "@microsoft/fast-element";
2
+ /**
3
+ * A dictionary of element options the TemplateElement will use to update the registered element
4
+ */
5
+ interface ElementOptions {
6
+ [key: string]: {
7
+ shadowOptions: ShadowRootOptions | undefined;
8
+ };
9
+ }
2
10
  /**
3
11
  * The <f-template> custom element that will provide view logic to the element
4
12
  */
@@ -7,7 +15,12 @@ declare class TemplateElement extends FASTElement {
7
15
  * The name of the custom element this template will be applied to
8
16
  */
9
17
  name?: string;
18
+ /**
19
+ * A dictionary of custom element options
20
+ */
21
+ static elementOptions: ElementOptions;
10
22
  private partials;
23
+ static options(elementOptions?: ElementOptions): typeof TemplateElement;
11
24
  connectedCallback(): void;
12
25
  /**
13
26
  * Resolve strings and values from an innerHTML string
@@ -101,4 +101,11 @@ interface OperatorConfig {
101
101
  * @returns Operator
102
102
  */
103
103
  export declare function getOperator(value: string): OperatorConfig;
104
+ /**
105
+ * This is the transform utility for rationalizing declarative HTML syntax
106
+ * with bindings in the ViewTemplate
107
+ * @param innerHTML The innerHTML to transform
108
+ * @param index The index to start the current slice of HTML to evaluate
109
+ */
110
+ export declare function transformInnerHTML(innerHTML: string, index?: number): string;
104
111
  export {};
@@ -1,7 +1,12 @@
1
1
  import { __awaiter, __decorate, __metadata } from "tslib";
2
- import { attr, FAST, FASTElement, fastElementRegistry, ViewTemplate, } from "@microsoft/fast-element";
2
+ import { attr, DOMAspect, FAST, FASTElement, fastElementRegistry, ViewTemplate, } from "@microsoft/fast-element";
3
3
  import { DOMPolicy } from "@microsoft/fast-element/dom-policy.js";
4
- import { getAllPartials, getNextBehavior, getOperator, pathResolver, } from "./utilities.js";
4
+ import { getAllPartials, getNextBehavior, getOperator, pathResolver, transformInnerHTML, } from "./utilities.js";
5
+ function allow(tagName, aspect, aspectName, sink) {
6
+ return (target, name, value, ...rest) => {
7
+ sink(target, name, value, ...rest);
8
+ };
9
+ }
5
10
  /**
6
11
  * The <f-template> custom element that will provide view logic to the element
7
12
  */
@@ -10,21 +15,30 @@ class TemplateElement extends FASTElement {
10
15
  super(...arguments);
11
16
  this.partials = {};
12
17
  }
18
+ static options(elementOptions = {}) {
19
+ this.elementOptions = elementOptions;
20
+ return this;
21
+ }
13
22
  connectedCallback() {
14
23
  super.connectedCallback();
15
24
  if (this.name) {
16
25
  this.$fastController.definition.registry
17
26
  .whenDefined(this.name)
18
27
  .then((value) => __awaiter(this, void 0, void 0, function* () {
28
+ var _a;
19
29
  const registeredFastElement = fastElementRegistry.getByType(value);
20
30
  const template = this.getElementsByTagName("template").item(0);
21
31
  if (template) {
22
- yield this.resolveAllPartials(this.innerHTML);
23
- const { strings, values } = yield this.resolveStringsAndValues(this.innerHTML);
32
+ const innerHTML = yield transformInnerHTML(this.innerHTML);
33
+ yield this.resolveAllPartials(innerHTML);
34
+ const { strings, values } = yield this.resolveStringsAndValues(innerHTML);
24
35
  if (registeredFastElement) {
25
36
  // all new elements will get the updated template
26
37
  registeredFastElement.template =
27
38
  this.resolveTemplateOrBehavior(strings, values);
39
+ // set shadow options as defined by the f-template
40
+ registeredFastElement.shadowOptions =
41
+ (_a = TemplateElement.elementOptions[this.name]) === null || _a === void 0 ? void 0 : _a.shadowOptions;
28
42
  }
29
43
  }
30
44
  else {
@@ -56,7 +70,15 @@ class TemplateElement extends FASTElement {
56
70
  * @param values - The interpreted values.
57
71
  */
58
72
  resolveTemplateOrBehavior(strings, values) {
59
- return ViewTemplate.create(strings, values, DOMPolicy.create());
73
+ return ViewTemplate.create(strings, values, DOMPolicy.create({
74
+ guards: {
75
+ aspects: {
76
+ [DOMAspect.property]: {
77
+ innerHTML: allow,
78
+ },
79
+ },
80
+ },
81
+ }));
60
82
  }
61
83
  /**
62
84
  * Resolve a template directive
@@ -280,6 +302,10 @@ class TemplateElement extends FASTElement {
280
302
  });
281
303
  }
282
304
  }
305
+ /**
306
+ * A dictionary of custom element options
307
+ */
308
+ TemplateElement.elementOptions = {};
283
309
  __decorate([
284
310
  attr,
285
311
  __metadata("design:type", String)
@@ -8,6 +8,10 @@ const openTagStart = "<f-";
8
8
  const tagEnd = ">";
9
9
  const closeTagStart = "</f-";
10
10
  const attributeDirectivePrefix = "f-";
11
+ const startInnerHTMLDiv = `<div :innerHTML="{{`;
12
+ const startInnerHTMLDivLength = startInnerHTMLDiv.length;
13
+ const endInnerHTMLDiv = `}}"></div>`;
14
+ const endInnerHTMLDivLength = endInnerHTMLDiv.length;
11
15
  /**
12
16
  * Get the index of the next matching tag
13
17
  * @param openingTagStartSlice - The slice starting from the opening tag
@@ -335,3 +339,32 @@ export function getOperator(value) {
335
339
  rightIsValue: null,
336
340
  };
337
341
  }
342
+ /**
343
+ * This is the transform utility for rationalizing declarative HTML syntax
344
+ * with bindings in the ViewTemplate
345
+ * @param innerHTML The innerHTML to transform
346
+ * @param index The index to start the current slice of HTML to evaluate
347
+ */
348
+ export function transformInnerHTML(innerHTML, index = 0) {
349
+ const sliceToEvaluate = innerHTML.slice(index);
350
+ const nextBinding = getNextBehavior(sliceToEvaluate);
351
+ let transformedInnerHTML = innerHTML;
352
+ if (nextBinding && nextBinding.type === "dataBinding") {
353
+ if (nextBinding.bindingType === "unescaped") {
354
+ transformedInnerHTML = `${innerHTML.slice(0, index)}${sliceToEvaluate.slice(0, nextBinding.openingStartIndex)}${startInnerHTMLDiv}${sliceToEvaluate.slice(nextBinding.openingStartIndex + 3, nextBinding.closingStartIndex)}${endInnerHTMLDiv}${sliceToEvaluate.slice(nextBinding.closingStartIndex + 3)}`;
355
+ return transformInnerHTML(transformedInnerHTML, index +
356
+ startInnerHTMLDivLength +
357
+ endInnerHTMLDivLength +
358
+ nextBinding.closingStartIndex -
359
+ 3);
360
+ }
361
+ else if (nextBinding.bindingType === "client") {
362
+ return transformInnerHTML(transformedInnerHTML, index + nextBinding.closingEndIndex);
363
+ }
364
+ return transformInnerHTML(transformedInnerHTML, index + nextBinding.closingStartIndex - 2);
365
+ }
366
+ else if (nextBinding) {
367
+ return transformInnerHTML(transformedInnerHTML, index + nextBinding.closingTagEndIndex);
368
+ }
369
+ return transformedInnerHTML;
370
+ }
@@ -1,6 +1,6 @@
1
1
  import { __awaiter } from "tslib";
2
2
  import { expect, test } from "@playwright/test";
3
- import { getNextBehavior, getAllPartials, getIndexOfNextMatchingTag, pathResolver } from "./utilities.js";
3
+ import { getNextBehavior, getAllPartials, getIndexOfNextMatchingTag, pathResolver, transformInnerHTML } from "./utilities.js";
4
4
  test.describe("utilities", () => __awaiter(void 0, void 0, void 0, function* () {
5
5
  test.describe("content", () => __awaiter(void 0, void 0, void 0, function* () {
6
6
  test("get the next content binding", () => __awaiter(void 0, void 0, void 0, function* () {
@@ -166,4 +166,21 @@ test.describe("utilities", () => __awaiter(void 0, void 0, void 0, function* ()
166
166
  expect(pathResolver("../foo")({}, { parent: { foo: "bar" } })).toEqual("bar");
167
167
  }));
168
168
  }));
169
+ test.describe("transformInnerHTML", () => __awaiter(void 0, void 0, void 0, function* () {
170
+ test("should resolve a single unescaped data binding", () => __awaiter(void 0, void 0, void 0, function* () {
171
+ expect(transformInnerHTML(`{{{html}}}`)).toEqual(`<div :innerHTML="{{html}}"></div>`);
172
+ }));
173
+ test("should resolve multiple unescaped data bindings", () => __awaiter(void 0, void 0, void 0, function* () {
174
+ expect(transformInnerHTML(`{{{foo}}}{{{bar}}}`)).toEqual(`<div :innerHTML="{{foo}}"></div><div :innerHTML="{{bar}}"></div>`);
175
+ }));
176
+ test("should resolve a unescaped data bindings in a mix of other data content bindings", () => __awaiter(void 0, void 0, void 0, function* () {
177
+ expect(transformInnerHTML(`{{text1}}{{{foo}}}{{text2}}{{{bar}}}{{text3}}`)).toEqual(`{{text1}}<div :innerHTML="{{foo}}"></div>{{text2}}<div :innerHTML="{{bar}}"></div>{{text3}}`);
178
+ }));
179
+ test("should resolve a unescaped data bindings in a mix of other data attribute bindings and nesting", () => __awaiter(void 0, void 0, void 0, function* () {
180
+ expect(transformInnerHTML(`<div data-foo="{{text1}}">{{{foo}}}</div><div data-bar="{{text2}}"></div>{{{bar}}}<div data-bat="{{text3}}"></div>`)).toEqual(`<div data-foo="{{text1}}"><div :innerHTML="{{foo}}"></div></div><div data-bar="{{text2}}"></div><div :innerHTML="{{bar}}"></div><div data-bat="{{text3}}"></div>`);
181
+ }));
182
+ test("should resolve a non-data and non-attribute bindings", () => __awaiter(void 0, void 0, void 0, function* () {
183
+ expect(transformInnerHTML(`<button @click="{handleNoArgsClick()}">No arguments</button>`)).toEqual(`<button @click="{handleNoArgsClick()}">No arguments</button>`);
184
+ }));
185
+ }));
169
186
  }));
@@ -13,7 +13,14 @@ __decorate([
13
13
  ], TestElement.prototype, "type", void 0);
14
14
  TestElement.define({
15
15
  name: "test-element",
16
+ shadowOptions: null,
16
17
  });
17
- TemplateElement.define({
18
+ TemplateElement.options({
19
+ "test-element": {
20
+ shadowOptions: {
21
+ mode: "closed",
22
+ },
23
+ },
24
+ }).define({
18
25
  name: "f-template",
19
26
  });
@@ -14,4 +14,10 @@ test.describe("f-template", () => __awaiter(void 0, void 0, void 0, function* ()
14
14
  yield expect(yield customElement.getAttribute("text")).toEqual("Hello pluto");
15
15
  yield expect(customElement).toHaveText("Hello pluto");
16
16
  }));
17
+ test("create an unescaped binding", ({ page }) => __awaiter(void 0, void 0, void 0, function* () {
18
+ yield page.goto("/binding");
19
+ const customElement = yield page.locator("test-element-unescaped");
20
+ yield expect(yield customElement.locator("p").count()).toEqual(1);
21
+ yield expect(customElement).toHaveText("Hello world");
22
+ }));
17
23
  }));
@@ -14,6 +14,27 @@ __decorate([
14
14
  TestElement.define({
15
15
  name: "test-element",
16
16
  });
17
- TemplateElement.define({
17
+ class TestElementUnescaped extends FASTElement {
18
+ constructor() {
19
+ super(...arguments);
20
+ this.html = `<p>Hello world</p>`;
21
+ }
22
+ }
23
+ TestElementUnescaped.define({
24
+ name: "test-element-unescaped",
25
+ shadowOptions: null,
26
+ });
27
+ TemplateElement.options({
28
+ "test-element": {
29
+ shadowOptions: {
30
+ mode: "closed",
31
+ },
32
+ },
33
+ "test-element-unescaped": {
34
+ shadowOptions: {
35
+ mode: "closed",
36
+ },
37
+ },
38
+ }).define({
18
39
  name: "f-template",
19
40
  });
@@ -18,7 +18,14 @@ __decorate([
18
18
  ], TestElement.prototype, "list", void 0);
19
19
  TestElement.define({
20
20
  name: "test-element",
21
+ shadowOptions: null,
21
22
  });
22
- TemplateElement.define({
23
+ TemplateElement.options({
24
+ "test-element": {
25
+ shadowOptions: {
26
+ mode: "closed",
27
+ },
28
+ },
29
+ }).define({
23
30
  name: "f-template",
24
31
  });
@@ -10,7 +10,14 @@ class TestElement extends FASTElement {
10
10
  }
11
11
  TestElement.define({
12
12
  name: "test-element",
13
+ shadowOptions: null,
13
14
  });
14
- TemplateElement.define({
15
+ TemplateElement.options({
16
+ "test-element": {
17
+ shadowOptions: {
18
+ mode: "closed",
19
+ },
20
+ },
21
+ }).define({
15
22
  name: "f-template",
16
23
  });
@@ -22,7 +22,14 @@ __decorate([
22
22
  ], TestElement.prototype, "foo", void 0);
23
23
  TestElement.define({
24
24
  name: "test-element",
25
+ shadowOptions: null,
25
26
  });
26
- TemplateElement.define({
27
+ TemplateElement.options({
28
+ "test-element": {
29
+ shadowOptions: {
30
+ mode: "closed",
31
+ },
32
+ },
33
+ }).define({
27
34
  name: "f-template",
28
35
  });
@@ -25,7 +25,14 @@ class TestElement extends FASTElement {
25
25
  }
26
26
  TestElement.define({
27
27
  name: "test-element",
28
+ shadowOptions: null,
28
29
  });
29
- TemplateElement.define({
30
+ TemplateElement.options({
31
+ "test-element": {
32
+ shadowOptions: {
33
+ mode: "closed",
34
+ },
35
+ },
36
+ }).define({
30
37
  name: "f-template",
31
38
  });
@@ -8,7 +8,14 @@ class TestElement extends FASTElement {
8
8
  }
9
9
  TestElement.define({
10
10
  name: "test-element",
11
+ shadowOptions: null,
11
12
  });
12
- TemplateElement.define({
13
+ TemplateElement.options({
14
+ "test-element": {
15
+ shadowOptions: {
16
+ mode: "closed",
17
+ },
18
+ },
19
+ }).define({
13
20
  name: "f-template",
14
21
  });
@@ -14,7 +14,14 @@ __decorate([
14
14
  ], TestElement.prototype, "list", void 0);
15
15
  TestElement.define({
16
16
  name: "test-element",
17
+ shadowOptions: null,
17
18
  });
18
- TemplateElement.define({
19
+ TemplateElement.options({
20
+ "test-element": {
21
+ shadowOptions: {
22
+ mode: "closed",
23
+ },
24
+ },
25
+ }).define({
19
26
  name: "f-template",
20
27
  });
@@ -16,7 +16,14 @@ __decorate([
16
16
  ], TestElement.prototype, "slottedNodes", void 0);
17
17
  TestElement.define({
18
18
  name: "test-element",
19
+ shadowOptions: null,
19
20
  });
20
- TemplateElement.define({
21
+ TemplateElement.options({
22
+ "test-element": {
23
+ shadowOptions: {
24
+ mode: "closed",
25
+ },
26
+ },
27
+ }).define({
21
28
  name: "f-template",
22
29
  });
@@ -140,7 +140,59 @@ __decorate([
140
140
  ], TestElementAnd.prototype, "thatVar", void 0);
141
141
  TestElementAnd.define({
142
142
  name: "test-element-and",
143
+ shadowOptions: null,
143
144
  });
144
- TemplateElement.define({
145
+ TemplateElement.options({
146
+ "test-element": {
147
+ shadowOptions: {
148
+ mode: "closed",
149
+ },
150
+ },
151
+ "test-element-not": {
152
+ shadowOptions: {
153
+ mode: "closed",
154
+ },
155
+ },
156
+ "test-element-equals": {
157
+ shadowOptions: {
158
+ mode: "closed",
159
+ },
160
+ },
161
+ "test-element-not-equals": {
162
+ shadowOptions: {
163
+ mode: "closed",
164
+ },
165
+ },
166
+ "test-element-ge": {
167
+ shadowOptions: {
168
+ mode: "closed",
169
+ },
170
+ },
171
+ "test-element-gt": {
172
+ shadowOptions: {
173
+ mode: "closed",
174
+ },
175
+ },
176
+ "test-element-le": {
177
+ shadowOptions: {
178
+ mode: "closed",
179
+ },
180
+ },
181
+ "test-element-lt": {
182
+ shadowOptions: {
183
+ mode: "closed",
184
+ },
185
+ },
186
+ "test-element-or": {
187
+ shadowOptions: {
188
+ mode: "closed",
189
+ },
190
+ },
191
+ "test-element-and": {
192
+ shadowOptions: {
193
+ mode: "closed",
194
+ },
195
+ },
196
+ }).define({
145
197
  name: "f-template",
146
198
  });