@microsoft/fast-html 1.0.0-alpha.4 → 1.0.0-alpha.41
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 +266 -19
- package/dist/dts/components/element.d.ts +10 -0
- package/dist/dts/components/index.d.ts +3 -1
- package/dist/dts/components/observer-map.d.ts +27 -0
- package/dist/dts/components/schema.d.ts +144 -0
- package/dist/dts/components/template.d.ts +83 -7
- package/dist/dts/components/utilities.d.ts +128 -37
- package/dist/dts/index.d.ts +1 -1
- package/dist/dts/tsdoc-metadata.json +1 -1
- package/dist/esm/components/element.js +73 -0
- package/dist/esm/components/index.js +3 -1
- package/dist/esm/components/observer-map.js +68 -0
- package/dist/esm/components/observer-map.spec.js +39 -0
- package/dist/esm/components/schema.js +250 -0
- package/dist/esm/components/schema.spec.js +484 -0
- package/dist/esm/components/template.js +242 -213
- package/dist/esm/components/utilities.js +1008 -64
- package/dist/esm/components/utilities.spec.js +522 -93
- package/dist/esm/index.js +1 -1
- package/dist/fast-html.api.json +350 -1
- package/dist/fast-html.d.ts +283 -6
- package/dist/fast-html.untrimmed.d.ts +283 -6
- package/package.json +27 -38
- package/rules/attribute-directives.yml +38 -0
- package/rules/call-expression-with-event-argument.yml +41 -0
- package/rules/member-expression.yml +33 -0
- package/rules/tag-function-to-template-literal.yml +16 -0
- package/dist/dts/fixtures/binding/binding.spec.d.ts +0 -1
- package/dist/dts/fixtures/binding/main.d.ts +0 -1
- package/dist/dts/fixtures/children/children.spec.d.ts +0 -1
- package/dist/dts/fixtures/children/main.d.ts +0 -1
- package/dist/dts/fixtures/dot-syntax/dot-syntax.spec.d.ts +0 -1
- package/dist/dts/fixtures/dot-syntax/main.d.ts +0 -1
- package/dist/dts/fixtures/event/event.spec.d.ts +0 -1
- package/dist/dts/fixtures/event/main.d.ts +0 -1
- package/dist/dts/fixtures/partial/main.d.ts +0 -1
- package/dist/dts/fixtures/partial/partial.spec.d.ts +0 -1
- package/dist/dts/fixtures/ref/main.d.ts +0 -1
- package/dist/dts/fixtures/ref/ref.spec.d.ts +0 -1
- package/dist/dts/fixtures/repeat/main.d.ts +0 -1
- package/dist/dts/fixtures/repeat/repeat.spec.d.ts +0 -1
- package/dist/dts/fixtures/slotted/main.d.ts +0 -1
- package/dist/dts/fixtures/slotted/slotted.spec.d.ts +0 -1
- package/dist/dts/fixtures/when/main.d.ts +0 -1
- package/dist/dts/fixtures/when/when.spec.d.ts +0 -1
- package/dist/esm/fixtures/attribute/attribute.spec.js +0 -23
- package/dist/esm/fixtures/attribute/main.js +0 -19
- package/dist/esm/fixtures/binding/binding.spec.js +0 -17
- package/dist/esm/fixtures/binding/main.js +0 -19
- package/dist/esm/fixtures/children/children.spec.js +0 -33
- package/dist/esm/fixtures/children/main.js +0 -24
- package/dist/esm/fixtures/dot-syntax/dot-syntax.spec.js +0 -9
- package/dist/esm/fixtures/dot-syntax/main.js +0 -16
- package/dist/esm/fixtures/event/event.spec.js +0 -12
- package/dist/esm/fixtures/event/main.js +0 -16
- package/dist/esm/fixtures/partial/main.js +0 -31
- package/dist/esm/fixtures/partial/partial.spec.js +0 -14
- package/dist/esm/fixtures/ref/main.js +0 -14
- package/dist/esm/fixtures/ref/ref.spec.js +0 -13
- package/dist/esm/fixtures/repeat/main.js +0 -19
- package/dist/esm/fixtures/repeat/repeat.spec.js +0 -26
- package/dist/esm/fixtures/slotted/main.js +0 -22
- package/dist/esm/fixtures/slotted/slotted.spec.js +0 -25
- package/dist/esm/fixtures/when/main.js +0 -146
- package/dist/esm/fixtures/when/when.spec.js +0 -82
- package/dist/esm/tsconfig.tsbuildinfo +0 -1
- /package/dist/dts/{fixtures/attribute/attribute.spec.d.ts → components/observer-map.spec.d.ts} +0 -0
- /package/dist/dts/{fixtures/attribute/main.d.ts → components/schema.spec.d.ts} +0 -0
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import { type JSONSchema, type JSONSchemaDefinition, Schema } from "./schema.js";
|
|
1
2
|
type BehaviorType = "dataBinding" | "templateDirective";
|
|
2
|
-
type TemplateDirective = "when" | "repeat"
|
|
3
|
+
type TemplateDirective = "when" | "repeat";
|
|
3
4
|
export type AttributeDirective = "children" | "slotted" | "ref";
|
|
5
|
+
type DataBindingBindingType = "client" | "default" | "unescaped";
|
|
4
6
|
interface BehaviorConfig {
|
|
5
7
|
type: BehaviorType;
|
|
6
8
|
}
|
|
9
|
+
export type PathType = "access" | "default" | "event" | "repeat";
|
|
7
10
|
export interface ContentDataBindingBehaviorConfig extends BaseDataBindingBehaviorConfig {
|
|
8
11
|
subtype: "content";
|
|
9
12
|
}
|
|
@@ -18,6 +21,7 @@ export interface AttributeDirectiveBindingBehaviorConfig extends BaseDataBinding
|
|
|
18
21
|
export type DataBindingBehaviorConfig = ContentDataBindingBehaviorConfig | AttributeDataBindingBehaviorConfig | AttributeDirectiveBindingBehaviorConfig;
|
|
19
22
|
export interface BaseDataBindingBehaviorConfig extends BehaviorConfig {
|
|
20
23
|
type: "dataBinding";
|
|
24
|
+
bindingType: DataBindingBindingType;
|
|
21
25
|
openingStartIndex: number;
|
|
22
26
|
openingEndIndex: number;
|
|
23
27
|
closingStartIndex: number;
|
|
@@ -32,11 +36,30 @@ export interface TemplateDirectiveBehaviorConfig extends BehaviorConfig {
|
|
|
32
36
|
closingTagStartIndex: number;
|
|
33
37
|
closingTagEndIndex: number;
|
|
34
38
|
}
|
|
35
|
-
interface
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
endIndex: number;
|
|
39
|
+
export interface ChildrenMap {
|
|
40
|
+
customElementName: string;
|
|
41
|
+
attributeName: string;
|
|
39
42
|
}
|
|
43
|
+
export declare const contextPrefix: string;
|
|
44
|
+
export declare const contextPrefixDot: string;
|
|
45
|
+
declare const LogicalOperator: {
|
|
46
|
+
AND: string;
|
|
47
|
+
OR: string;
|
|
48
|
+
};
|
|
49
|
+
type LogicalOperator = (typeof LogicalOperator)[keyof typeof LogicalOperator];
|
|
50
|
+
declare const Operator: {
|
|
51
|
+
readonly ACCESS: "access";
|
|
52
|
+
readonly EQUALS: "==";
|
|
53
|
+
readonly GREATER_THAN: ">";
|
|
54
|
+
readonly GREATER_THAN_OR_EQUALS: ">=";
|
|
55
|
+
readonly LESS_THAN: "<";
|
|
56
|
+
readonly LESS_THAN_OR_EQUALS: "<=";
|
|
57
|
+
readonly NOT: "!";
|
|
58
|
+
readonly NOT_EQUALS: "!=";
|
|
59
|
+
readonly AND: string;
|
|
60
|
+
readonly OR: string;
|
|
61
|
+
};
|
|
62
|
+
type Operator = (typeof Operator)[keyof typeof Operator];
|
|
40
63
|
/**
|
|
41
64
|
* Get the index of the next matching tag
|
|
42
65
|
* @param openingTagStartSlice - The slice starting from the opening tag
|
|
@@ -51,19 +74,7 @@ export declare function getIndexOfNextMatchingTag(openingTagStartSlice: string,
|
|
|
51
74
|
* @param innerHTML - The innerHTML string to evaluate
|
|
52
75
|
* @returns DataBindingBehaviorConfig | DirectiveBehaviorConfig | null - A configuration object or null
|
|
53
76
|
*/
|
|
54
|
-
export declare function getNextBehavior(innerHTML: string): DataBindingBehaviorConfig | TemplateDirectiveBehaviorConfig | null;
|
|
55
|
-
/**
|
|
56
|
-
* Gets all the partials with their IDs
|
|
57
|
-
* @param innerHTML - The innerHTML string to evaluate
|
|
58
|
-
* @param offset - The index offset from the innerHTML
|
|
59
|
-
* @param partials - The partials found
|
|
60
|
-
* @returns {[key: string]: PartialTemplateConfig}
|
|
61
|
-
*/
|
|
62
|
-
export declare function getAllPartials(innerHTML: string, offset?: number, partials?: {
|
|
63
|
-
[key: string]: PartialTemplateConfig;
|
|
64
|
-
}): {
|
|
65
|
-
[key: string]: PartialTemplateConfig;
|
|
66
|
-
};
|
|
77
|
+
export declare function getNextBehavior(innerHTML: string, offset?: number): DataBindingBehaviorConfig | TemplateDirectiveBehaviorConfig | null;
|
|
67
78
|
/**
|
|
68
79
|
* Create a function to resolve a value from an object using a path with dot syntax.
|
|
69
80
|
* e.g. "foo.bar"
|
|
@@ -71,32 +82,112 @@ export declare function getAllPartials(innerHTML: string, offset?: number, parti
|
|
|
71
82
|
* @param self - Where the first item in the path path refers to the item itself (used by repeat).
|
|
72
83
|
* @returns A function to access the value from a given path.
|
|
73
84
|
*/
|
|
74
|
-
export declare function pathResolver(path: string,
|
|
85
|
+
export declare function pathResolver(path: string, contextPath: string | null, level: number, rootSchema: JSONSchema): (accessibleObject: any, context: any) => any;
|
|
86
|
+
export declare function bindingResolver(previousString: string | null, rootPropertyName: string | null, path: string, parentContext: string | null, type: PathType, schema: Schema, currentContext: string | null, level: number): (accessibleObject: any, context: any) => any;
|
|
87
|
+
export declare function expressionResolver(rootPropertyName: string | null, expression: ChainedExpression, parentContext: string | null, level: number, schema: Schema): (accessibleObject: any, context: any) => any;
|
|
75
88
|
/**
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
* - not (!)
|
|
80
|
-
* - equals (==)
|
|
81
|
-
* - not equal (!=)
|
|
82
|
-
* - greater than or equal (>=)
|
|
83
|
-
* - greater than (>)
|
|
84
|
-
* - less than or equal (<=)
|
|
85
|
-
* - less than (<)
|
|
86
|
-
* - or (||)
|
|
87
|
-
* - and (&&) and the HTML character entity (&&)
|
|
89
|
+
* Extracts all paths from a ChainedExpression, including nested expressions
|
|
90
|
+
* @param chainedExpression - The chained expression to extract paths from
|
|
91
|
+
* @returns A Set containing all unique paths found in the expression chain
|
|
88
92
|
*/
|
|
89
|
-
|
|
90
|
-
interface
|
|
93
|
+
export declare function extractPathsFromChainedExpression(chainedExpression: ChainedExpression): Set<string>;
|
|
94
|
+
interface Expression {
|
|
91
95
|
operator: Operator;
|
|
92
96
|
left: string;
|
|
97
|
+
leftIsValue: boolean | null;
|
|
93
98
|
right: string | boolean | number | null;
|
|
94
99
|
rightIsValue: boolean | null;
|
|
95
100
|
}
|
|
101
|
+
export interface ChainedExpression {
|
|
102
|
+
operator?: LogicalOperator;
|
|
103
|
+
expression: Expression;
|
|
104
|
+
next?: ChainedExpression;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Gets the expression chain as a configuration object
|
|
108
|
+
* @param value - The binding string value
|
|
109
|
+
* @returns - A configuration object containing information about the expression
|
|
110
|
+
*/
|
|
111
|
+
export declare function getExpressionChain(value: string): ChainedExpression | void;
|
|
112
|
+
/**
|
|
113
|
+
* This is the transform utility for rationalizing declarative HTML syntax
|
|
114
|
+
* with bindings in the ViewTemplate
|
|
115
|
+
* @param innerHTML The innerHTML to transform
|
|
116
|
+
* @param index The index to start the current slice of HTML to evaluate
|
|
117
|
+
*/
|
|
118
|
+
export declare function transformInnerHTML(innerHTML: string, index?: number): string;
|
|
119
|
+
/**
|
|
120
|
+
* Resolves f-when
|
|
121
|
+
* @param self - Where the first item in the path path refers to the item itself (used by repeat).
|
|
122
|
+
* @param chainedExpression - The chained expression which includes the expression and the next expression
|
|
123
|
+
* if there is another in the chain
|
|
124
|
+
* @returns - A binding that resolves the chained expression logic
|
|
125
|
+
*/
|
|
126
|
+
export declare function resolveWhen(rootPropertyName: string | null, expression: ChainedExpression, parentContext: string | null, level: number, schema: Schema): (x: boolean, c: any) => any;
|
|
127
|
+
/**
|
|
128
|
+
* Find a definition
|
|
129
|
+
* This may exist as a $ref at the root or as a $ref in any anyOf or not at all
|
|
130
|
+
* if the Observer Map has not been enabled on a child component
|
|
131
|
+
* @param schema - The JSON schema to find the ref in
|
|
132
|
+
* @returns The definition or null
|
|
133
|
+
*/
|
|
134
|
+
export declare function findDef(schema: JSONSchema | JSONSchemaDefinition): string | null;
|
|
135
|
+
/**
|
|
136
|
+
* Assign observables to data
|
|
137
|
+
* @param schema - The schema
|
|
138
|
+
* @param rootSchema - The root schema mapping to the root property
|
|
139
|
+
* @param data - The data
|
|
140
|
+
* @param target - The target custom element
|
|
141
|
+
* @param rootProperty - The root property
|
|
142
|
+
* @returns
|
|
143
|
+
*/
|
|
144
|
+
export declare function assignObservables(schema: JSONSchema | JSONSchemaDefinition, rootSchema: JSONSchema, data: any, target: any, rootProperty: string): typeof Proxy;
|
|
145
|
+
/**
|
|
146
|
+
* Assign a proxy to an object
|
|
147
|
+
* @param schema - The current schema
|
|
148
|
+
* @param rootSchema - The root schema for the root property
|
|
149
|
+
* @param target - The target custom element
|
|
150
|
+
* @param rootProperty - The root property
|
|
151
|
+
* @param object - The object to assign the proxy to
|
|
152
|
+
* @returns Proxy object
|
|
153
|
+
*/
|
|
154
|
+
export declare function assignProxy(schema: JSONSchema | JSONSchemaDefinition, rootSchema: JSONSchema, target: any, rootProperty: string, object: any): typeof Proxy;
|
|
155
|
+
/**
|
|
156
|
+
* Get the root property name
|
|
157
|
+
* @param rootPropertyName - The root property
|
|
158
|
+
* @param path - The dot syntax path
|
|
159
|
+
* @param context - The context created by a repeat
|
|
160
|
+
* @param type - The type of path binding
|
|
161
|
+
* @returns
|
|
162
|
+
*/
|
|
163
|
+
export declare function getRootPropertyName(rootPropertyName: string | null, path: string, context: null | string, type: PathType): string | null;
|
|
164
|
+
/**
|
|
165
|
+
* Get details of bindings to the attributes of child custom elements
|
|
166
|
+
* @param previousString - The previous string before the binding
|
|
167
|
+
* @returns null, or a custom element name and attribute name
|
|
168
|
+
*/
|
|
169
|
+
export declare function getChildrenMap(previousString: string | null): ChildrenMap | null;
|
|
170
|
+
/**
|
|
171
|
+
* Deeply compares two objects for equality.
|
|
172
|
+
*
|
|
173
|
+
* @param obj1 - First object to compare
|
|
174
|
+
* @param obj2 - Second object to compare
|
|
175
|
+
* @returns True if the objects are deeply equal, false otherwise
|
|
176
|
+
*/
|
|
177
|
+
export declare function deepEqual(obj1: any, obj2: any): boolean;
|
|
96
178
|
/**
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
* @
|
|
179
|
+
* Checks if a value is a plain object (not an array, null, or other type).
|
|
180
|
+
*
|
|
181
|
+
* @param value - The value to check
|
|
182
|
+
* @returns True if the value is a plain object, false otherwise
|
|
183
|
+
*/
|
|
184
|
+
export declare function isPlainObject(value: any): value is Record<string, any>;
|
|
185
|
+
/**
|
|
186
|
+
* Deeply merges the source object into the target object.
|
|
187
|
+
*
|
|
188
|
+
* @param target - The target object to merge into
|
|
189
|
+
* @param source - The source object to merge from
|
|
190
|
+
* @returns boolean indicating whether changes were made
|
|
100
191
|
*/
|
|
101
|
-
export declare function
|
|
192
|
+
export declare function deepMerge(target: any, source: any): boolean;
|
|
102
193
|
export {};
|
package/dist/dts/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { TemplateElement } from "./components/index.js";
|
|
1
|
+
export { RenderableFASTElement, TemplateElement, ObserverMap, } from "./components/index.js";
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { attr } from "@microsoft/fast-element";
|
|
2
|
+
import { deferHydrationAttribute } from "@microsoft/fast-element/element-hydration.js";
|
|
3
|
+
import { composedParent } from "@microsoft/fast-element/utilities.js";
|
|
4
|
+
/**
|
|
5
|
+
* Waits for the `defer-hydration` attribute to be removed from the target element.
|
|
6
|
+
*
|
|
7
|
+
* @param target - The target element to wait for attribute removal
|
|
8
|
+
*/
|
|
9
|
+
function waitForAttributeRemoval(target) {
|
|
10
|
+
if (!target.hasAttribute(deferHydrationAttribute)) {
|
|
11
|
+
return Promise.resolve();
|
|
12
|
+
}
|
|
13
|
+
return new Promise(resolve => {
|
|
14
|
+
const observer = new MutationObserver(() => {
|
|
15
|
+
if (!target.hasAttribute(deferHydrationAttribute)) {
|
|
16
|
+
observer.disconnect();
|
|
17
|
+
resolve();
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
observer.observe(target, { attributeFilter: [deferHydrationAttribute] });
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Waits until all ancestor elements no longer have the `defer-hydration` attribute.
|
|
25
|
+
*
|
|
26
|
+
* @param element - The element to wait for ancestor hydration
|
|
27
|
+
*/
|
|
28
|
+
async function waitForAncestorHydration(element) {
|
|
29
|
+
let ancestor = composedParent(element);
|
|
30
|
+
while (ancestor) {
|
|
31
|
+
if (ancestor instanceof HTMLElement &&
|
|
32
|
+
ancestor.hasAttribute(deferHydrationAttribute)) {
|
|
33
|
+
await waitForAttributeRemoval(ancestor);
|
|
34
|
+
}
|
|
35
|
+
ancestor = composedParent(ancestor);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* A mixin function that extends a base class with additional functionality for
|
|
40
|
+
* rendering and hydration.
|
|
41
|
+
*
|
|
42
|
+
* @param BaseCtor - The base class to extend.
|
|
43
|
+
* @returns A new class that extends the provided base class with additional functionality for rendering and hydration.
|
|
44
|
+
* @public
|
|
45
|
+
*/
|
|
46
|
+
export function RenderableFASTElement(BaseCtor) {
|
|
47
|
+
const C = class extends BaseCtor {
|
|
48
|
+
constructor() {
|
|
49
|
+
super(...arguments);
|
|
50
|
+
this.deferHydration = true;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Prepares the element for hydration by calling the user-defined prepare method
|
|
54
|
+
* and waiting for all ancestor elements to be hydrated.
|
|
55
|
+
*
|
|
56
|
+
* @returns A promise that resolves when the element is ready for hydration.
|
|
57
|
+
*/
|
|
58
|
+
async _prepare() {
|
|
59
|
+
if (this.prepare) {
|
|
60
|
+
await this.prepare();
|
|
61
|
+
this.prepare = undefined;
|
|
62
|
+
}
|
|
63
|
+
await waitForAncestorHydration(this);
|
|
64
|
+
this.deferHydration = false;
|
|
65
|
+
}
|
|
66
|
+
connectedCallback() {
|
|
67
|
+
this._prepare();
|
|
68
|
+
super.connectedCallback();
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
attr({ mode: "boolean", attribute: deferHydrationAttribute })(C.prototype, "deferHydration");
|
|
72
|
+
return C;
|
|
73
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Observable } from "@microsoft/fast-element/observable.js";
|
|
2
|
+
import { assignObservables, deepMerge } from "./utilities.js";
|
|
3
|
+
/**
|
|
4
|
+
* ObserverMap provides functionality for caching binding paths, extracting root properties,
|
|
5
|
+
* and defining observable properties on class prototypes
|
|
6
|
+
*/
|
|
7
|
+
export class ObserverMap {
|
|
8
|
+
constructor(classPrototype, schema) {
|
|
9
|
+
/**
|
|
10
|
+
* Creates a property change handler function for observable properties
|
|
11
|
+
* This handler is called when an observable property transitions from undefined to a defined value
|
|
12
|
+
* @param propertyName - The name of the property for which to create the change handler
|
|
13
|
+
* @param existingChangedMethod - Optional existing changed method to call after the instance resolver logic
|
|
14
|
+
* @returns A function that handles property changes and sets up proxies for object values
|
|
15
|
+
*/
|
|
16
|
+
this.defineChanged = (propertyName, existingChangedMethod) => {
|
|
17
|
+
const getAndAssignObservablesAlias = this.getAndAssignObservables;
|
|
18
|
+
const schema = this.schema;
|
|
19
|
+
function instanceResolverChanged(prev, next) {
|
|
20
|
+
const isObjectAssignment = next !== null && typeof next === "object";
|
|
21
|
+
const isManagedArray = Array.isArray(next) && (next === null || next === void 0 ? void 0 : next.$fastController);
|
|
22
|
+
const shouldAssignProxy = isObjectAssignment && !(next === null || next === void 0 ? void 0 : next.$isProxy) && !isManagedArray;
|
|
23
|
+
const hasExistingProxy = prev !== null && typeof prev === "object" && prev.$isProxy;
|
|
24
|
+
if (shouldAssignProxy) {
|
|
25
|
+
if (hasExistingProxy) {
|
|
26
|
+
deepMerge(prev, next);
|
|
27
|
+
this[`_${propertyName}`] = prev;
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
this[propertyName] = getAndAssignObservablesAlias(this, propertyName, next, schema);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
else if (!isObjectAssignment) {
|
|
34
|
+
this[propertyName] = next;
|
|
35
|
+
}
|
|
36
|
+
existingChangedMethod === null || existingChangedMethod === void 0 ? void 0 : existingChangedMethod.call(this, prev, next);
|
|
37
|
+
}
|
|
38
|
+
return instanceResolverChanged;
|
|
39
|
+
};
|
|
40
|
+
this.classPrototype = classPrototype;
|
|
41
|
+
this.schema = schema;
|
|
42
|
+
}
|
|
43
|
+
defineProperties() {
|
|
44
|
+
const propertyNames = this.schema.getRootProperties();
|
|
45
|
+
const existingAccessors = Observable.getAccessors(this.classPrototype);
|
|
46
|
+
for (const propertyName of propertyNames) {
|
|
47
|
+
// Skip if property already has an accessor (from `@attr` or `@observable` decorator)
|
|
48
|
+
if (!existingAccessors.some(accessor => accessor.name === propertyName)) {
|
|
49
|
+
Observable.defineProperty(this.classPrototype, propertyName);
|
|
50
|
+
}
|
|
51
|
+
const changedMethodName = `${propertyName}Changed`;
|
|
52
|
+
const existingChangedMethod = this.classPrototype[changedMethodName];
|
|
53
|
+
this.classPrototype[changedMethodName] = this.defineChanged(propertyName, existingChangedMethod);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Creates a proxy for an object that intercepts property mutations and triggers Observable notifications
|
|
58
|
+
* @param target - The target instance that owns the root property
|
|
59
|
+
* @param rootProperty - The name of the root property for notification purposes
|
|
60
|
+
* @param object - The object to wrap with a proxy
|
|
61
|
+
* @returns A proxy that triggers notifications on property mutations
|
|
62
|
+
*/
|
|
63
|
+
getAndAssignObservables(target, rootProperty, object, schema) {
|
|
64
|
+
let proxiedObject = object;
|
|
65
|
+
proxiedObject = assignObservables(schema.getSchema(rootProperty), schema.getSchema(rootProperty), proxiedObject, target, rootProperty);
|
|
66
|
+
return proxiedObject;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { expect, test } from "@playwright/test";
|
|
2
|
+
import { ObserverMap } from "./observer-map.js";
|
|
3
|
+
import { Schema, defsPropertyName } from "./schema.js";
|
|
4
|
+
const testElementName = "test-class";
|
|
5
|
+
test.describe("ObserverMap", async () => {
|
|
6
|
+
let observerMap;
|
|
7
|
+
let schema;
|
|
8
|
+
class TestClass {
|
|
9
|
+
}
|
|
10
|
+
test.beforeEach(async () => {
|
|
11
|
+
schema = new Schema(testElementName);
|
|
12
|
+
// Use TestClass.prototype so instances will have the observable properties
|
|
13
|
+
observerMap = new ObserverMap(TestClass.prototype, schema);
|
|
14
|
+
});
|
|
15
|
+
test("should create new instances", async () => {
|
|
16
|
+
const instance1 = new ObserverMap(TestClass.prototype, schema);
|
|
17
|
+
const instance2 = new ObserverMap(TestClass.prototype, schema);
|
|
18
|
+
expect(instance1).not.toBe(instance2);
|
|
19
|
+
});
|
|
20
|
+
test("proxies direct object assignments", async () => {
|
|
21
|
+
const schemaMap = Schema.jsonSchemaMap.get(testElementName);
|
|
22
|
+
schemaMap.set("someData", {
|
|
23
|
+
$schema: "https://json-schema.org/draft/2019-09/schema",
|
|
24
|
+
$id: `https://fast.design/schemas/${testElementName}/someData.json`,
|
|
25
|
+
type: "object",
|
|
26
|
+
properties: {
|
|
27
|
+
foo: { type: "string" },
|
|
28
|
+
},
|
|
29
|
+
[defsPropertyName]: {},
|
|
30
|
+
});
|
|
31
|
+
observerMap.defineProperties();
|
|
32
|
+
const instance = new TestClass();
|
|
33
|
+
instance.someData = null;
|
|
34
|
+
const nextValue = { foo: "bar" };
|
|
35
|
+
instance.someData = nextValue;
|
|
36
|
+
expect(instance.someData).not.toBe(nextValue);
|
|
37
|
+
expect(instance.someData.$isProxy).toBeTruthy();
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
// The context, in most cases the array property e.g. users
|
|
2
|
+
export const fastContextMetaData = "$fast_context";
|
|
3
|
+
// The list of contexts preceeding this context, the first of which should be the root property
|
|
4
|
+
export const fastContextsMetaData = "$fast_parent_contexts";
|
|
5
|
+
export const defsPropertyName = "$defs";
|
|
6
|
+
export const refPropertyName = "$ref";
|
|
7
|
+
/**
|
|
8
|
+
* A constructed JSON schema from a template
|
|
9
|
+
*/
|
|
10
|
+
export class Schema {
|
|
11
|
+
constructor(name) {
|
|
12
|
+
this.customElementName = name;
|
|
13
|
+
Schema.jsonSchemaMap.set(this.customElementName, new Map());
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Add a path to a schema
|
|
17
|
+
* @param config RegisterPathConfig
|
|
18
|
+
*/
|
|
19
|
+
addPath(config) {
|
|
20
|
+
var _a, _b, _c;
|
|
21
|
+
const splitPath = this.getSplitPath(config.pathConfig.path);
|
|
22
|
+
let schema = Schema.jsonSchemaMap.get(this.customElementName).get(config.rootPropertyName);
|
|
23
|
+
let childRef = null;
|
|
24
|
+
// Create a root level property JSON
|
|
25
|
+
if (!schema) {
|
|
26
|
+
this.addNewSchema(config.rootPropertyName);
|
|
27
|
+
schema = Schema.jsonSchemaMap.get(this.customElementName).get(config.rootPropertyName);
|
|
28
|
+
}
|
|
29
|
+
if (config.childrenMap) {
|
|
30
|
+
childRef = this.getSchemaId(config.childrenMap.customElementName, config.childrenMap.attributeName);
|
|
31
|
+
if (splitPath.length === 1) {
|
|
32
|
+
schema.anyOf
|
|
33
|
+
? schema.anyOf.push({ [refPropertyName]: childRef })
|
|
34
|
+
: (schema.anyOf = [{ [refPropertyName]: childRef }]);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
switch (config.pathConfig.type) {
|
|
38
|
+
case "default":
|
|
39
|
+
case "access": {
|
|
40
|
+
if (splitPath.length > 1) {
|
|
41
|
+
if (config.pathConfig.currentContext === null) {
|
|
42
|
+
this.addPropertiesToAnObject(schema, splitPath.slice(1), config.pathConfig.currentContext, childRef);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
if (!((_a = schema[defsPropertyName]) === null || _a === void 0 ? void 0 : _a[splitPath[0]])) {
|
|
46
|
+
schema[defsPropertyName] = {
|
|
47
|
+
[splitPath[0]]: {},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
this.addPropertiesToAContext(schema[defsPropertyName][splitPath[0]], splitPath.slice(1), config.pathConfig.currentContext, childRef);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
case "repeat": {
|
|
56
|
+
this.addContext(schema, splitPath.at(-1), // example items
|
|
57
|
+
config.pathConfig.currentContext, // example item
|
|
58
|
+
config.pathConfig.parentContext);
|
|
59
|
+
if (splitPath.length > 2) {
|
|
60
|
+
let updatedSchema = schema;
|
|
61
|
+
const hasParentContext = !!config.pathConfig.parentContext;
|
|
62
|
+
if (hasParentContext) {
|
|
63
|
+
updatedSchema = this.addPropertiesToAnObject((_b = schema[defsPropertyName]) === null || _b === void 0 ? void 0 : _b[config.pathConfig.parentContext], splitPath.slice(1, -1), config.pathConfig.parentContext, childRef);
|
|
64
|
+
}
|
|
65
|
+
this.addPropertiesToAnObject(updatedSchema, hasParentContext ? splitPath.slice(2) : splitPath.slice(1), config.pathConfig.currentContext, childRef, "array");
|
|
66
|
+
}
|
|
67
|
+
else if (splitPath.length > 1) {
|
|
68
|
+
let schemaDefinition;
|
|
69
|
+
if (config.pathConfig.parentContext) {
|
|
70
|
+
schemaDefinition = (_c = schema === null || schema === void 0 ? void 0 : schema[defsPropertyName]) === null || _c === void 0 ? void 0 : _c[config.pathConfig.parentContext];
|
|
71
|
+
}
|
|
72
|
+
this.addPropertiesToAnObject(schemaDefinition !== null && schemaDefinition !== void 0 ? schemaDefinition : schema, splitPath.slice(1), config.pathConfig.currentContext, childRef, "array");
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
schema.type = "array";
|
|
76
|
+
schema[refPropertyName] = this.getDefsPath(config.pathConfig.currentContext);
|
|
77
|
+
}
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Gets the JSON schema for a property name
|
|
84
|
+
* @param rootPropertyName - the root property the JSON schema is mapped to
|
|
85
|
+
* @returns The JSON schema for the root property
|
|
86
|
+
*/
|
|
87
|
+
getSchema(rootPropertyName) {
|
|
88
|
+
var _a;
|
|
89
|
+
return ((_a = Schema.jsonSchemaMap.get(this.customElementName).get(rootPropertyName)) !== null && _a !== void 0 ? _a : null);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Gets root properties
|
|
93
|
+
* @returns IterableIterator<string>
|
|
94
|
+
*/
|
|
95
|
+
getRootProperties() {
|
|
96
|
+
return Schema.jsonSchemaMap.get(this.customElementName).keys();
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Get a path split into property names
|
|
100
|
+
* @param path The dot syntax path e.g. a.b.c
|
|
101
|
+
* @returns An array of items in the path
|
|
102
|
+
*/
|
|
103
|
+
getSplitPath(path) {
|
|
104
|
+
return path.split(".");
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Gets the path to the $def
|
|
108
|
+
* @param context The context name e.g. {{item in items}} in a repeat creates the "item" context
|
|
109
|
+
* @returns A string to use as a $ref
|
|
110
|
+
*/
|
|
111
|
+
getDefsPath(context) {
|
|
112
|
+
return `#/${defsPropertyName}/${context}`;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Get the schema $id
|
|
116
|
+
* @param customElementName - The custom element name
|
|
117
|
+
* @param propertyName - The property name
|
|
118
|
+
* @returns The ID that can be used in the JSON schema as $id
|
|
119
|
+
*/
|
|
120
|
+
getSchemaId(customElementName, propertyName) {
|
|
121
|
+
return `https://fast.design/schemas/${customElementName}/${propertyName}.json`;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Add a new JSON schema to the JSON schema map
|
|
125
|
+
* @param propertyName The name of the property to assign this JSON schema to
|
|
126
|
+
*/
|
|
127
|
+
addNewSchema(propertyName) {
|
|
128
|
+
Schema.jsonSchemaMap.get(this.customElementName).set(propertyName, {
|
|
129
|
+
$schema: "https://json-schema.org/draft/2019-09/schema",
|
|
130
|
+
$id: this.getSchemaId(this.customElementName, propertyName),
|
|
131
|
+
[defsPropertyName]: {},
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Add properties to a context
|
|
136
|
+
* @param schema The schema to add the properties to
|
|
137
|
+
* @param splitPath The path split into property/context names
|
|
138
|
+
* @param context The paths context
|
|
139
|
+
*/
|
|
140
|
+
addPropertiesToAContext(schema, splitPath, context, childRef) {
|
|
141
|
+
schema.type = "object";
|
|
142
|
+
if (schema.properties && !schema.properties[splitPath[0]]) {
|
|
143
|
+
schema.properties[splitPath[0]] = {};
|
|
144
|
+
}
|
|
145
|
+
else if (!schema.properties) {
|
|
146
|
+
schema.properties = {
|
|
147
|
+
[splitPath[0]]: {},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
if (splitPath.length > 1) {
|
|
151
|
+
this.addPropertiesToAnObject(schema.properties[splitPath[0]], splitPath.slice(1), context, childRef);
|
|
152
|
+
}
|
|
153
|
+
else if (childRef) {
|
|
154
|
+
if (schema.properties[splitPath[0]].anyOf) {
|
|
155
|
+
schema.properties[splitPath[0]].anyOf.push({
|
|
156
|
+
[refPropertyName]: childRef,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
schema.properties[splitPath[0]].anyOf = [{ [refPropertyName]: childRef }];
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Add properties to an object
|
|
166
|
+
* @param schema The schema to add the properties to
|
|
167
|
+
* @param splitPath The path split into property/context names
|
|
168
|
+
* @param context The paths context
|
|
169
|
+
* @param type The data type (see JSON schema for details)
|
|
170
|
+
*/
|
|
171
|
+
addPropertiesToAnObject(schema, splitPath, context, childRef, type = "object") {
|
|
172
|
+
schema.type = "object";
|
|
173
|
+
if (schema.properties && !schema.properties[splitPath[0]]) {
|
|
174
|
+
schema.properties[splitPath[0]] = {};
|
|
175
|
+
}
|
|
176
|
+
else if (!schema.properties) {
|
|
177
|
+
schema.properties = {
|
|
178
|
+
[splitPath[0]]: {},
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
if (type === "object" && splitPath.length > 1) {
|
|
182
|
+
return this.addPropertiesToAnObject(schema.properties[splitPath[0]], splitPath.slice(1), context, childRef, type);
|
|
183
|
+
}
|
|
184
|
+
else if (type === "array") {
|
|
185
|
+
if (splitPath.length > 1) {
|
|
186
|
+
return this.addPropertiesToAnObject(schema.properties[splitPath[0]], splitPath.slice(1), context, childRef, type);
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
return this.addArrayToAnObject(schema.properties[splitPath[0]], context);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (schema.properties[splitPath[0]].anyOf && childRef) {
|
|
193
|
+
schema.properties[splitPath[0]].anyOf.push({ [refPropertyName]: childRef });
|
|
194
|
+
}
|
|
195
|
+
else if (childRef) {
|
|
196
|
+
schema.properties[splitPath[0]].anyOf = [{ [refPropertyName]: childRef }];
|
|
197
|
+
}
|
|
198
|
+
return schema.properties[splitPath[0]];
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Add an array to an object property
|
|
202
|
+
* @param schema The schema to add the properties to
|
|
203
|
+
* @param context The name of the context
|
|
204
|
+
*/
|
|
205
|
+
addArrayToAnObject(schema, context) {
|
|
206
|
+
schema.type = "array";
|
|
207
|
+
schema.items = {
|
|
208
|
+
[refPropertyName]: this.getDefsPath(context),
|
|
209
|
+
};
|
|
210
|
+
return schema.items;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Add a context to the $defs property
|
|
214
|
+
* @param schema The schema to use
|
|
215
|
+
* @param propertyName The name of the property the context belongs to
|
|
216
|
+
* @param currentContext The current context
|
|
217
|
+
* @param parentContext The parent context
|
|
218
|
+
* @returns
|
|
219
|
+
*/
|
|
220
|
+
addContext(schema, propertyName, // e.g items
|
|
221
|
+
currentContext, // e.g. item
|
|
222
|
+
parentContext) {
|
|
223
|
+
if (schema[defsPropertyName][currentContext]) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
schema[defsPropertyName][currentContext] = {
|
|
227
|
+
[fastContextMetaData]: propertyName,
|
|
228
|
+
[fastContextsMetaData]: this.getParentContexts(schema, parentContext),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Get parent contexts
|
|
233
|
+
* @param schema The schema to use
|
|
234
|
+
* @param parentContext The parent context
|
|
235
|
+
* @param contexts A list of parent contexts
|
|
236
|
+
* @returns
|
|
237
|
+
*/
|
|
238
|
+
getParentContexts(schema, parentContext, contexts = []) {
|
|
239
|
+
var _a;
|
|
240
|
+
if (parentContext === null) {
|
|
241
|
+
return [null, ...contexts];
|
|
242
|
+
}
|
|
243
|
+
const parentParentContext = (_a = schema === null || schema === void 0 ? void 0 : schema[defsPropertyName]) === null || _a === void 0 ? void 0 : _a[parentContext][fastContextsMetaData];
|
|
244
|
+
return this.getParentContexts(schema, parentParentContext.at(-1), [parentContext, ...contexts]);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* A JSON schema describing each root schema
|
|
249
|
+
*/
|
|
250
|
+
Schema.jsonSchemaMap = new Map();
|