@microsoft/fast-html 1.0.0-alpha.43 → 1.0.0-alpha.45

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
@@ -243,18 +243,38 @@ Event bindings must include the `()` as well as being preceeded by `@` in keepin
243
243
  <button @click="{handleClick()}"></button>
244
244
  ```
245
245
 
246
- In addition you may include an event or attribute or observable, events are denoted with `e` as a reserved letter.
246
+ You can pass the DOM event object, the execution context, or both as arguments. Any other argument is treated as a binding expression and resolved against the current data source.
247
247
 
248
- Event:
248
+ **`$e` — DOM event object (preferred):**
249
249
  ```html
250
- <button @click="{handleClick(e)}"></button>
250
+ <button @click="{handleClick($e)}"></button>
251
251
  ```
252
252
 
253
- Attribute/Observable:
253
+ **`$c` — execution context:**
254
254
  ```html
255
- <button @click="{handleClick(foo)}"></button>
255
+ <button @click="{handleClick($c)}"></button>
256
256
  ```
257
257
 
258
+ **`$c.somePath` — a property of the execution context (e.g. `$c.parent`, `$c.event`):**
259
+ ```html
260
+ <button @click="{handleClick($c.parent)}"></button>
261
+ ```
262
+
263
+ **Multiple arguments:**
264
+ ```html
265
+ <button @click="{handleClick($e, $c)}"></button>
266
+ ```
267
+
268
+ **Arbitrary binding expressions** — any token that is not `$e`, `$c`, or `e` is resolved as a binding path on the data source:
269
+ ```html
270
+ <button @click="{handleClick(user.id)}"></button>
271
+ ```
272
+
273
+ > **Deprecated:** The bare `e` token still works but will emit a console warning. Migrate to `$e`.
274
+ > ```html
275
+ > <button @click="{handleClick(e)}"></button>
276
+ > ```
277
+
258
278
  #### Directives
259
279
 
260
280
  Directives are assumed to be either an attribute directive or a directive that also serves a template. Both are prepended by `f-`. The logic of these directives and what their use cases are is explained in the [FAST html documentation](https://fast.design/docs/getting-started/html-directives).
@@ -1,4 +1,4 @@
1
1
  /**
2
2
  * Default syntax for FAST declarative templates
3
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;
4
+ export declare const attributeDirectivePrefix: string, clientSideCloseExpression: string, clientSideOpenExpression: string, closeExpression: string, deprecatedEventArgAccessor: string, eventArgAccessor: string, executionContextAccessor: string, openExpression: string, repeatDirectiveClose: string, repeatDirectiveOpen: string, unescapedCloseExpression: string, unescapedOpenExpression: string, whenDirectiveClose: string, whenDirectiveOpen: string;
@@ -1,4 +1,4 @@
1
- import { FASTElement, HydrationControllerCallbacks, TemplateLifecycleCallbacks } from "@microsoft/fast-element";
1
+ import { FASTElement, type HydrationControllerCallbacks, type TemplateLifecycleCallbacks } from "@microsoft/fast-element";
2
2
  import "@microsoft/fast-element/install-hydratable-view-templates.js";
3
3
  /**
4
4
  * Values for the observerMap element option.
@@ -1,4 +1,5 @@
1
1
  import { type JSONSchema, type JSONSchemaDefinition, Schema } from "./schema.js";
2
+ import { deprecatedEventArgAccessor, eventArgAccessor, executionContextAccessor } from "./syntax.js";
2
3
  type BehaviorType = "dataBinding" | "templateDirective";
3
4
  type TemplateDirective = "when" | "repeat";
4
5
  export type AttributeDirective = "children" | "slotted" | "ref";
@@ -41,6 +42,34 @@ export interface ChildrenMap {
41
42
  attributeName: string;
42
43
  }
43
44
  export declare const contextPrefixDot: string;
45
+ export { deprecatedEventArgAccessor, eventArgAccessor, executionContextAccessor };
46
+ /**
47
+ * The type of a parsed event handler argument.
48
+ */
49
+ export type EventArgType = "event" | "deprecated-event" | "context" | "binding";
50
+ /**
51
+ * A parsed event handler argument descriptor.
52
+ */
53
+ export interface ParsedEventArg {
54
+ type: EventArgType;
55
+ /** The raw argument string, present only when `type` is `"binding"`. */
56
+ rawArg?: string;
57
+ }
58
+ /**
59
+ * Parses the arguments string of an event handler binding into an array of
60
+ * typed argument descriptors. Unrecognised tokens are returned as `"binding"`
61
+ * type with their raw string preserved.
62
+ *
63
+ * Special arguments:
64
+ * - `$e` — resolves to the DOM event object
65
+ * - `e` — resolves to the DOM event object (deprecated, use `$e`)
66
+ * - `$c` — resolves to the full execution context object
67
+ *
68
+ * @param argsString - The raw arguments string from between the parentheses,
69
+ * e.g. `""`, `"$e"`, `"$c"`, or `"$e, $c"`.
70
+ * @returns An array of {@link ParsedEventArg} descriptors.
71
+ */
72
+ export declare function parseEventArgs(argsString: string): ParsedEventArg[];
44
73
  declare const LogicalOperator: {
45
74
  AND: string;
46
75
  OR: string;
@@ -190,4 +219,3 @@ export declare function isPlainObject(value: any): value is Record<string, any>;
190
219
  * @returns boolean indicating whether changes were made
191
220
  */
192
221
  export declare function deepMerge(target: any, source: any): boolean;
193
- export {};
@@ -1,11 +1,13 @@
1
1
  /**
2
2
  * Default syntax for FAST declarative templates
3
3
  */
4
- export const { attributeDirectivePrefix, clientSideCloseExpression, clientSideOpenExpression, closeExpression, executionContextAccessor, openExpression, repeatDirectiveClose, repeatDirectiveOpen, unescapedCloseExpression, unescapedOpenExpression, whenDirectiveClose, whenDirectiveOpen, } = {
4
+ export const { attributeDirectivePrefix, clientSideCloseExpression, clientSideOpenExpression, closeExpression, deprecatedEventArgAccessor, eventArgAccessor, executionContextAccessor, openExpression, repeatDirectiveClose, repeatDirectiveOpen, unescapedCloseExpression, unescapedOpenExpression, whenDirectiveClose, whenDirectiveOpen, } = {
5
5
  attributeDirectivePrefix: "f-",
6
6
  clientSideCloseExpression: "}",
7
7
  clientSideOpenExpression: "{",
8
8
  closeExpression: "}}",
9
+ deprecatedEventArgAccessor: "e",
10
+ eventArgAccessor: "$e",
9
11
  executionContextAccessor: "$c",
10
12
  openExpression: "{{",
11
13
  unescapedCloseExpression: "}}}",
@@ -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, contextPrefixDot, getBooleanBinding, getExpressionChain, getNextBehavior, getRootPropertyName, transformInnerHTML, } from "./utilities.js";
6
+ import { bindingResolver, contextPrefixDot, eventArgAccessor, getBooleanBinding, getExpressionChain, getNextBehavior, getRootPropertyName, parseEventArgs, transformInnerHTML, } from "./utilities.js";
7
7
  /**
8
8
  * Values for the observerMap element option.
9
9
  */
@@ -226,7 +226,7 @@ class TemplateElement extends FASTElement {
226
226
  1);
227
227
  const type = "event";
228
228
  rootPropertyName = getRootPropertyName(rootPropertyName, propName, parentContext, type);
229
- const arg = bindingHTML.slice(openingParenthesis + 1, closingParenthesis);
229
+ const argsString = bindingHTML.slice(openingParenthesis + 1, closingParenthesis);
230
230
  const binding = bindingResolver(strings.join(""), rootPropertyName, propName, parentContext, type, schema, parentContext, level);
231
231
  const isContextPath = propName.startsWith(contextPrefixDot);
232
232
  const getOwner = isContextPath
@@ -235,11 +235,23 @@ class TemplateElement extends FASTElement {
235
235
  return ownerPath.reduce((prev, item) => prev === null || prev === void 0 ? void 0 : prev[item], c);
236
236
  }
237
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
- : []));
238
+ const parsedArgs = parseEventArgs(argsString);
239
+ if (parsedArgs.some(a => a.type === "deprecated-event")) {
240
+ console.warn(`[fast-html] Using "e" as an event argument is deprecated. ` +
241
+ `Use "${eventArgAccessor}" instead.`);
242
+ }
243
+ const argResolvers = parsedArgs.map((parsedArg) => {
244
+ switch (parsedArg.type) {
245
+ case "event":
246
+ case "deprecated-event":
247
+ return (_x, c) => c.event;
248
+ case "context":
249
+ return (_x, c) => c;
250
+ case "binding":
251
+ return bindingResolver(strings.join(""), rootPropertyName, parsedArg.rawArg, parentContext, type, schema, parentContext, level);
252
+ }
253
+ });
254
+ attributeBinding = (x, c) => binding(x, c).bind(getOwner(x, c))(...argResolvers.map(resolve => resolve(x, c)));
243
255
  break;
244
256
  }
245
257
  case "?": {
@@ -1,7 +1,42 @@
1
1
  import { Observable } from "@microsoft/fast-element/observable.js";
2
2
  import { defsPropertyName, fastContextMetaData, refPropertyName, Schema, } from "./schema.js";
3
- import { attributeDirectivePrefix, clientSideCloseExpression, clientSideOpenExpression, closeExpression, executionContextAccessor, openExpression, repeatDirectiveClose, repeatDirectiveOpen, unescapedCloseExpression, unescapedOpenExpression, whenDirectiveClose, whenDirectiveOpen, } from "./syntax.js";
3
+ import { attributeDirectivePrefix, clientSideCloseExpression, clientSideOpenExpression, closeExpression, deprecatedEventArgAccessor, eventArgAccessor, executionContextAccessor, openExpression, repeatDirectiveClose, repeatDirectiveOpen, unescapedCloseExpression, unescapedOpenExpression, whenDirectiveClose, whenDirectiveOpen, } from "./syntax.js";
4
4
  export const contextPrefixDot = `${executionContextAccessor}.`;
5
+ export { deprecatedEventArgAccessor, eventArgAccessor, executionContextAccessor };
6
+ /**
7
+ * Parses the arguments string of an event handler binding into an array of
8
+ * typed argument descriptors. Unrecognised tokens are returned as `"binding"`
9
+ * type with their raw string preserved.
10
+ *
11
+ * Special arguments:
12
+ * - `$e` — resolves to the DOM event object
13
+ * - `e` — resolves to the DOM event object (deprecated, use `$e`)
14
+ * - `$c` — resolves to the full execution context object
15
+ *
16
+ * @param argsString - The raw arguments string from between the parentheses,
17
+ * e.g. `""`, `"$e"`, `"$c"`, or `"$e, $c"`.
18
+ * @returns An array of {@link ParsedEventArg} descriptors.
19
+ */
20
+ export function parseEventArgs(argsString) {
21
+ if (argsString.trim() === "")
22
+ return [];
23
+ return argsString
24
+ .split(",")
25
+ .map(arg => arg.trim())
26
+ .filter(arg => arg !== "")
27
+ .map((arg) => {
28
+ switch (arg) {
29
+ case eventArgAccessor:
30
+ return { type: "event" };
31
+ case deprecatedEventArgAccessor:
32
+ return { type: "deprecated-event" };
33
+ case executionContextAccessor:
34
+ return { type: "context" };
35
+ default:
36
+ return { type: "binding", rawArg: arg };
37
+ }
38
+ });
39
+ }
5
40
  const startInnerHTMLDiv = `<div :innerHTML="{{`;
