@microsoft/fast-html 1.0.0-alpha.40 → 1.0.0-alpha.42

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
@@ -313,13 +313,36 @@ Where the right operand can be either a reference to a value (string e.g. `{{foo
313
313
  <ul><f-repeat value="{{item in list}}"><li>{{item}}</li></f-repeat></ul>
314
314
  ```
315
315
 
316
- Should you need to refer to the parent element (not the individual item in the list), you can use the context of a previous repeat, or no context which will resolve to the custom element.
316
+ Bindings inside `<f-repeat>` without a context prefix resolve to the custom element. For example, `{{title}}` below resolves to the host element's `title` property:
317
317
 
318
- Example:
319
318
  ```html
320
319
  <ul><f-repeat value="{{item in list}}"><li>{{item}} - {{title}}</li></f-repeat></ul>
321
320
  ```
322
321
 
322
+ #### Execution Context Access
323
+
324
+ In imperative fast-element templates, every binding expression receives both the data source and the execution context: `${(x, c) => c.parent.handleClick(c.event)}`. Declarative `<f-template>` expressions can access the same execution context using the `$c` prefix.
325
+
326
+ This is particularly useful inside `<f-repeat>`, where `$c.parent` refers to the parent view-model (typically the host element) and `$c.event` provides the DOM event.
327
+
328
+ Event handler with context access:
329
+
330
+ ```html
331
+ <f-repeat value="{{item in items}}">
332
+ <button @click="{$c.parent.handleItemClick($c.event)}">{{item.name}}</button>
333
+ </f-repeat>
334
+ ```
335
+
336
+ Conditional rendering using a host property inside a repeat:
337
+
338
+ ```html
339
+ <f-repeat value="{{item in items}}">
340
+ <f-when value="{{$c.parent.showNames}}">
341
+ <span>{{item.name}}</span>
342
+ </f-when>
343
+ </f-repeat>
344
+ ```
345
+
323
346
  #### Unescaped HTML
324
347
 
325
348
  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.
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Default syntax for FAST declarative templates
3
+ */
4
+ export declare const attributeDirectivePrefix: string, clientSideCloseExpression: string, clientSideOpenExpression: string, closeExpression: string, executionContextAccessor: string, openExpression: string, repeatDirectiveClose: string, repeatDirectiveOpen: string, unescapedCloseExpression: string, unescapedOpenExpression: string, whenDirectiveClose: string, whenDirectiveOpen: string;
@@ -40,6 +40,7 @@ export interface ChildrenMap {
40
40
  customElementName: string;
41
41
  attributeName: string;
42
42
  }
43
+ export declare const contextPrefixDot: string;
43
44
  declare const LogicalOperator: {
44
45
  AND: string;
45
46
  OR: string;
@@ -115,13 +116,14 @@ export declare function getExpressionChain(value: string): ChainedExpression | v
115
116
  */
116
117
  export declare function transformInnerHTML(innerHTML: string, index?: number): string;
117
118
  /**
118
- * Resolves f-when
119
+ * Resolves boolean logic
120
+ * used for f-when and boolean attributes
119
121
  * @param self - Where the first item in the path path refers to the item itself (used by repeat).
120
122
  * @param chainedExpression - The chained expression which includes the expression and the next expression
121
123
  * if there is another in the chain
122
124
  * @returns - A binding that resolves the chained expression logic
123
125
  */
124
- export declare function resolveWhen(rootPropertyName: string | null, expression: ChainedExpression, parentContext: string | null, level: number, schema: Schema): (x: boolean, c: any) => any;
126
+ export declare function getBooleanBinding(rootPropertyName: string | null, expression: ChainedExpression, parentContext: string | null, level: number, schema: Schema): (x: boolean, c: any) => any;
125
127
  /**
126
128
  * Find a definition
127
129
  * This may exist as a $ref at the root or as a $ref in any anyOf or not at all
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Default syntax for FAST declarative templates
3
+ */
4
+ export const { attributeDirectivePrefix, clientSideCloseExpression, clientSideOpenExpression, closeExpression, executionContextAccessor, openExpression, repeatDirectiveClose, repeatDirectiveOpen, unescapedCloseExpression, unescapedOpenExpression, whenDirectiveClose, whenDirectiveOpen, } = {
5
+ attributeDirectivePrefix: "f-",
6
+ clientSideCloseExpression: "}",
7
+ clientSideOpenExpression: "{",
8
+ closeExpression: "}}",
9
+ executionContextAccessor: "$c",
10
+ openExpression: "{{",
11
+ unescapedCloseExpression: "}}}",
12
+ unescapedOpenExpression: "{{{",
13
+ repeatDirectiveClose: "</f-repeat>",
14
+ repeatDirectiveOpen: "<f-repeat",
15
+ whenDirectiveClose: "</f-when>",
16
+ whenDirectiveOpen: "<f-when",
17
+ };
@@ -3,7 +3,7 @@ import { attr, children, elements, FAST, FASTElement, FASTElementDefinition, fas
3
3
  import "@microsoft/fast-element/install-hydratable-view-templates.js";
4
4
  import { ObserverMap } from "./observer-map.js";
5
5
  import { Schema } from "./schema.js";
6
- import { bindingResolver, getExpressionChain, getNextBehavior, getRootPropertyName, resolveWhen, transformInnerHTML, } from "./utilities.js";
6
+ import { bindingResolver, contextPrefixDot, getBooleanBinding, getExpressionChain, getNextBehavior, getRootPropertyName, transformInnerHTML, } from "./utilities.js";
7
7
  /**
8
8
  * Values for the observerMap element option.
9
9
  */
@@ -69,24 +69,24 @@ class TemplateElement extends FASTElement {
69
69
  }
70
70
  this.schema = new Schema(name);
71
71
  FASTElementDefinition.registerAsync(name).then(async (value) => {
72
- var _a, _b, _c, _d, _e, _f, _g;
72
+ var _a, _b, _d, _e, _f, _g, _h;
73
73
  (_b = (_a = TemplateElement.lifecycleCallbacks).elementDidRegister) === null || _b === void 0 ? void 0 : _b.call(_a, name);
74
- if (!((_c = TemplateElement.elementOptions) === null || _c === void 0 ? void 0 : _c[name])) {
74
+ if (!((_d = TemplateElement.elementOptions) === null || _d === void 0 ? void 0 : _d[name])) {
75
75
  TemplateElement.setOptions(name);
76
76
  }
77
- if (((_d = TemplateElement.elementOptions[name]) === null || _d === void 0 ? void 0 : _d.observerMap) === "all") {
77
+ if (((_e = TemplateElement.elementOptions[name]) === null || _e === void 0 ? void 0 : _e.observerMap) === "all") {
78
78
  this.observerMap = new ObserverMap(value.prototype, this.schema);
79
79
  }
80
80
  const registeredFastElement = fastElementRegistry.getByType(value);
81
81
  const template = this.getElementsByTagName("template").item(0);
82
82
  if (template) {
83
83
  // Callback: Before template has been evaluated and assigned
84
- (_f = (_e = TemplateElement.lifecycleCallbacks).templateWillUpdate) === null || _f === void 0 ? void 0 : _f.call(_e, name);
84
+ (_g = (_f = TemplateElement.lifecycleCallbacks).templateWillUpdate) === null || _g === void 0 ? void 0 : _g.call(_f, name);
85
85
  const innerHTML = transformInnerHTML(this.innerHTML);
86
86
  // Cache paths during template processing (pass undefined if observerMap is not available)
87
87
  const { strings, values } = await this.resolveStringsAndValues(null, innerHTML, false, null, 0, this.schema, this.observerMap);
88
88
  // Define the root properties cached in the observer map as observable (only if observerMap exists)
89
- (_g = this.observerMap) === null || _g === void 0 ? void 0 : _g.defineProperties();
89
+ (_h = this.observerMap) === null || _h === void 0 ? void 0 : _h.defineProperties();
90
90
  if (registeredFastElement) {
91
91
  // Attach lifecycle callbacks to the definition before assigning template
92
92
  // This allows the Observable notification to trigger the callbacks
@@ -140,7 +140,7 @@ class TemplateElement extends FASTElement {
140
140
  switch (behaviorConfig.name) {
141
141
  case "when": {
142
142
  const expressionChain = getExpressionChain(behaviorConfig.value);
143
- const whenLogic = resolveWhen(rootPropertyName, expressionChain, parentContext, level, schema);
143
+ const whenLogic = getBooleanBinding(rootPropertyName, expressionChain, parentContext, level, schema);
144
144
  const { strings, values } = await this.resolveStringsAndValues(rootPropertyName, innerHTML.slice(behaviorConfig.openingTagEndIndex, behaviorConfig.closingTagStartIndex), self, parentContext, level, schema, observerMap);
145
145
  externalValues.push(when(whenLogic, this.resolveTemplateOrBehavior(strings, values)));
146
146
  break;
@@ -215,32 +215,57 @@ class TemplateElement extends FASTElement {
215
215
  }
216
216
  case "attribute": {
217
217
  strings.push(innerHTML.slice(0, behaviorConfig.openingStartIndex));
218
- if (behaviorConfig.aspect === "@") {
219
- const bindingHTML = innerHTML.slice(behaviorConfig.openingEndIndex, behaviorConfig.closingStartIndex);
220
- const openingParenthesis = bindingHTML.indexOf("(");
221
- const closingParenthesis = bindingHTML.indexOf(")");
222
- const propName = innerHTML.slice(behaviorConfig.openingEndIndex, behaviorConfig.closingStartIndex -
223
- (closingParenthesis - openingParenthesis) -
224
- 1);
225
- const type = "event";
226
- rootPropertyName = getRootPropertyName(rootPropertyName, propName, parentContext, type);
227
- const arg = bindingHTML.slice(openingParenthesis + 1, closingParenthesis);
228
- const binding = bindingResolver(strings.join(""), rootPropertyName, propName, parentContext, type, schema, parentContext, level);
229
- const attributeBinding = (x, c) => binding(x, c).bind(x)(...(arg === "e" ? [c.event] : []), ...(arg !== "e" && arg !== ""
230
- ? [
231
- bindingResolver(strings.join(""), rootPropertyName, arg, parentContext, type, schema, parentContext, level)(x, c),
232
- ]
233
- : []));
234
- values.push(attributeBinding);
235
- }
236
- else {
237
- const propName = innerHTML.slice(behaviorConfig.openingEndIndex, behaviorConfig.closingStartIndex);
238
- const type = "access";
239
- rootPropertyName = getRootPropertyName(rootPropertyName, propName, parentContext, type);
240
- const binding = bindingResolver(strings.join(""), rootPropertyName, propName, parentContext, type, schema, parentContext, level);
241
- const attributeBinding = (x, c) => binding(x, c);
242
- values.push(attributeBinding);
218
+ let attributeBinding;
219
+ switch (behaviorConfig.aspect) {
220
+ case "@": {
221
+ const bindingHTML = innerHTML.slice(behaviorConfig.openingEndIndex, behaviorConfig.closingStartIndex);
222
+ const openingParenthesis = bindingHTML.indexOf("(");
223
+ const closingParenthesis = bindingHTML.indexOf(")");
224
+ const propName = innerHTML.slice(behaviorConfig.openingEndIndex, behaviorConfig.closingStartIndex -
225
+ (closingParenthesis - openingParenthesis) -
226
+ 1);
227
+ const type = "event";
228
+ rootPropertyName = getRootPropertyName(rootPropertyName, propName, parentContext, type);
229
+ const arg = bindingHTML.slice(openingParenthesis + 1, closingParenthesis);
230
+ const binding = bindingResolver(strings.join(""), rootPropertyName, propName, parentContext, type, schema, parentContext, level);
231
+ const isContextPath = propName.startsWith(contextPrefixDot);
232
+ const getOwner = isContextPath
233
+ ? (_x, c) => {
234
+ const ownerPath = propName.split(".").slice(1, -1);
235
+ return ownerPath.reduce((prev, item) => prev === null || prev === void 0 ? void 0 : prev[item], c);
236
+ }
237
+ : (x, _c) => x;
238
+ attributeBinding = (x, c) => binding(x, c).bind(getOwner(x, c))(...(arg === "e" ? [c.event] : []), ...(arg !== "e" && arg !== ""
239
+ ? [
240
+ bindingResolver(strings.join(""), rootPropertyName, arg, parentContext, type, schema, parentContext, level)(x, c),
241
+ ]
242
+ : []));
243
+ break;
244
+ }
245
+ case "?": {
246
+ const expression = innerHTML.slice(behaviorConfig.openingEndIndex, behaviorConfig.closingStartIndex);
247
+ const expressionChain = getExpressionChain(expression);
248
+ if (expressionChain === null || expressionChain === void 0 ? void 0 : expressionChain.expression.operator) {
249
+ attributeBinding = getBooleanBinding(rootPropertyName, expressionChain, parentContext, level, schema);
250
+ }
251
+ else {
252
+ const propName = innerHTML.slice(behaviorConfig.openingEndIndex, behaviorConfig.closingStartIndex);
253
+ const type = "access";
254
+ rootPropertyName = getRootPropertyName(rootPropertyName, propName, parentContext, type);
255
+ const binding = bindingResolver(strings.join(""), rootPropertyName, propName, parentContext, type, schema, parentContext, level);
256
+ attributeBinding = (x, c) => binding(x, c);
257
+ }
258
+ break;
259
+ }
260
+ default: {
261
+ const propName = innerHTML.slice(behaviorConfig.openingEndIndex, behaviorConfig.closingStartIndex);
262
+ const type = "access";
263
+ rootPropertyName = getRootPropertyName(rootPropertyName, propName, parentContext, type);
264
+ const binding = bindingResolver(strings.join(""), rootPropertyName, propName, parentContext, type, schema, parentContext, level);
265
+ attributeBinding = (x, c) => binding(x, c);
266
+ }
243
267
  }
268
+ values.push(attributeBinding);
244
269
  await this.resolveInnerHTML(rootPropertyName, innerHTML.slice(behaviorConfig.closingEndIndex, innerHTML.length), strings, values, self, parentContext, level, schema, observerMap);
245
270
  break;
246
271
  }
@@ -1,15 +1,7 @@
1
1
  import { Observable } from "@microsoft/fast-element/observable.js";
2
2
  import { defsPropertyName, fastContextMetaData, refPropertyName, Schema, } from "./schema.js";
3
- const openClientSideBinding = "{";
4
- const closeClientSideBinding = "}";
5
- const openContentBinding = "{{";
6
- const closeContentBinding = "}}";
7
- const openUnescapedBinding = "{{{";
8
- const closeUnescapedBinding = "}}}";
9
- const openTagStart = "<f-";
10
- const tagEnd = ">";
11
- const closeTagStart = "</f-";
12
- const attributeDirectivePrefix = "f-";
3
+ import { attributeDirectivePrefix, clientSideCloseExpression, clientSideOpenExpression, closeExpression, executionContextAccessor, openExpression, repeatDirectiveClose, repeatDirectiveOpen, unescapedCloseExpression, unescapedOpenExpression, whenDirectiveClose, whenDirectiveOpen, } from "./syntax.js";
4
+ export const contextPrefixDot = `${executionContextAccessor}.`;
13
5
  const startInnerHTMLDiv = `<div :innerHTML="{{`;
14
6
  const startInnerHTMLDivLength = startInnerHTMLDiv.length;
15
7
  const endInnerHTMLDiv = `}}"></div>`;
@@ -90,16 +82,23 @@ export function getIndexOfNextMatchingTag(openingTagStartSlice, openingTag, clos
90
82
  * @returns DirectiveBehaviorConfig - A configuration object
91
83
  */
92
84
  function getNextDirectiveBehavior(innerHTML) {
93
- const openingTagStartIndex = innerHTML.indexOf(openTagStart);
85
+ const whenIndex = innerHTML.indexOf(whenDirectiveOpen);
86
+ const repeatIndex = innerHTML.indexOf(repeatDirectiveOpen);
87
+ const isWhen = whenIndex !== -1 && (repeatIndex === -1 || whenIndex < repeatIndex);
88
+ let openingTag = repeatDirectiveOpen;
89
+ let closingTag = repeatDirectiveClose;
90
+ let directiveTag = "repeat";
91
+ let openingTagStartIndex = repeatIndex;
92
+ if (isWhen) {
93
+ openingTag = whenDirectiveOpen;
94
+ closingTag = whenDirectiveClose;
95
+ directiveTag = "when";
96
+ openingTagStartIndex = whenIndex;
97
+ }
94
98
  const openingTagStartSlice = innerHTML.slice(openingTagStartIndex);
95
99
  const openingTagEndIndex = // account for f-when which may include >= or > as operators, but will always include a condition attr
96
- openingTagStartSlice.indexOf(`"${tagEnd}`) + openingTagStartIndex + 2;
97
- const directiveTag = innerHTML
98
- .slice(openingTagStartIndex + 3, openingTagEndIndex - 1)
99
- .split(" ")[0];
100
+ openingTagStartSlice.indexOf(`">`) + openingTagStartIndex + 2;
100
101
  const directiveValue = getNextDataBindingBehavior(innerHTML);
101
- const openingTag = `${openTagStart}${directiveTag}`;
102
- const closingTag = `${closeTagStart}${directiveTag}${tagEnd}`;
103
102
  const matchingCloseTagIndex = getIndexOfNextMatchingTag(openingTagStartSlice, openingTag, closingTag, openingTagStartIndex);
104
103
  return {
105
104
  type: "templateDirective",
@@ -185,14 +184,14 @@ function getContentDataBindingConfig(config) {
185
184
  */
186
185
  function getIndexAndBindingTypeOfNextDataBindingBehavior(innerHTML) {
187
186
  // {{{}}} binding
188
- const openingUnescapedStartIndex = innerHTML.indexOf(openUnescapedBinding);
189
- const closingUnescapedStartIndex = innerHTML.indexOf(closeUnescapedBinding);
187
+ const openingUnescapedStartIndex = innerHTML.indexOf(unescapedOpenExpression);
188
+ const closingUnescapedStartIndex = innerHTML.indexOf(unescapedCloseExpression);
190
189
  // {{}} binding
191
- const openingContentStartIndex = innerHTML.indexOf(openContentBinding);
192
- const closingContentStartIndex = innerHTML.indexOf(closeContentBinding);
190
+ const openingContentStartIndex = innerHTML.indexOf(openExpression);
191
+ const closingContentStartIndex = innerHTML.indexOf(closeExpression);
193
192
  // {} binding
194
- const openingClientStartIndex = innerHTML.indexOf(openClientSideBinding);
195
- const closingClientStartIndex = innerHTML.indexOf(closeClientSideBinding);
193
+ const openingClientStartIndex = innerHTML.indexOf(clientSideOpenExpression);
194
+ const closingClientStartIndex = innerHTML.indexOf(clientSideCloseExpression);
196
195
  if (openingUnescapedStartIndex !== -1 &&
197
196
  openingUnescapedStartIndex <= openingContentStartIndex &&
198
197
  openingUnescapedStartIndex <= openingClientStartIndex) {
@@ -251,8 +250,14 @@ export function getNextBehavior(innerHTML, offset = 0) {
251
250
  while (true) {
252
251
  const currentSlice = innerHTML.slice(offset);
253
252
  // client side binding will capture all bindings starting with "{"
254
- const dataBindingOpen = currentSlice.indexOf(openClientSideBinding);
255
- const directiveBindingOpen = currentSlice.indexOf(openTagStart);
253
+ const dataBindingOpen = currentSlice.indexOf(clientSideOpenExpression);
254
+ const whenDirectiveIndex = currentSlice.indexOf(whenDirectiveOpen);
255
+ const repeatDirectiveIndex = currentSlice.indexOf(repeatDirectiveOpen);
256
+ const directiveBindingOpen = whenDirectiveIndex === -1
257
+ ? repeatDirectiveIndex
258
+ : repeatDirectiveIndex === -1
259
+ ? whenDirectiveIndex
260
+ : Math.min(whenDirectiveIndex, repeatDirectiveIndex);
256
261
  const nextDataBindingBehavior = getNextDataBindingBehavior(currentSlice);
257
262
  if (dataBindingOpen === -1 && directiveBindingOpen === -1) {
258
263
  return null;
@@ -318,6 +323,13 @@ function isLegitimateClientSideBinding(result) {
318
323
  export function pathResolver(path, contextPath, level, rootSchema) {
319
324
  var _a, _b;
320
325
  let splitPath = path.split(".");
326
+ // Explicit context access via executionContextAccessor — resolve directly from ExecutionContext
327
+ if (splitPath[0] === executionContextAccessor) {
328
+ const contextAccessPath = splitPath.slice(1);
329
+ return (_accessibleObject, context) => {
330
+ return contextAccessPath.reduce((prev, item) => prev === null || prev === void 0 ? void 0 : prev[item], context);
331
+ };
332
+ }
321
333
  let levelCount = level;
322
334
  let self = splitPath[0] === contextPath;
323
335
  const parentContexts = [];
@@ -370,6 +382,13 @@ function pathWithContextResolver(splitPath, self) {
370
382
  };
371
383
  }
372
384
  export function bindingResolver(previousString, rootPropertyName, path, parentContext, type, schema, currentContext, level) {
385
+ // Explicit context access — resolve from ExecutionContext, skip schema tracking
386
+ if (path.startsWith(contextPrefixDot)) {
387
+ const segments = path.split(".").slice(1);
388
+ return (_x, context) => {
389
+ return segments.reduce((prev, item) => prev === null || prev === void 0 ? void 0 : prev[item], context);
390
+ };
391
+ }
373
392
  rootPropertyName = getRootPropertyName(rootPropertyName, path, currentContext, type);
374
393
  if (type !== "event" && rootPropertyName !== null) {
375
394
  const childrenMap = getChildrenMap(previousString);
@@ -391,6 +410,8 @@ export function expressionResolver(rootPropertyName, expression, parentContext,
391
410
  if (rootPropertyName !== null) {
392
411
  const paths = extractPathsFromChainedExpression(expression);
393
412
  paths.forEach(path => {
413
+ if (path.startsWith(contextPrefixDot))
414
+ return;
394
415
  schema.addPath({
395
416
  pathConfig: {
396
417
  type: "access",
@@ -443,7 +464,7 @@ function isOperandValue(operand) {
443
464
  isValue: true,
444
465
  };
445
466
  }
446
- catch (e) {
467
+ catch {
447
468
  return {
448
469
  value: operand,
449
470
  isValue: false,
@@ -586,9 +607,11 @@ function resolveExpression(x, c, level, contextPath, expression, rootSchema) {
586
607
  return !resolvedLeft;
587
608
  }
588
609
  case Operator.EQUALS: {
610
+ // biome-ignore lint/suspicious/noDoubleEquals: Breaks prior existing functionality - see when fixture
589
611
  return resolvedLeft == resolvedRight;
590
612
  }
591
613
  case Operator.NOT_EQUALS: {
614
+ // biome-ignore lint/suspicious/noDoubleEquals: Breaks prior existing functionality - see when fixture
592
615
  return resolvedLeft != resolvedRight;
593
616
  }
594
617
  case Operator.GREATER_THAN_OR_EQUALS: {
@@ -673,13 +696,14 @@ export function transformInnerHTML(innerHTML, index = 0) {
673
696
  return transformedInnerHTML;
674
697
  }
675
698
  /**
676
- * Resolves f-when
699
+ * Resolves boolean logic
700
+ * used for f-when and boolean attributes
677
701
  * @param self - Where the first item in the path path refers to the item itself (used by repeat).
678
702
  * @param chainedExpression - The chained expression which includes the expression and the next expression
679
703
  * if there is another in the chain
680
704
  * @returns - A binding that resolves the chained expression logic
681
705
  */
682
- export function resolveWhen(rootPropertyName, expression, parentContext, level, schema) {
706
+ export function getBooleanBinding(rootPropertyName, expression, parentContext, level, schema) {
683
707
  const binding = expressionResolver(rootPropertyName, expression, parentContext, level, schema);
684
708
  return (x, c) => binding(x, c);
685
709
  }
@@ -1,6 +1,6 @@
1
1
  import { expect, test } from "@playwright/test";
2
2
  import { refPropertyName, Schema } from "./schema.js";
3
- import { getNextBehavior, getIndexOfNextMatchingTag, pathResolver, transformInnerHTML, getExpressionChain, extractPathsFromChainedExpression, getChildrenMap, findDef, resolveWhen, } from "./utilities.js";
3
+ import { extractPathsFromChainedExpression, findDef, getBooleanBinding, getChildrenMap, getExpressionChain, getIndexOfNextMatchingTag, getNextBehavior, pathResolver, transformInnerHTML, } from "./utilities.js";
4
4
  test.describe("utilities", async () => {
5
5
  test.describe("content", async () => {
6
6
  test("get the next content binding", async () => {
@@ -17,7 +17,7 @@ test.describe("utilities", async () => {
17
17
  });
18
18
  test.describe("attributes", async () => {
19
19
  test("get the next attribute binding", async () => {
20
- const innerHTML = "<input type=\"{{type}}\" disabled>";
20
+ const innerHTML = '<input type="{{type}}" disabled>';
21
21
  const templateResult = getNextBehavior(innerHTML);
22
22
  expect(templateResult === null || templateResult === void 0 ? void 0 : templateResult.type).toEqual("dataBinding");
23
23
  expect(templateResult === null || templateResult === void 0 ? void 0 : templateResult.subtype).toEqual("attribute");
@@ -29,7 +29,7 @@ test.describe("utilities", async () => {
29
29
  expect(templateResult === null || templateResult === void 0 ? void 0 : templateResult.closingEndIndex).toEqual(21);
30
30
  });
31
31
  test("get the next attribute event binding", async () => {
32
- const innerHTML = "<input @click=\"{handleClick()}\">";
32
+ const innerHTML = '<input @click="{handleClick()}">';
33
33
  const templateResult = getNextBehavior(innerHTML);
34
34
  expect(templateResult === null || templateResult === void 0 ? void 0 : templateResult.type).toEqual("dataBinding");
35
35
  expect(templateResult === null || templateResult === void 0 ? void 0 : templateResult.subtype).toEqual("attribute");
@@ -46,10 +46,10 @@ test.describe("utilities", async () => {
46
46
  expect(templateResult).toBeNull();
47
47
  });
48
48
  test("skip single-brace non-event, non-property attribute bindings", async () => {
49
- const innerHTML1 = "<input type=\"{type}\">";
49
+ const innerHTML1 = '<input type="{type}">';
50
50
  const templateResult1 = getNextBehavior(innerHTML1);
51
51
  expect(templateResult1).toBeNull();
52
- const innerHTML2 = "<input ?disabled=\"{disabled}\">";
52
+ const innerHTML2 = '<input ?disabled="{disabled}">';
53
53
  const templateResult2 = getNextBehavior(innerHTML2);
54
54
  expect(templateResult2).toBeNull();
55
55
  });
@@ -63,7 +63,7 @@ test.describe("utilities", async () => {
63
63
  expect(templateResult === null || templateResult === void 0 ? void 0 : templateResult.closingStartIndex).toEqual(67);
64
64
  });
65
65
  test("find event binding after skipped single-brace content", async () => {
66
- const innerHTML = "<style>.foo { color: red }</style><button @click=\"{handler()}\">";
66
+ const innerHTML = '<style>.foo { color: red }</style><button @click="{handler()}">';
67
67
  const templateResult = getNextBehavior(innerHTML);
68
68
  expect(templateResult === null || templateResult === void 0 ? void 0 : templateResult.type).toEqual("dataBinding");
69
69
  expect(templateResult === null || templateResult === void 0 ? void 0 : templateResult.subtype).toEqual("attribute");
@@ -71,7 +71,7 @@ test.describe("utilities", async () => {
71
71
  expect(templateResult === null || templateResult === void 0 ? void 0 : templateResult.bindingType).toEqual("client");
72
72
  });
73
73
  test("find property binding after skipped single-brace content", async () => {
74
- const innerHTML = "<style>.foo { color: red } .bar { color: blue }</style><button :value=\"{someValue}\">";
74
+ const innerHTML = '<style>.foo { color: red } .bar { color: blue }</style><button :value="{someValue}">';
75
75
  const templateResult = getNextBehavior(innerHTML);
76
76
  expect(templateResult === null || templateResult === void 0 ? void 0 : templateResult.type).toEqual("dataBinding");
77
77
  expect(templateResult === null || templateResult === void 0 ? void 0 : templateResult.subtype).toEqual("attribute");
@@ -79,13 +79,13 @@ test.describe("utilities", async () => {
79
79
  expect(templateResult === null || templateResult === void 0 ? void 0 : templateResult.bindingType).toEqual("client");
80
80
  });
81
81
  test("ensure if there are expected missing {} this does not cause parsing issues", async () => {
82
- const innerHTML = "<f-when value=\"missing\">";
82
+ const innerHTML = '<f-when value="missing">';
83
83
  expect(getNextBehavior(innerHTML)).toBeTruthy();
84
84
  });
85
85
  });
86
86
  test.describe("templates", async () => {
87
87
  test("when directive", async () => {
88
- const innerHTML = "<f-when value=\"{{show}}\">Hello world</f-when>";
88
+ const innerHTML = '<f-when value="{{show}}">Hello world</f-when>';
89
89
  const templateResult = getNextBehavior(innerHTML);
90
90
  expect(templateResult === null || templateResult === void 0 ? void 0 : templateResult.type).toEqual("templateDirective");
91
91
  expect(templateResult === null || templateResult === void 0 ? void 0 : templateResult.openingTagStartIndex).toEqual(0);
@@ -94,7 +94,7 @@ test.describe("utilities", async () => {
94
94
  expect(templateResult === null || templateResult === void 0 ? void 0 : templateResult.closingTagEndIndex).toEqual(45);
95
95
  });
96
96
  test("when directive with content", async () => {
97
- const innerHTML = "Hello pluto<f-when value=\"{{show}}\">Hello world</f-when>";
97
+ const innerHTML = 'Hello pluto<f-when value="{{show}}">Hello world</f-when>';
98
98
  const templateResult = getNextBehavior(innerHTML);
99
99
  expect(templateResult === null || templateResult === void 0 ? void 0 : templateResult.type).toEqual("templateDirective");
100
100
  expect(templateResult === null || templateResult === void 0 ? void 0 : templateResult.openingTagStartIndex).toEqual(11);
@@ -103,7 +103,7 @@ test.describe("utilities", async () => {
103
103
  expect(templateResult === null || templateResult === void 0 ? void 0 : templateResult.closingTagEndIndex).toEqual(56);
104
104
  });
105
105
  test("when directive with binding", async () => {
106
- const innerHTML = "<f-when value=\"{{show}}\">{{text}}</f-when>";
106
+ const innerHTML = '<f-when value="{{show}}">{{text}}</f-when>';
107
107
  const templateResult = getNextBehavior(innerHTML);
108
108
  expect(templateResult === null || templateResult === void 0 ? void 0 : templateResult.type).toEqual("templateDirective");
109
109
  expect(templateResult === null || templateResult === void 0 ? void 0 : templateResult.openingTagStartIndex).toEqual(0);
@@ -111,10 +111,16 @@ test.describe("utilities", async () => {
111
111
  expect(templateResult === null || templateResult === void 0 ? void 0 : templateResult.closingTagStartIndex).toEqual(33);
112
112
  expect(templateResult === null || templateResult === void 0 ? void 0 : templateResult.closingTagEndIndex).toEqual(42);
113
113
  });
114
+ test("should not treat an unknown f- tag as a directive", async () => {
115
+ const innerHTML = '<f-foo value="{{bar}}">Hello</f-foo>';
116
+ const templateResult = getNextBehavior(innerHTML);
117
+ expect(templateResult === null || templateResult === void 0 ? void 0 : templateResult.type).toEqual("dataBinding");
118
+ expect(templateResult === null || templateResult === void 0 ? void 0 : templateResult.subtype).toEqual("attribute");
119
+ });
114
120
  });
115
121
  test.describe("attributes", async () => {
116
122
  test("children directive", async () => {
117
- const innerHTML = "<ul f-children=\"{list}\"></ul>";
123
+ const innerHTML = '<ul f-children="{list}"></ul>';
118
124
  const result = getNextBehavior(innerHTML);
119
125
  expect(result === null || result === void 0 ? void 0 : result.type).toEqual("dataBinding");
120
126
  expect(result === null || result === void 0 ? void 0 : result.subtype).toEqual("attributeDirective");
@@ -126,7 +132,7 @@ test.describe("utilities", async () => {
126
132
  expect(result === null || result === void 0 ? void 0 : result.closingEndIndex).toEqual(22);
127
133
  });
128
134
  test("slotted directive", async () => {
129
- const innerHTML = "<slot f-slotted=\"{slottedNodes}\"></slot>";
135
+ const innerHTML = '<slot f-slotted="{slottedNodes}"></slot>';
130
136
  const result = getNextBehavior(innerHTML);
131
137
  expect(result === null || result === void 0 ? void 0 : result.type).toEqual("dataBinding");
132
138
  expect(result === null || result === void 0 ? void 0 : result.subtype).toEqual("attributeDirective");
@@ -138,7 +144,7 @@ test.describe("utilities", async () => {
138
144
  expect(result === null || result === void 0 ? void 0 : result.closingEndIndex).toEqual(31);
139
145
  });
140
146
  test("ref directive", async () => {
141
- const innerHTML = "<video f-ref=\"{video}\"></video>";
147
+ const innerHTML = '<video f-ref="{video}"></video>';
142
148
  const result = getNextBehavior(innerHTML);
143
149
  expect(result === null || result === void 0 ? void 0 : result.type).toEqual("dataBinding");
144
150
  expect(result === null || result === void 0 ? void 0 : result.subtype).toEqual("attributeDirective");
@@ -214,7 +220,7 @@ test.describe("utilities", async () => {
214
220
  leftIsValue: false,
215
221
  right: null,
216
222
  rightIsValue: null,
217
- }
223
+ },
218
224
  });
219
225
  });
220
226
  test("should resolve a falsy value", async () => {
@@ -225,7 +231,7 @@ test.describe("utilities", async () => {
225
231
  leftIsValue: false,
226
232
  right: null,
227
233
  rightIsValue: null,
228
- }
234
+ },
229
235
  });
230
236
  });
231
237
  test("should resolve a path not equal to string value", async () => {
@@ -236,7 +242,7 @@ test.describe("utilities", async () => {
236
242
  leftIsValue: false,
237
243
  right: "test",
238
244
  rightIsValue: true,
239
- }
245
+ },
240
246
  });
241
247
  });
242
248
  test("should resolve a path not equal to boolean value", async () => {
@@ -247,7 +253,7 @@ test.describe("utilities", async () => {
247
253
  leftIsValue: false,
248
254
  right: false,
249
255
  rightIsValue: true,
250
- }
256
+ },
251
257
  });
252
258
  });
253
259
  test("should resolve a path not equal to numerical value", async () => {
@@ -258,7 +264,7 @@ test.describe("utilities", async () => {
258
264
  leftIsValue: false,
259
265
  right: 5,
260
266
  rightIsValue: true,
261
- }
267
+ },
262
268
  });
263
269
  });
264
270
  test("should resolve chained expressions", async () => {
@@ -278,8 +284,8 @@ test.describe("utilities", async () => {
278
284
  leftIsValue: false,
279
285
  right: "baz",
280
286
  rightIsValue: true,
281
- }
282
- }
287
+ },
288
+ },
283
289
  });
284
290
  expect(getExpressionChain("foo && bar")).toEqual({
285
291
  expression: {
@@ -297,8 +303,8 @@ test.describe("utilities", async () => {
297
303
  leftIsValue: false,
298
304
  right: null,
299
305
  rightIsValue: null,
300
- }
301
- }
306
+ },
307
+ },
302
308
  });
303
309
  });
304
310
  });
@@ -401,7 +407,7 @@ test.describe("utilities", async () => {
401
407
  expect(paths.has("app.user.profile.settings.theme")).toBe(true);
402
408
  });
403
409
  });
404
- test.describe("resolveWhen - default case truthiness evaluation", async () => {
410
+ test.describe("getBooleanBinding - default case truthiness evaluation", async () => {
405
411
  // Helper to create a basic schema for testing
406
412
  const createTestSchema = (rootPropertyName, propertyName) => {
407
413
  const schema = new Schema("test-element");
@@ -420,98 +426,98 @@ test.describe("utilities", async () => {
420
426
  test("should evaluate boolean true as truthy", async () => {
421
427
  const schema = createTestSchema("testData", "boolTrue");
422
428
  const expression = getExpressionChain("boolTrue");
423
- const resolver = resolveWhen("testData", expression, null, 0, schema);
429
+ const resolver = getBooleanBinding("testData", expression, null, 0, schema);
424
430
  const result = resolver({ boolTrue: true }, null);
425
431
  expect(result).toBe(true);
426
432
  });
427
433
  test("should evaluate boolean false as falsy", async () => {
428
434
  const schema = createTestSchema("testData", "boolFalse");
429
435
  const expression = getExpressionChain("boolFalse");
430
- const resolver = resolveWhen("testData", expression, null, 0, schema);
436
+ const resolver = getBooleanBinding("testData", expression, null, 0, schema);
431
437
  const result = resolver({ boolFalse: false }, null);
432
438
  expect(result).toBe(false);
433
439
  });
434
440
  test("should evaluate number 0 as falsy", async () => {
435
441
  const schema = createTestSchema("testData", "numberZero");
436
442
  const expression = getExpressionChain("numberZero");
437
- const resolver = resolveWhen("testData", expression, null, 0, schema);
443
+ const resolver = getBooleanBinding("testData", expression, null, 0, schema);
438
444
  const result = resolver({ numberZero: 0 }, null);
439
445
  expect(result).toBe(false);
440
446
  });
441
447
  test("should evaluate positive number as truthy", async () => {
442
448
  const schema = createTestSchema("testData", "numberPositive");
443
449
  const expression = getExpressionChain("numberPositive");
444
- const resolver = resolveWhen("testData", expression, null, 0, schema);
450
+ const resolver = getBooleanBinding("testData", expression, null, 0, schema);
445
451
  const result = resolver({ numberPositive: 42 }, null);
446
452
  expect(result).toBe(true);
447
453
  });
448
454
  test("should evaluate negative number as truthy", async () => {
449
455
  const schema = createTestSchema("testData", "numberNegative");
450
456
  const expression = getExpressionChain("numberNegative");
451
- const resolver = resolveWhen("testData", expression, null, 0, schema);
457
+ const resolver = getBooleanBinding("testData", expression, null, 0, schema);
452
458
  const result = resolver({ numberNegative: -5 }, null);
453
459
  expect(result).toBe(true);
454
460
  });
455
461
  test("should evaluate empty string as falsy", async () => {
456
462
  const schema = createTestSchema("testData", "stringEmpty");
457
463
  const expression = getExpressionChain("stringEmpty");
458
- const resolver = resolveWhen("testData", expression, null, 0, schema);
464
+ const resolver = getBooleanBinding("testData", expression, null, 0, schema);
459
465
  const result = resolver({ stringEmpty: "" }, null);
460
466
  expect(result).toBe(false);
461
467
  });
462
468
  test("should evaluate non-empty string as truthy", async () => {
463
469
  const schema = createTestSchema("testData", "stringNonEmpty");
464
470
  const expression = getExpressionChain("stringNonEmpty");
465
- const resolver = resolveWhen("testData", expression, null, 0, schema);
471
+ const resolver = getBooleanBinding("testData", expression, null, 0, schema);
466
472
  const result = resolver({ stringNonEmpty: "hello" }, null);
467
473
  expect(result).toBe(true);
468
474
  });
469
475
  test("should evaluate null as falsy", async () => {
470
476
  const schema = createTestSchema("testData", "objectNull");
471
477
  const expression = getExpressionChain("objectNull");
472
- const resolver = resolveWhen("testData", expression, null, 0, schema);
478
+ const resolver = getBooleanBinding("testData", expression, null, 0, schema);
473
479
  const result = resolver({ objectNull: null }, null);
474
480
  expect(result).toBe(false);
475
481
  });
476
482
  test("should evaluate undefined as falsy", async () => {
477
483
  const schema = createTestSchema("testData", "undefinedProp");
478
484
  const expression = getExpressionChain("undefinedProp");
479
- const resolver = resolveWhen("testData", expression, null, 0, schema);
485
+ const resolver = getBooleanBinding("testData", expression, null, 0, schema);
480
486
  const result = resolver({ undefinedProp: undefined }, null);
481
487
  expect(result).toBe(false);
482
488
  });
483
489
  test("should evaluate non-null object as truthy", async () => {
484
490
  const schema = createTestSchema("testData", "objectNonNull");
485
491
  const expression = getExpressionChain("objectNonNull");
486
- const resolver = resolveWhen("testData", expression, null, 0, schema);
492
+ const resolver = getBooleanBinding("testData", expression, null, 0, schema);
487
493
  const result = resolver({ objectNonNull: { foo: "bar" } }, null);
488
494
  expect(result).toBe(true);
489
495
  });
490
496
  test("should evaluate empty array as truthy", async () => {
491
497
  const schema = createTestSchema("testData", "arrayEmpty");
492
498
  const expression = getExpressionChain("arrayEmpty");
493
- const resolver = resolveWhen("testData", expression, null, 0, schema);
499
+ const resolver = getBooleanBinding("testData", expression, null, 0, schema);
494
500
  const result = resolver({ arrayEmpty: [] }, null);
495
501
  expect(result).toBe(true);
496
502
  });
497
503
  test("should evaluate non-empty array as truthy", async () => {
498
504
  const schema = createTestSchema("testData", "arrayNonEmpty");
499
505
  const expression = getExpressionChain("arrayNonEmpty");
500
- const resolver = resolveWhen("testData", expression, null, 0, schema);
506
+ const resolver = getBooleanBinding("testData", expression, null, 0, schema);
501
507
  const result = resolver({ arrayNonEmpty: [1, 2, 3] }, null);
502
508
  expect(result).toBe(true);
503
509
  });
504
510
  test("should evaluate string with only whitespace as truthy", async () => {
505
511
  const schema = createTestSchema("testData", "stringWhitespace");
506
512
  const expression = getExpressionChain("stringWhitespace");
507
- const resolver = resolveWhen("testData", expression, null, 0, schema);
513
+ const resolver = getBooleanBinding("testData", expression, null, 0, schema);
508
514
  const result = resolver({ stringWhitespace: " " }, null);
509
515
  expect(result).toBe(true);
510
516
  });
511
517
  test("should evaluate number NaN as truthy", async () => {
512
518
  const schema = createTestSchema("testData", "numberNaN");
513
519
  const expression = getExpressionChain("numberNaN");
514
- const resolver = resolveWhen("testData", expression, null, 0, schema);
520
+ const resolver = getBooleanBinding("testData", expression, null, 0, schema);
515
521
  const result = resolver({ numberNaN: NaN }, null);
516
522
  expect(result).toBe(true);
517
523
  });
@@ -548,37 +554,37 @@ test.describe("utilities", async () => {
548
554
  test.describe("findDef", async () => {
549
555
  test("should resolve from the root of a schema", async () => {
550
556
  expect(findDef({
551
- [refPropertyName]: "#/$defs/MyType"
557
+ [refPropertyName]: "#/$defs/MyType",
552
558
  })).toEqual("MyType");
553
559
  });
554
560
  test("should resolve as null from an anyOf array containing a reference to another component", async () => {
555
561
  expect(findDef({
556
562
  anyOf: [
557
563
  {
558
- [refPropertyName]: "https://fast.design/schemas/test-element/c.json"
559
- }
560
- ]
564
+ [refPropertyName]: "https://fast.design/schemas/test-element/c.json",
565
+ },
566
+ ],
561
567
  })).toEqual(null);
562
568
  });
563
569
  test("should resolve from an anyOf array containing a reference to a $def", async () => {
564
570
  expect(findDef({
565
571
  anyOf: [
566
572
  {
567
- [refPropertyName]: "#/$defs/MyType"
568
- }
569
- ]
573
+ [refPropertyName]: "#/$defs/MyType",
574
+ },
575
+ ],
570
576
  })).toEqual("MyType");
571
577
  });
572
578
  test("should resolve from an anyOf array containing a reference to another component and a reference to a $def", async () => {
573
579
  expect(findDef({
574
580
  anyOf: [
575
581
  {
576
- [refPropertyName]: "https://fast.design/schemas/test-element/c.json"
582
+ [refPropertyName]: "https://fast.design/schemas/test-element/c.json",
577
583
  },
578
584
  {
579
- [refPropertyName]: "#/$defs/MyType"
580
- }
581
- ]
585
+ [refPropertyName]: "#/$defs/MyType",
586
+ },
587
+ ],
582
588
  })).toEqual("MyType");
583
589
  });
584
590
  test("should resolve as null if not found", async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@microsoft/fast-html",
3
- "version": "1.0.0-alpha.40",
3
+ "version": "1.0.0-alpha.42",
4
4
  "type": "module",
5
5
  "author": {
6
6
  "name": "Microsoft",
@@ -23,17 +23,15 @@
23
23
  "scripts": {
24
24
  "build:tsc": "tsc -p ./tsconfig.json",
25
25
  "build": "npm run build:tsc && npm run doc",
26
- "clean": "rimraf dist temp test-results",
26
+ "clean": "clean dist temp test-results",
27
27
  "dev:full": "concurrently -k -n fast-element,fast-html,server \"npm run dev --workspace=@microsoft/fast-element\" \"npm:watch\" \"npm:test-server\"",
28
28
  "dev": "concurrently -k -n tsc,server \"npm run watch\" \"npm run test-server\"",
29
29
  "doc:ci": "api-extractor run",
30
30
  "doc": "npm run doc:ci -- --local",
31
- "eslint:fix": "npm run eslint -- --fix",
32
- "eslint": "eslint . --ext .ts",
31
+ "lint": "biome lint .",
32
+ "lint:fix": "biome lint --fix .",
33
33
  "install-playwright-browsers": "npm run playwright install",
34
34
  "prepublishOnly": "npm run clean && npm run build",
35
- "prettier:diff": "prettier --config ../../.prettierrc \"**/*.{ts,html}\" --list-different",
36
- "prettier": "prettier --config ../../.prettierrc --write \"**/*.{ts,html}\"",
37
35
  "test-server": "npx vite test/ --clearScreen false",
38
36
  "test:playwright": "playwright test",
39
37
  "test:rules": "sg test --skip-snapshot-tests",