@microsoft/fast-html 1.0.0-alpha.22 → 1.0.0-alpha.24

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.
@@ -1,5 +1,5 @@
1
1
  import { Observable } from "@microsoft/fast-element/observable.js";
2
- import { defsPropertyName, refPropertyName, } from "./schema.js";
2
+ import { defsPropertyName, refPropertyName, Schema, } from "./schema.js";
3
3
  const openClientSideBinding = "{";
4
4
  const closeClientSideBinding = "}";
5
5
  const openContentBinding = "{{";
@@ -14,6 +14,10 @@ const startInnerHTMLDiv = `<div :innerHTML="{{`;
14
14
  const startInnerHTMLDivLength = startInnerHTMLDiv.length;
15
15
  const endInnerHTMLDiv = `}}"></div>`;
16
16
  const endInnerHTMLDivLength = endInnerHTMLDiv.length;
17
+ /**
18
+ * A map of proxied objects
19
+ */
20
+ const objectTargetsMap = new WeakMap();
17
21
  /**
18
22
  * Get the index of the next matching tag
19
23
  * @param openingTagStartSlice - The slice starting from the opening tag
@@ -281,9 +285,10 @@ function pathWithContextResolver(splitPath, self) {
281
285
  }, accessibleObject);
282
286
  };
283
287
  }
284
- export function bindingResolver(rootPropertyName, path, parentContext, type, schema, currentContext, level) {
288
+ export function bindingResolver(previousString, rootPropertyName, path, parentContext, type, schema, currentContext, level) {
285
289
  rootPropertyName = getRootPropertyName(rootPropertyName, path, currentContext, type);
286
290
  if (type !== "event" && rootPropertyName !== null) {
291
+ const childrenMap = getChildrenMap(previousString);
287
292
  schema.addPath({
288
293
  pathConfig: {
289
294
  type,
@@ -292,6 +297,7 @@ export function bindingResolver(rootPropertyName, path, parentContext, type, sch
292
297
  path,
293
298
  },
294
299
  rootPropertyName,
300
+ childrenMap,
295
301
  });
296
302
  }
297
303
  return pathResolver(path, currentContext, level, schema.getSchema(rootPropertyName));
@@ -574,6 +580,40 @@ function getDataType(data) {
574
580
  return "object";
575
581
  return "primitive";
576
582
  }
583
+ /**
584
+ * Get properties from an anyOf array
585
+ * @param anyOf - The anyOf array in a JSON schema
586
+ * @returns The array item matching a ref if it exists
587
+ */
588
+ function getSchemaPropertiesFromAnyOf(anyOf) {
589
+ let propertiesFromAnyOf = null;
590
+ for (const anyOfItem of anyOf) {
591
+ if (anyOfItem[refPropertyName]) {
592
+ const splitRef = anyOfItem[refPropertyName].split("/");
593
+ const customElement = splitRef.slice(-2)[0];
594
+ const attributeName = splitRef.slice(-1)[0].slice(0, -5);
595
+ if (Schema.jsonSchemaMap.has(customElement)) {
596
+ const customElementSchemaMap = Schema.jsonSchemaMap.get(customElement);
597
+ propertiesFromAnyOf = customElementSchemaMap.get(attributeName);
598
+ }
599
+ }
600
+ }
601
+ return propertiesFromAnyOf;
602
+ }
603
+ /**
604
+ * Gets a properties definition if one exists
605
+ * @param schema - The JSON schema to get properties from
606
+ * @returns A JSON schema with properties or null
607
+ */
608
+ function getSchemaProperties(schema) {
609
+ if (schema === null || schema === void 0 ? void 0 : schema.properties) {
610
+ return schema.properties;
611
+ }
612
+ else if (schema === null || schema === void 0 ? void 0 : schema.anyOf) {
613
+ return getSchemaPropertiesFromAnyOf(schema.anyOf);
614
+ }
615
+ return null;
616
+ }
577
617
  /**
578
618
  * Assigns Observable properties to items in an array and sets up change notifications
579
619
  * @param proxiedData - The array data to make observable
@@ -612,6 +652,33 @@ function getDefFromRef(defName) {
612
652
  const splitName = defName.split("/");
613
653
  return splitName.at(-1);
614
654
  }
655
+ /**
656
+ * Find a definition
657
+ * This may exist as a $ref at the root or as a $ref in any anyOf or not at all
658
+ * if the Observer Map has not been enabled on a child component
659
+ * @param schema - The JSON schema to find the ref in
660
+ * @returns The definition or null
661
+ */
662
+ export function findDef(schema) {
663
+ const defStartingString = "#/$defs";
664
+ if (schema[refPropertyName] &&
665
+ schema[refPropertyName].startsWith(defStartingString)) {
666
+ return getDefFromRef(schema[refPropertyName]);
667
+ }
668
+ if (schema.anyOf) {
669
+ const index = schema.anyOf.findIndex((anyOfItem) => {
670
+ return (!!anyOfItem[refPropertyName] &&
671
+ anyOfItem[refPropertyName].startsWith(defStartingString));
672
+ });
673
+ if (index > -1) {
674
+ const ref = schema.anyOf[index][refPropertyName];
675
+ if (ref.startsWith(defStartingString)) {
676
+ return getDefFromRef(ref);
677
+ }
678
+ }
679
+ }
680
+ return null;
681
+ }
615
682
  /**
616
683
  * Assign observables to data
617
684
  * @param schema - The schema
@@ -627,8 +694,10 @@ export function assignObservables(schema, rootSchema, data, target, rootProperty
627
694
  let proxiedData = data;
628
695
  switch (dataType) {
629
696
  case "array": {
630
- const context = getDefFromRef(schema[refPropertyName]);
631
- proxiedData = assignObservablesToArray(proxiedData, (_a = rootSchema[defsPropertyName]) === null || _a === void 0 ? void 0 : _a[context], rootSchema);
697
+ const context = findDef(schema);
698
+ if (context) {
699
+ proxiedData = assignObservablesToArray(proxiedData, (_a = rootSchema[defsPropertyName]) === null || _a === void 0 ? void 0 : _a[context], rootSchema);
700
+ }
632
701
  break;
633
702
  }
634
703
  case "object": {
@@ -666,26 +735,62 @@ function assignProxyToItemsInArray(item, originalItem, schema, rootSchema) {
666
735
  function assignProxyToItemsInObject(target, rootProperty, data, schema, rootSchema) {
667
736
  var _a;
668
737
  const type = getDataType(data);
738
+ const schemaProperties = getSchemaProperties(schema);
669
739
  let proxiedData = data;
670
- if (type === "object" && (schema === null || schema === void 0 ? void 0 : schema.properties)) {
740
+ if (type === "object" && schemaProperties) {
671
741
  // navigate through all items in the object
672
- Object.keys(schema.properties).forEach(property => {
673
- if (proxiedData[property] && schema && schema.properties) {
674
- proxiedData[property] = assignProxyToItemsInObject(target, rootProperty, proxiedData[property], schema.properties[property], rootSchema);
742
+ Object.keys(schemaProperties).forEach(property => {
743
+ if (proxiedData[property] && schema && schemaProperties) {
744
+ proxiedData[property] = assignProxyToItemsInObject(target, rootProperty, proxiedData[property], schemaProperties[property], rootSchema);
675
745
  }
676
746
  });
677
747
  // assign a Proxy to the object
678
748
  proxiedData = assignProxy(schema, rootSchema, target, rootProperty, data);
749
+ // Add this target to the object's target list
750
+ addTargetToObject(proxiedData, target, rootProperty);
679
751
  }
680
752
  else if (type === "array") {
681
- const context = getDefFromRef(schema.items[refPropertyName]);
682
- const definition = (_a = rootSchema[defsPropertyName]) === null || _a === void 0 ? void 0 : _a[context];
683
- if ((definition === null || definition === void 0 ? void 0 : definition.type) === "object") {
684
- proxiedData = assignObservablesToArray(proxiedData, definition, rootSchema);
753
+ const context = findDef(schema.items);
754
+ if (context) {
755
+ const definition = (_a = rootSchema[defsPropertyName]) === null || _a === void 0 ? void 0 : _a[context];
756
+ if ((definition === null || definition === void 0 ? void 0 : definition.type) === "object") {
757
+ proxiedData = assignObservablesToArray(proxiedData, definition, rootSchema);
758
+ }
685
759
  }
686
760
  }
687
761
  return proxiedData;
688
762
  }
763
+ /**
764
+ * Add a target to an object's target list
765
+ * @param object - The object to associate with the target
766
+ * @param target - The target custom element
767
+ * @param rootProperty - The root property name
768
+ */
769
+ function addTargetToObject(object, target, rootProperty) {
770
+ if (!objectTargetsMap.has(object)) {
771
+ objectTargetsMap.set(object, []);
772
+ }
773
+ const targets = objectTargetsMap.get(object);
774
+ targets.push({ target, rootProperty });
775
+ }
776
+ /**
777
+ * Get all targets for an object
778
+ * @param object - The object to get targets for
779
+ * @returns Array of target info objects
780
+ */
781
+ function getTargetsForObject(object) {
782
+ return objectTargetsMap.get(object) || [];
783
+ }
784
+ /**
785
+ * Notify any observables mapped to the object
786
+ * @param targetObject The object that is mapped to a target and rootProperty
787
+ */
788
+ function notifyObservables(targetObject) {
789
+ getTargetsForObject(targetObject).forEach((targetItem) => {
790
+ // Trigger notification for property changes
791
+ Observable.notify(targetItem.target, targetItem.rootProperty);
792
+ });
793
+ }
689
794
  /**
690
795
  * Assign a proxy to an object
691
796
  * @param schema - The current schema
@@ -698,11 +803,10 @@ function assignProxyToItemsInObject(target, rootProperty, data, schema, rootSche
698
803
  export function assignProxy(schema, rootSchema, target, rootProperty, object) {
699
804
  if (object.$isProxy === undefined) {
700
805
  // Create a proxy for the object that triggers Observable.notify on mutations
701
- return new Proxy(object, {
806
+ const proxy = new Proxy(object, {
702
807
  set: (obj, prop, value) => {
703
808
  obj[prop] = assignObservables(schema, rootSchema, value, target, rootProperty);
704
- // Trigger notification for property changes
705
- Observable.notify(target, rootProperty);
809
+ notifyObservables(proxy);
706
810
  return true;
707
811
  },
708
812
  get: (target, key) => {
@@ -714,13 +818,13 @@ export function assignProxy(schema, rootSchema, target, rootProperty, object) {
714
818
  deleteProperty: (obj, prop) => {
715
819
  if (prop in obj) {
716
820
  delete obj[prop];
717
- // Trigger notification for property deletion
718
- Observable.notify(target, rootProperty);
821
+ notifyObservables(proxy);
719
822
  return true;
720
823
  }
721
824
  return false;
722
825
  },
723
826
  });
827
+ return proxy;
724
828
  }
725
829
  return object;
726
830
  }
@@ -737,3 +841,51 @@ export function getRootPropertyName(rootPropertyName, path, context, type) {
737
841
  ? path.split(".")[0]
738
842
  : rootPropertyName;
739
843
  }
844
+ /**
845
+ * Get details of bindings to the attributes of child custom elements
846
+ * @param previousString - The previous string before the binding
847
+ * @returns null, or a custom element name and attribute name
848
+ */
849
+ export function getChildrenMap(previousString) {
850
+ if (typeof previousString === "string" &&
851
+ isAttribute(previousString, previousString.length)) {
852
+ const customElementName = getAttributesCustomElementName(previousString);
853
+ if (customElementName) {
854
+ return {
855
+ customElementName,
856
+ attributeName: getAttributeName(previousString),
857
+ };
858
+ }
859
+ }
860
+ return null;
861
+ }
862
+ /**
863
+ * Get the HTML element that is passing the attribute binding
864
+ * @param previousString - The previous string before the binding
865
+ * @returns null if this is not a custom element, or the custom element that is passing the binding as an attribute
866
+ */
867
+ function getAttributesCustomElementName(previousString) {
868
+ const indexOfElementTagStart = previousString.lastIndexOf("<") + 1;
869
+ const indexOfElementTagEnd = previousString.slice(indexOfElementTagStart).indexOf(" ") +
870
+ indexOfElementTagStart;
871
+ const elementName = previousString.slice(indexOfElementTagStart, indexOfElementTagEnd);
872
+ if (elementName.includes("-")) {
873
+ return elementName;
874
+ }
875
+ return null;
876
+ }
877
+ /**
878
+ * Gets a non-aspected attribute name
879
+ * @param previousString - The previous string before the binding
880
+ * @returns The attribute name with any aspects (:, ?, @) removed
881
+ */
882
+ function getAttributeName(previousString) {
883
+ const indexOfAttributeStart = previousString.lastIndexOf(" ") + 1;
884
+ const indexOfAttributeEnd = previousString.slice(indexOfAttributeStart).indexOf("=") + indexOfAttributeStart;
885
+ const attributeName = previousString.slice(indexOfAttributeStart, indexOfAttributeEnd);
886
+ const potentialAspect = attributeName.charAt(0);
887
+ if (potentialAspect === ":" || potentialAspect === "@" || potentialAspect === "?") {
888
+ return attributeName.slice(1);
889
+ }
890
+ return attributeName;
891
+ }
@@ -1,6 +1,7 @@
1
1
  import { __awaiter } from "tslib";