6
41
  const startInnerHTMLDivLength = startInnerHTMLDiv.length;
7
42
  const endInnerHTMLDiv = `}}"></div>`;
@@ -789,7 +824,29 @@ function assignObservablesToArray(proxiedData, schema, rootSchema, target, rootP
789
824
  });
790
825
  },
791
826
  });
792
- return data;
827
+ if (schemaProperties !== null) {
828
+ return data;
829
+ }
830
+ // For primitive arrays, wrap in a Proxy so that direct index assignment
831
+ // (e.g. arr[0] = value) triggers FAST's splice-based change tracking and
832
+ // keeps repeat directives in sync. Object arrays are not wrapped because
833
+ // their items are individually proxied, and FAST's own push/splice/etc.
834
+ // already carry splice records — double-wrapping would produce duplicate
835
+ // splice notifications.
836
+ return new Proxy(data, {
837
+ set: (arr, prop, value) => {
838
+ const idx = typeof prop === "string" ? Number(prop) : NaN;
839
+ if (typeof prop !== "symbol" && Number.isInteger(idx) && idx >= 0) {
840
+ // splice() replaces the item in-place and creates the splice
841
+ // record that FAST's ArrayObserver delivers to repeat directives.
842
+ Array.prototype.splice.call(arr, idx, 1, value);
843
+ }
844
+ else {
845
+ arr[prop] = value;
846
+ }
847
+ return true;
848
+ },
849
+ });
793
850
  }
