@rettangoli/fe 1.0.4 → 1.1.1
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 +1 -1
- package/package.json +1 -1
- package/src/cli/frontendEntrySource.js +4 -4
- package/src/core/contracts/componentFiles.js +4 -20
- package/src/core/runtime/componentOrchestrator.js +34 -1
- package/src/core/runtime/lifecycle.js +39 -0
- package/src/core/runtime/props.js +75 -0
- package/src/core/view/bindings.js +1 -1
- package/src/core/view/templatePropertyBindings.js +276 -0
- package/src/parser.js +2 -0
- package/src/web/createWebComponentClass.js +15 -1
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -113,6 +113,10 @@ export const generateFrontendEntrySource = ({
|
|
|
113
113
|
} else if (YAML_FILE_TYPES.has(fileType)) {
|
|
114
114
|
const yamlObject = readYamlObject(filePath);
|
|
115
115
|
|
|
116
|
+
if (fileType === "view" || fileType === "schema") {
|
|
117
|
+
componentContractEntry.yamlObject = structuredClone(yamlObject);
|
|
118
|
+
}
|
|
119
|
+
|
|
116
120
|
if (fileType === "view") {
|
|
117
121
|
try {
|
|
118
122
|
yamlObject.template = parseTemplate(yamlObject.template);
|
|
@@ -127,10 +131,6 @@ export const generateFrontendEntrySource = ({
|
|
|
127
131
|
validateConstantsRoot({ filePath, yamlObject, errorPrefix });
|
|
128
132
|
}
|
|
129
133
|
|
|
130
|
-
if (fileType === "view" || fileType === "schema") {
|
|
131
|
-
componentContractEntry.yamlObject = yamlObject;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
134
|
declarationLines.push(
|
|
135
135
|
`const ${declarationName} = ${JSON.stringify(yamlObject)};`,
|
|
136
136
|
);
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { findUnsupportedTemplatePropertyBindingSyntax } from "../view/templatePropertyBindings.js";
|
|
2
|
+
|
|
1
3
|
export const FORBIDDEN_VIEW_KEYS = Object.freeze([
|
|
2
4
|
"elementName",
|
|
3
5
|
"viewDataSchema",
|
|
@@ -7,24 +9,6 @@ export const FORBIDDEN_VIEW_KEYS = Object.freeze([
|
|
|
7
9
|
"attrsSchema",
|
|
8
10
|
]);
|
|
9
11
|
|
|
10
|
-
const LEGACY_PROP_BINDING_REGEX = /(^|\s)\.[A-Za-z_][A-Za-z0-9_-]*\s*=/;
|
|
11
|
-
|
|
12
|
-
const hasLegacyDotPropBinding = (node) => {
|
|
13
|
-
if (Array.isArray(node)) {
|
|
14
|
-
return node.some((item) => hasLegacyDotPropBinding(item));
|
|
15
|
-
}
|
|
16
|
-
if (!node || typeof node !== "object") {
|
|
17
|
-
return false;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
return Object.entries(node).some(([key, value]) => {
|
|
21
|
-
if (LEGACY_PROP_BINDING_REGEX.test(key)) {
|
|
22
|
-
return true;
|
|
23
|
-
}
|
|
24
|
-
return hasLegacyDotPropBinding(value);
|
|
25
|
-
});
|
|
26
|
-
};
|
|
27
|
-
|
|
28
12
|
export const buildComponentContractIndex = (entries = []) => {
|
|
29
13
|
const index = {};
|
|
30
14
|
|
|
@@ -99,10 +83,10 @@ export const validateComponentContractIndex = (index = {}) => {
|
|
|
99
83
|
});
|
|
100
84
|
});
|
|
101
85
|
|
|
102
|
-
if (
|
|
86
|
+
if (findUnsupportedTemplatePropertyBindingSyntax(viewYaml.template)) {
|
|
103
87
|
errors.push({
|
|
104
88
|
code: "RTGL-CONTRACT-003",
|
|
105
|
-
message: `${componentLabel}: legacy
|
|
89
|
+
message: `${componentLabel}: legacy property binding syntax is not supported. Use ':prop=\${value}' in .view.yaml.`,
|
|
106
90
|
filePath: viewFilePath || representativeFile,
|
|
107
91
|
});
|
|
108
92
|
}
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
cleanupEventRateLimitState,
|
|
8
8
|
syncRefIds,
|
|
9
9
|
} from "./componentRuntime.js";
|
|
10
|
-
import { buildOnUpdateChanges } from "./lifecycle.js";
|
|
10
|
+
import { buildOnPropUpdateChanges, buildOnUpdateChanges } from "./lifecycle.js";
|
|
11
11
|
import { normalizeAttributeValue, toCamelCase } from "./props.js";
|
|
12
12
|
|
|
13
13
|
export const createRuntimeDepsForInstance = ({ instance }) => {
|
|
@@ -112,6 +112,39 @@ export const runAttributeChangedComponentLifecycle = ({
|
|
|
112
112
|
});
|
|
113
113
|
};
|
|
114
114
|
|
|
115
|
+
export const runPropChangedComponentLifecycle = ({
|
|
116
|
+
instance,
|
|
117
|
+
propName,
|
|
118
|
+
oldValue,
|
|
119
|
+
newValue,
|
|
120
|
+
scheduleFrameFn,
|
|
121
|
+
}) => {
|
|
122
|
+
if (oldValue === newValue || !instance.render) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (instance.isConnected === false || !instance.renderTarget) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (instance.handlers?.handleOnUpdate) {
|
|
131
|
+
const runtimeDeps = createRuntimeDepsForInstance({ instance });
|
|
132
|
+
const changes = buildOnPropUpdateChanges({
|
|
133
|
+
propName,
|
|
134
|
+
oldValue,
|
|
135
|
+
newValue,
|
|
136
|
+
deps: runtimeDeps,
|
|
137
|
+
propsSchemaKeys: instance._propsSchemaKeys,
|
|
138
|
+
});
|
|
139
|
+
instance.handlers.handleOnUpdate(runtimeDeps, changes);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
scheduleFrameFn(() => {
|
|
144
|
+
instance.render();
|
|
145
|
+
});
|
|
146
|
+
};
|
|
147
|
+
|
|
115
148
|
export const runRenderComponentLifecycle = ({
|
|
116
149
|
instance,
|
|
117
150
|
createComponentUpdateHookFn,
|
|
@@ -122,3 +122,42 @@ export const buildOnUpdateChanges = ({
|
|
|
122
122
|
newProps,
|
|
123
123
|
};
|
|
124
124
|
};
|
|
125
|
+
|
|
126
|
+
export const buildOnPropUpdateChanges = ({
|
|
127
|
+
propName,
|
|
128
|
+
oldValue,
|
|
129
|
+
newValue,
|
|
130
|
+
deps,
|
|
131
|
+
propsSchemaKeys,
|
|
132
|
+
}) => {
|
|
133
|
+
const newProps = {};
|
|
134
|
+
|
|
135
|
+
propsSchemaKeys.forEach((propKey) => {
|
|
136
|
+
const propValue = deps.props[propKey];
|
|
137
|
+
if (propValue !== undefined) {
|
|
138
|
+
newProps[propKey] = propValue;
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const oldProps = {
|
|
143
|
+
...newProps,
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
if (oldValue === undefined) {
|
|
147
|
+
delete oldProps[propName];
|
|
148
|
+
} else {
|
|
149
|
+
oldProps[propName] = oldValue;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (newValue === undefined) {
|
|
153
|
+
delete newProps[propName];
|
|
154
|
+
} else {
|
|
155
|
+
newProps[propName] = newValue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
changedProp: propName,
|
|
160
|
+
oldProps,
|
|
161
|
+
newProps,
|
|
162
|
+
};
|
|
163
|
+
};
|
|
@@ -28,6 +28,81 @@ export const readPropFallbackFromAttributes = (source, propName) => {
|
|
|
28
28
|
return undefined;
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
+
const REACTIVE_PROP_VALUES = Symbol("rtglReactivePropValues");
|
|
32
|
+
|
|
33
|
+
const ensureReactivePropValues = (source) => {
|
|
34
|
+
if (!Object.prototype.hasOwnProperty.call(source, REACTIVE_PROP_VALUES)) {
|
|
35
|
+
Object.defineProperty(source, REACTIVE_PROP_VALUES, {
|
|
36
|
+
value: Object.create(null),
|
|
37
|
+
enumerable: false,
|
|
38
|
+
configurable: false,
|
|
39
|
+
writable: false,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return source[REACTIVE_PROP_VALUES];
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const installReactiveProps = ({
|
|
47
|
+
source,
|
|
48
|
+
allowedKeys = [],
|
|
49
|
+
onPropChange,
|
|
50
|
+
}) => {
|
|
51
|
+
const reactiveValues = ensureReactivePropValues(source);
|
|
52
|
+
|
|
53
|
+
allowedKeys.forEach((propName) => {
|
|
54
|
+
if (typeof propName !== "string" || propName.length === 0) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const presetValue = Object.prototype.hasOwnProperty.call(source, propName)
|
|
59
|
+
? source[propName]
|
|
60
|
+
: undefined;
|
|
61
|
+
const hadPresetValue = Object.prototype.hasOwnProperty.call(source, propName);
|
|
62
|
+
|
|
63
|
+
if (hadPresetValue) {
|
|
64
|
+
delete source[propName];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
Object.defineProperty(source, propName, {
|
|
68
|
+
configurable: true,
|
|
69
|
+
enumerable: true,
|
|
70
|
+
get() {
|
|
71
|
+
if (Object.prototype.hasOwnProperty.call(reactiveValues, propName)) {
|
|
72
|
+
return reactiveValues[propName];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return readPropFallbackFromAttributes(source, propName);
|
|
76
|
+
},
|
|
77
|
+
set(value) {
|
|
78
|
+
const oldValue = Object.prototype.hasOwnProperty.call(reactiveValues, propName)
|
|
79
|
+
? reactiveValues[propName]
|
|
80
|
+
: readPropFallbackFromAttributes(source, propName);
|
|
81
|
+
|
|
82
|
+
if (value === undefined) {
|
|
83
|
+
delete reactiveValues[propName];
|
|
84
|
+
} else {
|
|
85
|
+
reactiveValues[propName] = value;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (oldValue === value) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
onPropChange?.({
|
|
93
|
+
propName,
|
|
94
|
+
oldValue,
|
|
95
|
+
newValue: value,
|
|
96
|
+
});
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
if (hadPresetValue) {
|
|
101
|
+
reactiveValues[propName] = presetValue;
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
};
|
|
105
|
+
|
|
31
106
|
export const createPropsProxy = (source, allowedKeys) => {
|
|
32
107
|
const allowed = new Set(allowedKeys);
|
|
33
108
|
return new Proxy(
|
|
@@ -149,7 +149,7 @@ export const parseNodeBindings = ({
|
|
|
149
149
|
}
|
|
150
150
|
if (Object.prototype.hasOwnProperty.call(props, normalizedPropName)) {
|
|
151
151
|
throw new Error(
|
|
152
|
-
`[Parser] Duplicate prop binding '${normalizedPropName}' on '${tagName}'. Use only one of 'name=value' or ':name
|
|
152
|
+
`[Parser] Duplicate prop binding '${normalizedPropName}' on '${tagName}'. Use only one of 'name=value' or ':name=\${expr}'.`,
|
|
153
153
|
);
|
|
154
154
|
}
|
|
155
155
|
props[normalizedPropName] = propValue;
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { parse as parseTemplate } from "jempl";
|
|
2
|
+
import { NodeType } from "jempl/src/parse/constants.js";
|
|
3
|
+
|
|
4
|
+
const ATTR_ASSIGNMENT_REGEX = /(\S+?)=(?:\"([^\"]*)\"|\'([^\']*)\'|([^\s]*))/g;
|
|
5
|
+
const LOOP_DIRECTIVE_REGEX = /^\$for\s+([A-Za-z_][A-Za-z0-9_]*)(?:\s*,\s*([A-Za-z_][A-Za-z0-9_]*))?\s+in\s+.+$/;
|
|
6
|
+
const INTERPOLATION_ONLY_REGEX = /^\$\{([^{}]+)\}$/;
|
|
7
|
+
const SIMPLE_PATH_REGEX = /^[A-Za-z_][A-Za-z0-9_]*(?:(?:\.[A-Za-z_][A-Za-z0-9_]*)|\[(?:\d+|"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')\])*$/;
|
|
8
|
+
|
|
9
|
+
const normalizedTemplateCache = new WeakSet();
|
|
10
|
+
|
|
11
|
+
const extendScopeVars = (scopeVars, itemVar, indexVar) => {
|
|
12
|
+
const nextScopeVars = new Set(scopeVars);
|
|
13
|
+
if (itemVar) {
|
|
14
|
+
nextScopeVars.add(itemVar);
|
|
15
|
+
}
|
|
16
|
+
if (indexVar) {
|
|
17
|
+
nextScopeVars.add(indexVar);
|
|
18
|
+
}
|
|
19
|
+
return nextScopeVars;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const getLoopScopeVarsFromRawKey = (key, scopeVars) => {
|
|
23
|
+
if (typeof key !== "string") {
|
|
24
|
+
return scopeVars;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const loopMatch = key.match(LOOP_DIRECTIVE_REGEX);
|
|
28
|
+
if (!loopMatch) {
|
|
29
|
+
return scopeVars;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return extendScopeVars(scopeVars, loopMatch[1], loopMatch[2]);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const getPropertyBindingViolationForKey = (key) => {
|
|
36
|
+
if (typeof key !== "string" || (!key.includes(":") && !key.includes("."))) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
ATTR_ASSIGNMENT_REGEX.lastIndex = 0;
|
|
41
|
+
let match;
|
|
42
|
+
while ((match = ATTR_ASSIGNMENT_REGEX.exec(key)) !== null) {
|
|
43
|
+
const rawBindingName = match[1];
|
|
44
|
+
if (rawBindingName.startsWith(".")) {
|
|
45
|
+
return {
|
|
46
|
+
bindingName: rawBindingName,
|
|
47
|
+
rawValue: match[2] ?? match[3] ?? match[4] ?? "",
|
|
48
|
+
key,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!rawBindingName.startsWith(":")) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const isQuoted = match[2] !== undefined || match[3] !== undefined;
|
|
57
|
+
const rawValue = match[2] ?? match[3] ?? match[4] ?? "";
|
|
58
|
+
if (isQuoted || !INTERPOLATION_ONLY_REGEX.test(rawValue)) {
|
|
59
|
+
return {
|
|
60
|
+
bindingName: rawBindingName,
|
|
61
|
+
rawValue,
|
|
62
|
+
key,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return null;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const walkRawTemplateForViolation = (node, scopeVars = new Set()) => {
|
|
71
|
+
if (Array.isArray(node)) {
|
|
72
|
+
for (const item of node) {
|
|
73
|
+
const violation = walkRawTemplateForViolation(item, scopeVars);
|
|
74
|
+
if (violation) {
|
|
75
|
+
return violation;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!node || typeof node !== "object") {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
for (const [key, value] of Object.entries(node)) {
|
|
86
|
+
const violation = getPropertyBindingViolationForKey(key);
|
|
87
|
+
if (violation) {
|
|
88
|
+
return violation;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const nextScopeVars = getLoopScopeVarsFromRawKey(key, scopeVars);
|
|
92
|
+
const nestedViolation = walkRawTemplateForViolation(value, nextScopeVars);
|
|
93
|
+
if (nestedViolation) {
|
|
94
|
+
return nestedViolation;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return null;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const walkAstTemplateForViolation = (node, scopeVars = new Set()) => {
|
|
102
|
+
if (!node || typeof node !== "object") {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (Array.isArray(node)) {
|
|
107
|
+
for (const item of node) {
|
|
108
|
+
const violation = walkAstTemplateForViolation(item, scopeVars);
|
|
109
|
+
if (violation) {
|
|
110
|
+
return violation;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (node.type === NodeType.LOOP) {
|
|
117
|
+
const nextScopeVars = extendScopeVars(scopeVars, node.itemVar, node.indexVar);
|
|
118
|
+
return walkAstTemplateForViolation(node.body, nextScopeVars);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (node.type === NodeType.ARRAY && Array.isArray(node.items)) {
|
|
122
|
+
return walkAstTemplateForViolation(node.items, scopeVars);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (node.type === NodeType.OBJECT && Array.isArray(node.properties)) {
|
|
126
|
+
for (const property of node.properties) {
|
|
127
|
+
const violation = getPropertyBindingViolationForKey(property.key);
|
|
128
|
+
if (violation) {
|
|
129
|
+
return violation;
|
|
130
|
+
}
|
|
131
|
+
const nestedViolation = walkAstTemplateForViolation(property.value, scopeVars);
|
|
132
|
+
if (nestedViolation) {
|
|
133
|
+
return nestedViolation;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
for (const value of Object.values(node)) {
|
|
140
|
+
const violation = walkAstTemplateForViolation(value, scopeVars);
|
|
141
|
+
if (violation) {
|
|
142
|
+
return violation;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return null;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const deriveParsedKey = (key) => {
|
|
150
|
+
const parsed = parseTemplate([{ [key]: "" }]);
|
|
151
|
+
const property = parsed?.items?.[0]?.properties?.[0];
|
|
152
|
+
return property?.parsedKey;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const getInterpolationExpression = (rawValue) => {
|
|
156
|
+
const match = rawValue.match(INTERPOLATION_ONLY_REGEX);
|
|
157
|
+
return match ? match[1].trim() : null;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const getBaseIdentifier = (expression) => {
|
|
161
|
+
const match = expression.match(/^([A-Za-z_][A-Za-z0-9_]*)/);
|
|
162
|
+
return match ? match[1] : null;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const normalizePropertyBindingsInKey = (key, scopeVars) => {
|
|
166
|
+
let changed = false;
|
|
167
|
+
|
|
168
|
+
const normalizedKey = key.replace(
|
|
169
|
+
ATTR_ASSIGNMENT_REGEX,
|
|
170
|
+
(fullMatch, rawBindingName, doubleQuotedValue, singleQuotedValue, bareValue) => {
|
|
171
|
+
if (!rawBindingName.startsWith(":")) {
|
|
172
|
+
return fullMatch;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const rawValue = doubleQuotedValue ?? singleQuotedValue ?? bareValue ?? "";
|
|
176
|
+
const expression = getInterpolationExpression(rawValue);
|
|
177
|
+
if (!expression || !SIMPLE_PATH_REGEX.test(expression)) {
|
|
178
|
+
return fullMatch;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const baseIdentifier = getBaseIdentifier(expression);
|
|
182
|
+
if (!baseIdentifier) {
|
|
183
|
+
return fullMatch;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const internalValue = scopeVars.has(baseIdentifier)
|
|
187
|
+
? `#{${expression}}`
|
|
188
|
+
: expression;
|
|
189
|
+
|
|
190
|
+
if (internalValue === rawValue) {
|
|
191
|
+
return fullMatch;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
changed = true;
|
|
195
|
+
return `${rawBindingName}=${internalValue}`;
|
|
196
|
+
},
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
return changed ? normalizedKey : key;
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const normalizeAstTemplate = (node, scopeVars = new Set()) => {
|
|
203
|
+
if (!node || typeof node !== "object") {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (Array.isArray(node)) {
|
|
208
|
+
node.forEach((item) => normalizeAstTemplate(item, scopeVars));
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (node.type === NodeType.LOOP) {
|
|
213
|
+
const nextScopeVars = extendScopeVars(scopeVars, node.itemVar, node.indexVar);
|
|
214
|
+
normalizeAstTemplate(node.body, nextScopeVars);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (node.type === NodeType.ARRAY && Array.isArray(node.items)) {
|
|
219
|
+
node.items.forEach((item) => normalizeAstTemplate(item, scopeVars));
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (node.type === NodeType.OBJECT && Array.isArray(node.properties)) {
|
|
224
|
+
node.properties.forEach((property) => {
|
|
225
|
+
const normalizedKey = normalizePropertyBindingsInKey(property.key, scopeVars);
|
|
226
|
+
if (normalizedKey !== property.key) {
|
|
227
|
+
property.key = normalizedKey;
|
|
228
|
+
const parsedKey = deriveParsedKey(normalizedKey);
|
|
229
|
+
if (parsedKey) {
|
|
230
|
+
property.parsedKey = parsedKey;
|
|
231
|
+
} else {
|
|
232
|
+
delete property.parsedKey;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
normalizeAstTemplate(property.value, scopeVars);
|
|
237
|
+
});
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
Object.values(node).forEach((value) => {
|
|
242
|
+
normalizeAstTemplate(value, scopeVars);
|
|
243
|
+
});
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
export const findUnsupportedTemplatePropertyBindingSyntax = (template) => {
|
|
247
|
+
if (!template) {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (template.type && typeof template.type === "number") {
|
|
252
|
+
return walkAstTemplateForViolation(template);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return walkRawTemplateForViolation(template);
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
export const ensureNormalizedTemplatePropertyBindings = (template) => {
|
|
259
|
+
if (!template || typeof template !== "object") {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (normalizedTemplateCache.has(template)) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const violation = findUnsupportedTemplatePropertyBindingSyntax(template);
|
|
268
|
+
if (violation) {
|
|
269
|
+
throw new Error(
|
|
270
|
+
`Property-form bindings must use ':prop=\${value}' syntax. Found '${violation.bindingName}=${violation.rawValue}' in '${violation.key}'.`,
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
normalizeAstTemplate(template);
|
|
275
|
+
normalizedTemplateCache.add(template);
|
|
276
|
+
};
|
package/src/parser.js
CHANGED
|
@@ -2,6 +2,7 @@ import { parseAndRender as jemplParseAndRender, render as jemplRender } from "je
|
|
|
2
2
|
|
|
3
3
|
import { flattenArrays } from "./utils/flattenArrays.js";
|
|
4
4
|
import { parseNodeBindings } from './core/view/bindings.js';
|
|
5
|
+
import { ensureNormalizedTemplatePropertyBindings } from "./core/view/templatePropertyBindings.js";
|
|
5
6
|
import {
|
|
6
7
|
createRefMatchers,
|
|
7
8
|
resolveBestRefMatcher,
|
|
@@ -20,6 +21,7 @@ export const parseView = ({
|
|
|
20
21
|
handlers,
|
|
21
22
|
createComponentUpdateHook,
|
|
22
23
|
}) => {
|
|
24
|
+
ensureNormalizedTemplatePropertyBindings(template);
|
|
23
25
|
const result = jemplRender(template, viewData, {});
|
|
24
26
|
|
|
25
27
|
// Flatten the array carefully to maintain structure
|
|
@@ -7,9 +7,10 @@ import {
|
|
|
7
7
|
runAttributeChangedComponentLifecycle,
|
|
8
8
|
runConnectedComponentLifecycle,
|
|
9
9
|
runDisconnectedComponentLifecycle,
|
|
10
|
+
runPropChangedComponentLifecycle,
|
|
10
11
|
runRenderComponentLifecycle,
|
|
11
12
|
} from "../core/runtime/componentOrchestrator.js";
|
|
12
|
-
import { createPropsProxy, toKebabCase } from "../core/runtime/props.js";
|
|
13
|
+
import { createPropsProxy, installReactiveProps, toKebabCase } from "../core/runtime/props.js";
|
|
13
14
|
import {
|
|
14
15
|
buildObservedAttributes,
|
|
15
16
|
} from "../core/runtime/componentRuntime.js";
|
|
@@ -120,6 +121,19 @@ export const createWebComponentClass = ({
|
|
|
120
121
|
fileConstants: constants,
|
|
121
122
|
});
|
|
122
123
|
this.propsSchema = propsSchema;
|
|
124
|
+
installReactiveProps({
|
|
125
|
+
source: this,
|
|
126
|
+
allowedKeys: propsSchemaKeys,
|
|
127
|
+
onPropChange: ({ propName, oldValue, newValue }) => {
|
|
128
|
+
runPropChangedComponentLifecycle({
|
|
129
|
+
instance: this,
|
|
130
|
+
propName,
|
|
131
|
+
oldValue,
|
|
132
|
+
newValue,
|
|
133
|
+
scheduleFrameFn: scheduleFrame,
|
|
134
|
+
});
|
|
135
|
+
},
|
|
136
|
+
});
|
|
123
137
|
this.props = propsSchema
|
|
124
138
|
? createPropsProxy(this, propsSchemaKeys)
|
|
125
139
|
: {};
|