@rettangoli/fe 0.0.14 → 1.0.0-rc1
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 +24 -8
- package/package.json +9 -3
- package/src/cli/blank/blank.handlers.js +5 -2
- package/src/cli/blank/blank.schema.yaml +11 -0
- package/src/cli/blank/blank.view.yaml +0 -11
- package/src/cli/build.js +55 -23
- package/src/cli/check.js +53 -0
- package/src/cli/contracts.js +143 -0
- package/src/cli/index.js +5 -11
- package/src/cli/scaffold.js +6 -0
- package/src/cli/watch.js +3 -2
- package/src/core/contracts/componentFiles.js +119 -0
- package/src/core/runtime/componentOrchestrator.js +156 -0
- package/src/core/runtime/componentRuntime.js +54 -0
- package/src/core/runtime/constants.js +27 -0
- package/src/core/runtime/events.js +191 -0
- package/src/core/runtime/globalListeners.js +87 -0
- package/src/core/runtime/lifecycle.js +124 -0
- package/src/core/runtime/methods.js +40 -0
- package/src/core/runtime/payload.js +3 -0
- package/src/core/runtime/props.js +79 -0
- package/src/core/runtime/refs.js +70 -0
- package/src/core/runtime/store.js +42 -0
- package/src/core/schema/validateSchemaContract.js +26 -0
- package/src/core/style/yamlToCss.js +44 -0
- package/src/core/view/bindings.js +189 -0
- package/src/core/view/refs.js +234 -0
- package/src/createComponent.js +37 -518
- package/src/parser.js +83 -249
- package/src/web/componentDom.js +49 -0
- package/src/web/componentUpdateHook.js +43 -0
- package/src/web/createWebComponentClass.js +150 -0
- package/src/web/scheduler.js +6 -0
package/src/parser.js
CHANGED
|
@@ -1,65 +1,25 @@
|
|
|
1
|
-
import { render as jemplRender } from
|
|
1
|
+
import { parseAndRender as jemplParseAndRender, render as jemplRender } from "jempl";
|
|
2
2
|
|
|
3
3
|
import { flattenArrays } from './common.js';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if (current) {
|
|
24
|
-
parts.push(current);
|
|
25
|
-
current = '';
|
|
26
|
-
}
|
|
27
|
-
inBrackets = true;
|
|
28
|
-
} else if (inBrackets && char === ']') {
|
|
29
|
-
if (current) {
|
|
30
|
-
// Remove quotes if present and add the key
|
|
31
|
-
if ((current.startsWith('"') && current.endsWith('"')) ||
|
|
32
|
-
(current.startsWith("'") && current.endsWith("'"))) {
|
|
33
|
-
parts.push(current.slice(1, -1));
|
|
34
|
-
} else {
|
|
35
|
-
// Numeric index or unquoted string
|
|
36
|
-
const numValue = Number(current);
|
|
37
|
-
parts.push(isNaN(numValue) ? current : numValue);
|
|
38
|
-
}
|
|
39
|
-
current = '';
|
|
40
|
-
}
|
|
41
|
-
inBrackets = false;
|
|
42
|
-
quoteChar = null;
|
|
43
|
-
} else if (inBrackets && (char === '"' || char === "'")) {
|
|
44
|
-
if (!quoteChar) {
|
|
45
|
-
quoteChar = char;
|
|
46
|
-
} else if (char === quoteChar) {
|
|
47
|
-
quoteChar = null;
|
|
48
|
-
}
|
|
49
|
-
current += char;
|
|
50
|
-
} else {
|
|
51
|
-
current += char;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
if (current) {
|
|
56
|
-
parts.push(current);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return parts.reduce((acc, part) => acc && acc[part], obj);
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
export const parseView = ({ h, template, viewData, refs, handlers }) => {
|
|
4
|
+
import { parseNodeBindings } from './core/view/bindings.js';
|
|
5
|
+
import {
|
|
6
|
+
createRefMatchers,
|
|
7
|
+
resolveBestRefMatcher,
|
|
8
|
+
validateElementIdForRefs,
|
|
9
|
+
} from './core/view/refs.js';
|
|
10
|
+
import {
|
|
11
|
+
createConfiguredEventListener,
|
|
12
|
+
getEventRateLimitState,
|
|
13
|
+
} from "./core/runtime/events.js";
|
|
14
|
+
|
|
15
|
+
export const parseView = ({
|
|
16
|
+
h,
|
|
17
|
+
template,
|
|
18
|
+
viewData,
|
|
19
|
+
refs,
|
|
20
|
+
handlers,
|
|
21
|
+
createComponentUpdateHook,
|
|
22
|
+
}) => {
|
|
63
23
|
const result = jemplRender(template, viewData, {});
|
|
64
24
|
|
|
65
25
|
// Flatten the array carefully to maintain structure
|
|
@@ -71,6 +31,7 @@ export const parseView = ({ h, template, viewData, refs, handlers }) => {
|
|
|
71
31
|
refs,
|
|
72
32
|
handlers,
|
|
73
33
|
viewData,
|
|
34
|
+
createComponentUpdateHook,
|
|
74
35
|
});
|
|
75
36
|
|
|
76
37
|
const vdom = h("div", { style: { display: "contents" } }, childNodes);
|
|
@@ -91,13 +52,17 @@ export const createVirtualDom = ({
|
|
|
91
52
|
items,
|
|
92
53
|
refs = {},
|
|
93
54
|
handlers = {},
|
|
94
|
-
viewData = {}
|
|
55
|
+
viewData = {},
|
|
56
|
+
createComponentUpdateHook,
|
|
95
57
|
}) => {
|
|
96
58
|
if (!Array.isArray(items)) {
|
|
97
59
|
console.error("Input to createVirtualDom must be an array.");
|
|
98
60
|
return [h("div", {}, [])];
|
|
99
61
|
}
|
|
100
62
|
|
|
63
|
+
const refMatchers = createRefMatchers(refs);
|
|
64
|
+
const hasIdRefMatchers = refMatchers.some((refMatcher) => refMatcher.targetType === "id");
|
|
65
|
+
|
|
101
66
|
function processItems(currentItems, parentPath = "") {
|
|
102
67
|
return currentItems
|
|
103
68
|
.map((item, index) => {
|
|
@@ -156,70 +121,13 @@ export const createVirtualDom = ({
|
|
|
156
121
|
const tagName = selector.split(/[.#]/)[0];
|
|
157
122
|
const isWebComponent = tagName.includes("-");
|
|
158
123
|
|
|
159
|
-
// 1. Parse
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const processedAttrs = new Set();
|
|
167
|
-
|
|
168
|
-
while ((match = attrRegex.exec(attrsString)) !== null) {
|
|
169
|
-
processedAttrs.add(match[1]);
|
|
170
|
-
if (match[1].startsWith(".")) {
|
|
171
|
-
const propName = match[1].substring(1);
|
|
172
|
-
const valuePathName = match[4];
|
|
173
|
-
props[propName] = lodashGet(viewData, valuePathName);
|
|
174
|
-
} else if (match[1].startsWith("?")) {
|
|
175
|
-
// Handle conditional boolean attributes
|
|
176
|
-
const attrName = match[1].substring(1);
|
|
177
|
-
const attrValue = match[2] || match[3] || match[4];
|
|
178
|
-
|
|
179
|
-
// Convert string values to boolean
|
|
180
|
-
let evalValue;
|
|
181
|
-
if (attrValue === "true") {
|
|
182
|
-
evalValue = true;
|
|
183
|
-
} else if (attrValue === "false") {
|
|
184
|
-
evalValue = false;
|
|
185
|
-
} else {
|
|
186
|
-
// Try to get from viewData if it's not a literal boolean
|
|
187
|
-
evalValue = lodashGet(viewData, attrValue);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// Only add attribute if value is truthy
|
|
191
|
-
if (evalValue) {
|
|
192
|
-
attrs[attrName] = "";
|
|
193
|
-
}
|
|
194
|
-
} else {
|
|
195
|
-
attrs[match[1]] = match[2] || match[3] || match[4];
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// Then, handle boolean attributes without values
|
|
200
|
-
// Remove all processed attribute-value pairs from the string first
|
|
201
|
-
let remainingAttrsString = attrsString;
|
|
202
|
-
const processedMatches = [];
|
|
203
|
-
let tempMatch;
|
|
204
|
-
const tempAttrRegex = /(\S+?)=(?:\"([^\"]*)\"|\'([^\']*)\'|([^\s]+))/g;
|
|
205
|
-
while ((tempMatch = tempAttrRegex.exec(attrsString)) !== null) {
|
|
206
|
-
processedMatches.push(tempMatch[0]);
|
|
207
|
-
}
|
|
208
|
-
// Remove all matched attribute=value pairs
|
|
209
|
-
processedMatches.forEach(match => {
|
|
210
|
-
remainingAttrsString = remainingAttrsString.replace(match, ' ');
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
const booleanAttrRegex = /\b(\S+?)(?=\s|$)/g;
|
|
214
|
-
let boolMatch;
|
|
215
|
-
while ((boolMatch = booleanAttrRegex.exec(remainingAttrsString)) !== null) {
|
|
216
|
-
const attrName = boolMatch[1];
|
|
217
|
-
// Skip if already processed or starts with . (prop) or contains =
|
|
218
|
-
if (!processedAttrs.has(attrName) && !attrName.startsWith(".") && !attrName.includes("=")) {
|
|
219
|
-
attrs[attrName] = "";
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
}
|
|
124
|
+
// 1. Parse selector bindings into attrs/props.
|
|
125
|
+
const { attrs, props } = parseNodeBindings({
|
|
126
|
+
attrsString,
|
|
127
|
+
viewData,
|
|
128
|
+
tagName,
|
|
129
|
+
isWebComponent,
|
|
130
|
+
});
|
|
223
131
|
|
|
224
132
|
// 2. Handle ID from selector string (e.g., tag#id)
|
|
225
133
|
// If an 'id' was already parsed from attrsString (e.g. id=value), it takes precedence.
|
|
@@ -243,18 +151,21 @@ export const createVirtualDom = ({
|
|
|
243
151
|
elementIdForRefs = tagName;
|
|
244
152
|
}
|
|
245
153
|
|
|
154
|
+
const selectorClassMatches = selector.match(/\.([^.#]+)/g) || [];
|
|
155
|
+
const selectorClassNames = selectorClassMatches.map((classMatch) => classMatch.substring(1));
|
|
156
|
+
const attributeClassNames = typeof attrs.class === "string"
|
|
157
|
+
? attrs.class.split(/\s+/).filter(Boolean)
|
|
158
|
+
: [];
|
|
159
|
+
const classNamesForRefs = [...new Set([...selectorClassNames, ...attributeClassNames])];
|
|
160
|
+
|
|
246
161
|
// Extract classes and ID from selector (if not a web component)
|
|
247
162
|
const classObj = Object.create(null); // Using Object.create(null) to avoid prototype issues
|
|
248
163
|
let elementId = null;
|
|
249
164
|
|
|
250
165
|
if (!isWebComponent) {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const className = classMatch.substring(1);
|
|
255
|
-
classObj[className] = true;
|
|
256
|
-
});
|
|
257
|
-
}
|
|
166
|
+
selectorClassNames.forEach((className) => {
|
|
167
|
+
classObj[className] = true;
|
|
168
|
+
});
|
|
258
169
|
|
|
259
170
|
const idMatch = selector.match(/#([^.#\s]+)/);
|
|
260
171
|
if (idMatch) {
|
|
@@ -283,78 +194,44 @@ export const createVirtualDom = ({
|
|
|
283
194
|
// Apply event listeners
|
|
284
195
|
const eventHandlers = Object.create(null);
|
|
285
196
|
|
|
286
|
-
if (
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
.replace(/\\\*/g, ".*") +
|
|
295
|
-
"$";
|
|
296
|
-
try {
|
|
297
|
-
const regex = new RegExp(pattern);
|
|
298
|
-
if (regex.test(elementIdForRefs)) {
|
|
299
|
-
matchingRefKeys.push(refKey);
|
|
300
|
-
}
|
|
301
|
-
} catch (e) {
|
|
302
|
-
// Keep this warning for invalid regex patterns
|
|
303
|
-
console.warn(
|
|
304
|
-
`[Parser] Invalid regex pattern created from refKey '${refKey}': ${pattern}`,
|
|
305
|
-
e,
|
|
306
|
-
);
|
|
307
|
-
}
|
|
308
|
-
} else {
|
|
309
|
-
if (elementIdForRefs === refKey) {
|
|
310
|
-
matchingRefKeys.push(refKey);
|
|
311
|
-
}
|
|
312
|
-
}
|
|
197
|
+
if (refMatchers.length > 0) {
|
|
198
|
+
if (hasIdRefMatchers && elementIdForRefs) {
|
|
199
|
+
validateElementIdForRefs(elementIdForRefs);
|
|
200
|
+
}
|
|
201
|
+
const bestMatchRef = resolveBestRefMatcher({
|
|
202
|
+
elementIdForRefs,
|
|
203
|
+
classNames: classNamesForRefs,
|
|
204
|
+
refMatchers,
|
|
313
205
|
});
|
|
314
206
|
|
|
315
|
-
if (
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
const bIsExact = !b.includes("*");
|
|
319
|
-
if (aIsExact && !bIsExact) return -1;
|
|
320
|
-
if (!aIsExact && bIsExact) return 1;
|
|
321
|
-
return b.length - a.length;
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
const bestMatchRefKey = matchingRefKeys[0];
|
|
207
|
+
if (bestMatchRef) {
|
|
208
|
+
const bestMatchRefKey = bestMatchRef.refKey;
|
|
209
|
+
const matchIdentity = bestMatchRef.matchedValue || elementIdForRefs || bestMatchRefKey;
|
|
325
210
|
|
|
326
|
-
if (
|
|
327
|
-
const eventListeners =
|
|
211
|
+
if (bestMatchRef.refConfig && bestMatchRef.refConfig.eventListeners) {
|
|
212
|
+
const eventListeners = bestMatchRef.refConfig.eventListeners;
|
|
213
|
+
const eventRateLimitState = getEventRateLimitState(handlers);
|
|
328
214
|
Object.entries(eventListeners).forEach(
|
|
329
215
|
([eventType, eventConfig]) => {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
216
|
+
const stateKey = `${bestMatchRefKey}:${matchIdentity}:${eventType}`;
|
|
217
|
+
const listener = createConfiguredEventListener({
|
|
218
|
+
eventType,
|
|
219
|
+
eventConfig,
|
|
220
|
+
refKey: bestMatchRefKey,
|
|
221
|
+
handlers,
|
|
222
|
+
eventRateLimitState,
|
|
223
|
+
stateKey,
|
|
224
|
+
parseAndRenderFn: jemplParseAndRender,
|
|
225
|
+
onMissingHandler: (missingHandlerName) => {
|
|
226
|
+
console.warn(
|
|
227
|
+
`[Parser] Handler '${missingHandlerName}' for refKey '${bestMatchRefKey}' (matching '${matchIdentity}') is referenced but not found in available handlers.`,
|
|
228
|
+
);
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
if (!listener) {
|
|
342
232
|
return;
|
|
343
233
|
}
|
|
344
|
-
|
|
345
|
-
if (eventConfig.handler && handlers[eventConfig.handler]) {
|
|
346
|
-
eventHandlers[eventType] = (event) => {
|
|
347
|
-
handlers[eventConfig.handler]({
|
|
348
|
-
...eventConfig.payload,
|
|
349
|
-
_event: event,
|
|
350
|
-
});
|
|
351
|
-
};
|
|
352
|
-
} else if (eventConfig.handler) {
|
|
353
|
-
// Keep this warning for missing handlers
|
|
354
|
-
console.warn(
|
|
355
|
-
`[Parser] Handler '${eventConfig.handler}' for refKey '${bestMatchRefKey}' (matching elementId '${elementIdForRefs}') is referenced but not found in available handlers.`,
|
|
356
|
-
);
|
|
357
|
-
}
|
|
234
|
+
eventHandlers[eventType] = listener;
|
|
358
235
|
},
|
|
359
236
|
);
|
|
360
237
|
}
|
|
@@ -396,58 +273,15 @@ export const createVirtualDom = ({
|
|
|
396
273
|
snabbdomData.props = props;
|
|
397
274
|
}
|
|
398
275
|
|
|
399
|
-
//
|
|
400
|
-
if (isWebComponent) {
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
// Check if props have changed
|
|
409
|
-
const propsChanged =
|
|
410
|
-
JSON.stringify(oldProps) !== JSON.stringify(newProps);
|
|
411
|
-
|
|
412
|
-
// Check if attrs have changed
|
|
413
|
-
const attrsChanged =
|
|
414
|
-
JSON.stringify(oldAttrs) !== JSON.stringify(newAttrs);
|
|
415
|
-
|
|
416
|
-
if (propsChanged || attrsChanged) {
|
|
417
|
-
// Set isDirty attribute and trigger re-render
|
|
418
|
-
const element = vnode.elm;
|
|
419
|
-
if (
|
|
420
|
-
element &&
|
|
421
|
-
element.render &&
|
|
422
|
-
typeof element.render === "function"
|
|
423
|
-
) {
|
|
424
|
-
element.setAttribute("isDirty", "true");
|
|
425
|
-
requestAnimationFrame(() => {
|
|
426
|
-
element.render();
|
|
427
|
-
element.removeAttribute("isDirty");
|
|
428
|
-
// Call the specific component's handleOnUpdate instead of the parent's onUpdate
|
|
429
|
-
if (element.handlers && element.handlers.handleOnUpdate) {
|
|
430
|
-
const deps = {
|
|
431
|
-
...(element.deps),
|
|
432
|
-
store: element.store,
|
|
433
|
-
render: element.render.bind(element),
|
|
434
|
-
handlers: element.handlers,
|
|
435
|
-
dispatchEvent: element.dispatchEvent.bind(element),
|
|
436
|
-
refIds: element.refIds || {},
|
|
437
|
-
getRefIds: () => element.refIds || {},
|
|
438
|
-
};
|
|
439
|
-
element.handlers.handleOnUpdate(deps, {
|
|
440
|
-
oldProps,
|
|
441
|
-
newProps,
|
|
442
|
-
oldAttrs,
|
|
443
|
-
newAttrs,
|
|
444
|
-
});
|
|
445
|
-
}
|
|
446
|
-
});
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
},
|
|
450
|
-
};
|
|
276
|
+
// Hook behavior is injected so parser core stays environment-agnostic.
|
|
277
|
+
if (isWebComponent && typeof createComponentUpdateHook === "function") {
|
|
278
|
+
const componentHook = createComponentUpdateHook({
|
|
279
|
+
selector,
|
|
280
|
+
tagName,
|
|
281
|
+
});
|
|
282
|
+
if (componentHook) {
|
|
283
|
+
snabbdomData.hook = componentHook;
|
|
284
|
+
}
|
|
451
285
|
}
|
|
452
286
|
|
|
453
287
|
try {
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const COMMON_LINK_STYLE_TEXT = `
|
|
2
|
+
a, a:link, a:visited, a:hover, a:active {
|
|
3
|
+
display: contents;
|
|
4
|
+
color: inherit;
|
|
5
|
+
text-decoration: none;
|
|
6
|
+
background: none;
|
|
7
|
+
border: none;
|
|
8
|
+
padding: 0;
|
|
9
|
+
margin: 0;
|
|
10
|
+
font: inherit;
|
|
11
|
+
cursor: pointer;
|
|
12
|
+
}
|
|
13
|
+
`;
|
|
14
|
+
|
|
15
|
+
export const initializeComponentDom = ({
|
|
16
|
+
host,
|
|
17
|
+
cssText,
|
|
18
|
+
createStyleSheet = () => new CSSStyleSheet(),
|
|
19
|
+
createElement = (tagName) => document.createElement(tagName),
|
|
20
|
+
}) => {
|
|
21
|
+
const shadow = host.attachShadow({ mode: "open" });
|
|
22
|
+
|
|
23
|
+
const commonStyleSheet = createStyleSheet();
|
|
24
|
+
commonStyleSheet.replaceSync(COMMON_LINK_STYLE_TEXT);
|
|
25
|
+
|
|
26
|
+
const adoptedStyleSheets = [commonStyleSheet];
|
|
27
|
+
|
|
28
|
+
if (cssText) {
|
|
29
|
+
const styleSheet = createStyleSheet();
|
|
30
|
+
styleSheet.replaceSync(cssText);
|
|
31
|
+
adoptedStyleSheets.push(styleSheet);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
shadow.adoptedStyleSheets = adoptedStyleSheets;
|
|
35
|
+
|
|
36
|
+
const renderTarget = createElement("div");
|
|
37
|
+
renderTarget.style.cssText = "display: contents;";
|
|
38
|
+
shadow.appendChild(renderTarget);
|
|
39
|
+
if (!renderTarget.parentNode) {
|
|
40
|
+
host.appendChild(renderTarget);
|
|
41
|
+
}
|
|
42
|
+
host.style.display = "contents";
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
shadow,
|
|
46
|
+
renderTarget,
|
|
47
|
+
adoptedStyleSheets,
|
|
48
|
+
};
|
|
49
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { scheduleFrame } from "./scheduler.js";
|
|
2
|
+
|
|
3
|
+
export const createWebComponentUpdateHook = ({
|
|
4
|
+
scheduleFrameFn = scheduleFrame,
|
|
5
|
+
} = {}) => {
|
|
6
|
+
return {
|
|
7
|
+
update: (oldVnode, vnode) => {
|
|
8
|
+
const oldProps = oldVnode.data?.props || {};
|
|
9
|
+
const newProps = vnode.data?.props || {};
|
|
10
|
+
|
|
11
|
+
const propsChanged = JSON.stringify(oldProps) !== JSON.stringify(newProps);
|
|
12
|
+
if (!propsChanged) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const element = vnode.elm;
|
|
17
|
+
if (!element || typeof element.render !== "function") {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
element.setAttribute("isDirty", "true");
|
|
22
|
+
scheduleFrameFn(() => {
|
|
23
|
+
element.render();
|
|
24
|
+
element.removeAttribute("isDirty");
|
|
25
|
+
|
|
26
|
+
if (element.handlers && element.handlers.handleOnUpdate) {
|
|
27
|
+
const deps = {
|
|
28
|
+
...(element.deps),
|
|
29
|
+
store: element.store,
|
|
30
|
+
render: element.render.bind(element),
|
|
31
|
+
handlers: element.handlers,
|
|
32
|
+
dispatchEvent: element.dispatchEvent.bind(element),
|
|
33
|
+
refs: element.refIds || {},
|
|
34
|
+
};
|
|
35
|
+
element.handlers.handleOnUpdate(deps, {
|
|
36
|
+
oldProps,
|
|
37
|
+
newProps,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
};
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { parseAndRender } from "jempl";
|
|
2
|
+
import { bindMethods } from "../core/runtime/methods.js";
|
|
3
|
+
import { bindStore } from "../core/runtime/store.js";
|
|
4
|
+
import { resolveConstants } from "../core/runtime/constants.js";
|
|
5
|
+
import { yamlToCss } from "../core/style/yamlToCss.js";
|
|
6
|
+
import {
|
|
7
|
+
runAttributeChangedComponentLifecycle,
|
|
8
|
+
runConnectedComponentLifecycle,
|
|
9
|
+
runDisconnectedComponentLifecycle,
|
|
10
|
+
runRenderComponentLifecycle,
|
|
11
|
+
} from "../core/runtime/componentOrchestrator.js";
|
|
12
|
+
import { createPropsProxy, toKebabCase } from "../core/runtime/props.js";
|
|
13
|
+
import {
|
|
14
|
+
buildObservedAttributes,
|
|
15
|
+
} from "../core/runtime/componentRuntime.js";
|
|
16
|
+
import { initializeComponentDom } from "./componentDom.js";
|
|
17
|
+
import { createWebComponentUpdateHook } from "./componentUpdateHook.js";
|
|
18
|
+
import { scheduleFrame } from "./scheduler.js";
|
|
19
|
+
|
|
20
|
+
export const createWebComponentClass = ({
|
|
21
|
+
elementName,
|
|
22
|
+
propsSchema,
|
|
23
|
+
propsSchemaKeys,
|
|
24
|
+
template,
|
|
25
|
+
refs,
|
|
26
|
+
styles,
|
|
27
|
+
handlers,
|
|
28
|
+
methods,
|
|
29
|
+
constants,
|
|
30
|
+
store,
|
|
31
|
+
patch,
|
|
32
|
+
h,
|
|
33
|
+
deps,
|
|
34
|
+
}) => {
|
|
35
|
+
class BaseComponent extends HTMLElement {
|
|
36
|
+
elementName;
|
|
37
|
+
styles;
|
|
38
|
+
h;
|
|
39
|
+
store;
|
|
40
|
+
props;
|
|
41
|
+
propsSchema;
|
|
42
|
+
template;
|
|
43
|
+
handlers;
|
|
44
|
+
methods;
|
|
45
|
+
constants;
|
|
46
|
+
transformedHandlers = {};
|
|
47
|
+
refs;
|
|
48
|
+
refIds = {};
|
|
49
|
+
patch;
|
|
50
|
+
_unmountCallback;
|
|
51
|
+
_globalListenersCleanup;
|
|
52
|
+
_oldVNode;
|
|
53
|
+
deps;
|
|
54
|
+
_propsSchemaKeys = [];
|
|
55
|
+
cssText;
|
|
56
|
+
|
|
57
|
+
static get observedAttributes() {
|
|
58
|
+
return ["key"];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
get viewData() {
|
|
62
|
+
let data = {};
|
|
63
|
+
if (this.store.selectViewData) {
|
|
64
|
+
data = this.store.selectViewData();
|
|
65
|
+
}
|
|
66
|
+
return data;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
connectedCallback() {
|
|
70
|
+
const dom = initializeComponentDom({
|
|
71
|
+
host: this,
|
|
72
|
+
cssText: this.cssText,
|
|
73
|
+
});
|
|
74
|
+
this.shadow = dom.shadow;
|
|
75
|
+
this.renderTarget = dom.renderTarget;
|
|
76
|
+
runConnectedComponentLifecycle({
|
|
77
|
+
instance: this,
|
|
78
|
+
parseAndRenderFn: parseAndRender,
|
|
79
|
+
renderFn: this.render,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
disconnectedCallback() {
|
|
84
|
+
runDisconnectedComponentLifecycle({
|
|
85
|
+
instance: this,
|
|
86
|
+
clearTimerFn: clearTimeout,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
91
|
+
runAttributeChangedComponentLifecycle({
|
|
92
|
+
instance: this,
|
|
93
|
+
attributeName: name,
|
|
94
|
+
oldValue,
|
|
95
|
+
newValue,
|
|
96
|
+
scheduleFrameFn: scheduleFrame,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
render = () => {
|
|
101
|
+
runRenderComponentLifecycle({
|
|
102
|
+
instance: this,
|
|
103
|
+
createComponentUpdateHookFn: createWebComponentUpdateHook,
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
class MyComponent extends BaseComponent {
|
|
109
|
+
static get observedAttributes() {
|
|
110
|
+
return buildObservedAttributes({
|
|
111
|
+
propsSchemaKeys,
|
|
112
|
+
toKebabCase,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
constructor() {
|
|
117
|
+
super();
|
|
118
|
+
this.constants = resolveConstants({
|
|
119
|
+
setupConstants: deps?.constants,
|
|
120
|
+
fileConstants: constants,
|
|
121
|
+
});
|
|
122
|
+
this.propsSchema = propsSchema;
|
|
123
|
+
this.props = propsSchema
|
|
124
|
+
? createPropsProxy(this, propsSchemaKeys)
|
|
125
|
+
: {};
|
|
126
|
+
this._propsSchemaKeys = propsSchemaKeys;
|
|
127
|
+
this.elementName = elementName;
|
|
128
|
+
this.styles = styles;
|
|
129
|
+
this.store = bindStore(store, this.props, this.constants);
|
|
130
|
+
this.template = template;
|
|
131
|
+
this.handlers = handlers;
|
|
132
|
+
this.methods = methods;
|
|
133
|
+
this.refs = refs;
|
|
134
|
+
this.patch = patch;
|
|
135
|
+
this.deps = {
|
|
136
|
+
...deps,
|
|
137
|
+
store: this.store,
|
|
138
|
+
render: this.render,
|
|
139
|
+
handlers,
|
|
140
|
+
props: this.props,
|
|
141
|
+
constants: this.constants,
|
|
142
|
+
};
|
|
143
|
+
bindMethods(this, this.methods);
|
|
144
|
+
this.h = h;
|
|
145
|
+
this.cssText = yamlToCss(elementName, styles);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return MyComponent;
|
|
150
|
+
};
|