794
851
  /**
795
852
  * Extracts the definition name from a JSON Schema $ref property
@@ -866,6 +923,12 @@ export function assignObservables(schema, rootSchema, data, target, rootProperty
866
923
  }));
867
924
  }
868
925
  }
926
+ else {
927
+ // Primitive array (items have no schema $ref): wrap in a proxy so that
928
+ // direct index assignments (e.g. arr[0] = value) use FAST's splice-based
929
+ // change tracking and keep repeat directives in sync.
930
+ proxiedData = assignObservablesToArray(proxiedData, schema, rootSchema, target, rootProperty);
931
+ }
869
932
  break;
870
933
  }
871
934
  case "object": {
@@ -1,6 +1,6 @@
1
1
  import { expect, test } from "@playwright/test";
2
2
  import { refPropertyName, Schema } from "./schema.js";
3
- import { extractPathsFromChainedExpression, findDef, getBooleanBinding, getChildrenMap, getExpressionChain, getIndexOfNextMatchingTag, getNextBehavior, pathResolver, transformInnerHTML, } from "./utilities.js";
3
+ import { eventArgAccessor, extractPathsFromChainedExpression, findDef, getBooleanBinding, getChildrenMap, getExpressionChain, getIndexOfNextMatchingTag, getNextBehavior, parseEventArgs, 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 () => {
@@ -592,4 +592,55 @@ test.describe("utilities", async () => {
592
592
  });
593
593
  });
594
594
  });
595
+ test.describe("parseEventArgs", async () => {
596
+ test("should return an empty array for an empty string", async () => {
597
+ expect(parseEventArgs("")).toEqual([]);
598
+ });
599
+ test("should parse $e as an event argument", async () => {
600
+ expect(parseEventArgs(eventArgAccessor)).toEqual([{ type: "event" }]);
601
+ });
602
+ test("should parse e (no $) as a deprecated-event argument", async () => {
603
+ expect(parseEventArgs("e")).toEqual([{ type: "deprecated-event" }]);
604
+ });
605
+ test("should parse $c as a context argument", async () => {
606
+ expect(parseEventArgs("$c")).toEqual([{ type: "context" }]);
607
+ });
608
+ test("should return a binding type for unrecognised tokens", async () => {
609
+ expect(parseEventArgs("foo")).toEqual([{ type: "binding", rawArg: "foo" }]);
610
+ });
611
+ test("should return a binding type for $c.path tokens", async () => {
612
+ expect(parseEventArgs("$c.eventDetail")).toEqual([
613
+ { type: "binding", rawArg: "$c.eventDetail" },
614
+ ]);
615
+ });
616
+ test("should return a binding type for $c.event", async () => {
617
+ expect(parseEventArgs("$c.event")).toEqual([
618
+ { type: "binding", rawArg: "$c.event" },
619
+ ]);
620
+ });
621
+ test("should parse multiple arguments: $e, $c", async () => {
622
+ expect(parseEventArgs("$e, $c")).toEqual([
623
+ { type: "event" },
624
+ { type: "context" },
625
+ ]);
626
+ });
627
+ test("should parse multiple arguments without spaces", async () => {
628
+ expect(parseEventArgs("$e,$c")).toEqual([
629
+ { type: "event" },
630
+ { type: "context" },
631
+ ]);
632
+ });
633
+ test("should parse e (deprecated) mixed with $c", async () => {
634
+ expect(parseEventArgs("e, $c")).toEqual([
635
+ { type: "deprecated-event" },
636
+ { type: "context" },
637
+ ]);
638
+ });
639
+ test("should return a binding type for unrecognised tokens in a mixed list", async () => {
640
+ expect(parseEventArgs("$e, foo")).toEqual([
641
+ { type: "event" },
642
+ { type: "binding", rawArg: "foo" },
643
+ ]);
644
+ });
645
+ });
595
646
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@microsoft/fast-html",
3
- "version": "1.0.0-alpha.43",
3
+ "version": "1.0.0-alpha.45",
4
4
  "type": "module",
5
5
  "author": {
6
6
  "name": "Microsoft",
@@ -23,6 +23,7 @@
23
23
  "scripts": {
24
24
  "build:tsc": "tsc -p ./tsconfig.json",
25
25
  "build": "npm run build:tsc && npm run doc",
26
+ "build:fixtures": "node scripts/build-fixtures.js",
26
27
  "clean": "clean dist temp test-results",
27
28
  "dev:full": "concurrently -k -n fast-element,fast-html,server \"npm run dev --workspace=@microsoft/fast-element\" \"npm:watch\" \"npm:test-server\"",
28
29
  "dev": "concurrently -k -n tsc,server \"npm run watch\" \"npm run test-server\"",
@@ -59,6 +60,7 @@
59
60
  "@microsoft/fast-element": "^2.10.2"
60
61
  },
61
62
  "devDependencies": {
63
+ "@microsoft/fast-build": "^0.1.2",
62
64
  "@microsoft/fast-element": "^2.10.2"
63
65
  },
64
66
  "beachball": {