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

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
@@ -666,16 +706,19 @@ function assignProxyToItemsInArray(item, originalItem, schema, rootSchema) {
666
706
  function assignProxyToItemsInObject(target, rootProperty, data, schema, rootSchema) {
667
707
  var _a;
668
708
  const type = getDataType(data);
709
+ const schemaProperties = getSchemaProperties(schema);
669
710
  let proxiedData = data;
670
- if (type === "object" && (schema === null || schema === void 0 ? void 0 : schema.properties)) {
711
+ if (type === "object" && schemaProperties) {
671
712
  // 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);
713
+ Object.keys(schemaProperties).forEach(property => {
714
+ if (proxiedData[property] && schema && schemaProperties) {
715
+ proxiedData[property] = assignProxyToItemsInObject(target, rootProperty, proxiedData[property], schemaProperties[property], rootSchema);
675
716
  }
676
717
  });
677
718
  // assign a Proxy to the object
678
719
  proxiedData = assignProxy(schema, rootSchema, target, rootProperty, data);
720
+ // Add this target to the object's target list
721
+ addTargetToObject(proxiedData, target, rootProperty);
679
722
  }
680
723
  else if (type === "array") {
681
724
  const context = getDefFromRef(schema.items[refPropertyName]);
@@ -686,6 +729,37 @@ function assignProxyToItemsInObject(target, rootProperty, data, schema, rootSche
686
729
  }
687
730
  return proxiedData;
688
731
  }
732
+ /**
733
+ * Add a target to an object's target list
734
+ * @param object - The object to associate with the target
735
+ * @param target - The target custom element
736
+ * @param rootProperty - The root property name
737
+ */
738
+ function addTargetToObject(object, target, rootProperty) {
739
+ if (!objectTargetsMap.has(object)) {
740
+ objectTargetsMap.set(object, []);
741
+ }
742
+ const targets = objectTargetsMap.get(object);
743
+ targets.push({ target, rootProperty });
744
+ }
745
+ /**
746
+ * Get all targets for an object
747
+ * @param object - The object to get targets for
748
+ * @returns Array of target info objects
749
+ */
750
+ function getTargetsForObject(object) {
751
+ return objectTargetsMap.get(object) || [];
752
+ }
753
+ /**
754
+ * Notify any observables mapped to the object
755
+ * @param targetObject The object that is mapped to a target and rootProperty
756
+ */
757
+ function notifyObservables(targetObject) {
758
+ getTargetsForObject(targetObject).forEach((targetItem) => {
759
+ // Trigger notification for property changes
760
+ Observable.notify(targetItem.target, targetItem.rootProperty);
761
+ });
762
+ }
689
763
  /**
690
764
  * Assign a proxy to an object
691
765
  * @param schema - The current schema
@@ -698,11 +772,10 @@ function assignProxyToItemsInObject(target, rootProperty, data, schema, rootSche
698
772
  export function assignProxy(schema, rootSchema, target, rootProperty, object) {
699
773
  if (object.$isProxy === undefined) {
700
774
  // Create a proxy for the object that triggers Observable.notify on mutations
701
- return new Proxy(object, {
775
+ const proxy = new Proxy(object, {
702
776
  set: (obj, prop, value) => {
703
777
  obj[prop] = assignObservables(schema, rootSchema, value, target, rootProperty);
704
- // Trigger notification for property changes
705
- Observable.notify(target, rootProperty);
778
+ notifyObservables(proxy);
706
779
  return true;
707
780
  },
708
781
  get: (target, key) => {
@@ -714,13 +787,13 @@ export function assignProxy(schema, rootSchema, target, rootProperty, object) {
714
787
  deleteProperty: (obj, prop) => {
715
788
  if (prop in obj) {
716
789
  delete obj[prop];
717
- // Trigger notification for property deletion
718
- Observable.notify(target, rootProperty);
790
+ notifyObservables(proxy);
719
791
  return true;
720
792
  }
721
793
  return false;
722
794
  },
723
795
  });
796
+ return proxy;
724
797
  }
725
798
  return object;
726
799
  }
@@ -737,3 +810,51 @@ export function getRootPropertyName(rootPropertyName, path, context, type) {
737
810
  ? path.split(".")[0]
738
811
  : rootPropertyName;
739
812
  }
813
+ /**
814
+ * Get details of bindings to the attributes of child custom elements
815
+ * @param previousString - The previous string before the binding
816
+ * @returns null, or a custom element name and attribute name
817
+ */
818
+ export function getChildrenMap(previousString) {
819
+ if (typeof previousString === "string" &&
820
+ isAttribute(previousString, previousString.length)) {
821
+ const customElementName = getAttributesCustomElementName(previousString);
822
+ if (customElementName) {
823
+ return {
824
+ customElementName,
825
+ attributeName: getAttributeName(previousString),
826
+ };
827
+ }
828
+ }
829
+ return null;
830
+ }
831
+ /**
832
+ * Get the HTML element that is passing the attribute binding
833
+ * @param previousString - The previous string before the binding
834
+ * @returns null if this is not a custom element, or the custom element that is passing the binding as an attribute
835
+ */
836
+ function getAttributesCustomElementName(previousString) {
837
+ const indexOfElementTagStart = previousString.lastIndexOf("<") + 1;
838
+ const indexOfElementTagEnd = previousString.slice(indexOfElementTagStart).indexOf(" ") +
839
+ indexOfElementTagStart;
840
+ const elementName = previousString.slice(indexOfElementTagStart, indexOfElementTagEnd);
841
+ if (elementName.includes("-")) {
842
+ return elementName;
843
+ }
844
+ return null;
845
+ }
846
+ /**
847
+ * Gets a non-aspected attribute name
848
+ * @param previousString - The previous string before the binding
849
+ * @returns The attribute name with any aspects (:, ?, @) removed
850
+ */
851
+ function getAttributeName(previousString) {
852
+ const indexOfAttributeStart = previousString.lastIndexOf(" ") + 1;
853
+ const indexOfAttributeEnd = previousString.slice(indexOfAttributeStart).indexOf("=") + indexOfAttributeStart;
854
+ const attributeName = previousString.slice(indexOfAttributeStart, indexOfAttributeEnd);
855
+ const potentialAspect = attributeName.charAt(0);
856
+ if (potentialAspect === ":" || potentialAspect === "@" || potentialAspect === "?") {
857
+ return attributeName.slice(1);
858
+ }
859
+ return attributeName;
860
+ }
@@ -1,6 +1,6 @@
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 { getNextBehavior, getIndexOfNextMatchingTag, pathResolver, transformInnerHTML, getExpressionChain, extractPathsFromChainedExpression, getChildrenMap, } 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* () {
@@ -359,4 +359,32 @@ test.describe("utilities", () => __awaiter(void 0, void 0, void 0, function* ()
359
359
  expect(paths.has("app.user.profile.settings.theme")).toBe(true);
360
360
  }));
361
361
  }));