2
2
  import { expect, test } from "@playwright/test";
3
- import { getNextBehavior, getIndexOfNextMatchingTag, pathResolver, transformInnerHTML, getExpressionChain, extractPathsFromChainedExpression, } from "./utilities.js";
3
+ import { refPropertyName } from "./schema.js";
4
+ import { getNextBehavior, getIndexOfNextMatchingTag, pathResolver, transformInnerHTML, getExpressionChain, extractPathsFromChainedExpression, getChildrenMap, findDef, } from "./utilities.js";
4
5
  test.describe("utilities", () => __awaiter(void 0, void 0, void 0, function* () {
5
6
  test.describe("content", () => __awaiter(void 0, void 0, void 0, function* () {
6
7
  test("get the next content binding", () => __awaiter(void 0, void 0, void 0, function* () {
@@ -359,4 +360,74 @@ test.describe("utilities", () => __awaiter(void 0, void 0, void 0, function* ()
359
360
  expect(paths.has("app.user.profile.settings.theme")).toBe(true);
360
361
  }));
361
362
  }));
363
+ test.describe("getChildrenMap", () => __awaiter(void 0, void 0, void 0, function* () {
364
+ test("should get a ChildrenMap if an attribute is part of a custom element", () => __awaiter(void 0, void 0, void 0, function* () {
365
+ const childrenMap = getChildrenMap(`<template><my-element foo="`);
366
+ expect(childrenMap).not.toBeNull();
367
+ expect(childrenMap === null || childrenMap === void 0 ? void 0 : childrenMap.attributeName).toEqual("foo");
368
+ expect(childrenMap === null || childrenMap === void 0 ? void 0 : childrenMap.customElementName).toEqual("my-element");
369
+ }));
370
+ test("should not get a ChildrenMap if an attribute is part of a non-custom element", () => __awaiter(void 0, void 0, void 0, function* () {
371
+ const childrenMap = getChildrenMap(`<template><button foo="`);
372
+ expect(childrenMap).toBeNull();
373
+ }));
374
+ test("should remove any aspected attributes from an attribute name", () => __awaiter(void 0, void 0, void 0, function* () {
375
+ const childrenMap = getChildrenMap(`<template><my-element :foo="`);
376
+ expect(childrenMap).not.toBeNull();
377
+ expect(childrenMap === null || childrenMap === void 0 ? void 0 : childrenMap.attributeName).toEqual("foo");
378
+ expect(childrenMap === null || childrenMap === void 0 ? void 0 : childrenMap.customElementName).toEqual("my-element");
379
+ }));
380
+ test("should not get a ChildreMap if the previous string indicates the binding was not an attribute", () => __awaiter(void 0, void 0, void 0, function* () {
381
+ const childrenMap = getChildrenMap(`<template><my-element>`);
382
+ expect(childrenMap).toBeNull();
383
+ }));
384
+ test("should get a ChildrenMap if there are multiple attributes are listed before this attribute", () => __awaiter(void 0, void 0, void 0, function* () {
385
+ const childrenMap = getChildrenMap(`<template><my-element foo="" bar="" bat="`);
386
+ expect(childrenMap).not.toBeNull();
387
+ expect(childrenMap === null || childrenMap === void 0 ? void 0 : childrenMap.attributeName).toEqual("bat");
388
+ expect(childrenMap === null || childrenMap === void 0 ? void 0 : childrenMap.customElementName).toEqual("my-element");
389
+ }));
390
+ }));
391
+ test.describe("schema functions", () => __awaiter(void 0, void 0, void 0, function* () {
392
+ test.describe("findDef", () => __awaiter(void 0, void 0, void 0, function* () {
393
+ test("should resolve from the root of a schema", () => __awaiter(void 0, void 0, void 0, function* () {
394
+ expect(findDef({
395
+ [refPropertyName]: "#/$defs/MyType"
396
+ })).toEqual("MyType");
397
+ }));
398
+ test("should resolve as null from an anyOf array containing a reference to another component", () => __awaiter(void 0, void 0, void 0, function* () {
399
+ expect(findDef({
400
+ anyOf: [
401
+ {
402
+ [refPropertyName]: "https://fast.design/schemas/test-element/c.json"
403
+ }
404
+ ]
405
+ })).toEqual(null);
406
+ }));
407
+ test("should resolve from an anyOf array containing a reference to a $def", () => __awaiter(void 0, void 0, void 0, function* () {
408
+ expect(findDef({
409
+ anyOf: [
410
+ {
411
+ [refPropertyName]: "#/$defs/MyType"
412
+ }
413
+ ]
414
+ })).toEqual("MyType");
415
+ }));
416
+ test("should resolve from an anyOf array containing a reference to another component and a reference to a $def", () => __awaiter(void 0, void 0, void 0, function* () {
417
+ expect(findDef({
418
+ anyOf: [
419
+ {
420
+ [refPropertyName]: "https://fast.design/schemas/test-element/c.json"
421
+ },
422
+ {
423
+ [refPropertyName]: "#/$defs/MyType"
424
+ }
425
+ ]
426
+ })).toEqual("MyType");
427
+ }));
428
+ test("should resolve as null if not found", () => __awaiter(void 0, void 0, void 0, function* () {
429
+ expect(findDef({})).toEqual(null);
430
+ }));
431
+ }));
432
+ }));
362
433
  }));