362
+ test.describe("getChildrenMap", () => __awaiter(void 0, void 0, void 0, function* () {
363
+ test("should get a ChildrenMap if an attribute is part of a custom element", () => __awaiter(void 0, void 0, void 0, function* () {
364
+ const childrenMap = getChildrenMap(`<template><my-element foo="`);
365
+ expect(childrenMap).not.toBeNull();
366
+ expect(childrenMap === null || childrenMap === void 0 ? void 0 : childrenMap.attributeName).toEqual("foo");
367
+ expect(childrenMap === null || childrenMap === void 0 ? void 0 : childrenMap.customElementName).toEqual("my-element");
368
+ }));
369
+ test("should not get a ChildrenMap if an attribute is part of a non-custom element", () => __awaiter(void 0, void 0, void 0, function* () {
370
+ const childrenMap = getChildrenMap(`<template><button foo="`);
371
+ expect(childrenMap).toBeNull();
372
+ }));
373
+ test("should remove any aspected attributes from an attribute name", () => __awaiter(void 0, void 0, void 0, function* () {
374
+ const childrenMap = getChildrenMap(`<template><my-element :foo="`);
375
+ expect(childrenMap).not.toBeNull();
376
+ expect(childrenMap === null || childrenMap === void 0 ? void 0 : childrenMap.attributeName).toEqual("foo");
377
+ expect(childrenMap === null || childrenMap === void 0 ? void 0 : childrenMap.customElementName).toEqual("my-element");
378
+ }));
379
+ 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* () {
380
+ const childrenMap = getChildrenMap(`<template><my-element>`);
381
+ expect(childrenMap).toBeNull();
382
+ }));
383
+ test("should get a ChildrenMap if there are multiple attributes are listed before this attribute", () => __awaiter(void 0, void 0, void 0, function* () {
384
+ const childrenMap = getChildrenMap(`<template><my-element foo="" bar="" bat="`);
385
+ expect(childrenMap).not.toBeNull();
386
+ expect(childrenMap === null || childrenMap === void 0 ? void 0 : childrenMap.attributeName).toEqual("bat");
387
+ expect(childrenMap === null || childrenMap === void 0 ? void 0 : childrenMap.customElementName).toEqual("my-element");
388
+ }));
389
+ }));
362
390
  }));