@symbo.ls/element 3.14.1 → 3.14.2
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/package.json +1 -1
- package/src/create.js +114 -11
- package/src/extends.js +28 -10
- package/src/methods.js +11 -1
package/package.json
CHANGED
package/src/create.js
CHANGED
|
@@ -90,6 +90,22 @@ export function create(definition, parent, key, options = {}, attachOptions) {
|
|
|
90
90
|
const components = element.context?.components
|
|
91
91
|
if (components && (components[baseName] || components['smbls.' + baseName])) {
|
|
92
92
|
definition = { extends: baseName, ...definition }
|
|
93
|
+
} else if (
|
|
94
|
+
// FRAMEWORK-5: dev-mode warning. A key shaped like a multi-instance
|
|
95
|
+
// suffix (e.g. `Link_1`) clearly intends to auto-extend a base
|
|
96
|
+
// component, so silently rendering as a bare div is the kind of
|
|
97
|
+
// failure that takes a click test to discover. Warn loudly in dev
|
|
98
|
+
// so the typo / missing import is visible at mount time.
|
|
99
|
+
components &&
|
|
100
|
+
process.env.NODE_ENV !== 'production' &&
|
|
101
|
+
/^[A-Z]\w*_\d+$/.test(key)
|
|
102
|
+
) {
|
|
103
|
+
console.warn(
|
|
104
|
+
`[smbls] auto-extend: key "${key}" looks like a multi-instance ` +
|
|
105
|
+
`(${baseName}_N) but neither "${baseName}" nor "smbls.${baseName}" ` +
|
|
106
|
+
`is registered in context.components — element will render as a ` +
|
|
107
|
+
`plain <div>. Register the base component or rename the key.`
|
|
108
|
+
)
|
|
93
109
|
}
|
|
94
110
|
}
|
|
95
111
|
}
|
|
@@ -165,15 +181,22 @@ export function create(definition, parent, key, options = {}, attachOptions) {
|
|
|
165
181
|
// — the user's reactive function got `undefined` and either threw or
|
|
166
182
|
// rendered nothing. The frank-rewriter's `__scope` helper already chains
|
|
167
183
|
// through globalScope; this brings the runtime `el.scope` to parity.
|
|
184
|
+
// Capture scope set by the extends chain (applyExtends ran above) BEFORE
|
|
185
|
+
// we rebuild element.scope. Otherwise scope blocks declared on the base
|
|
186
|
+
// component (e.g. `Navbar.scope = { LANG_META }`) get blown away when
|
|
187
|
+
// a consumer extends without redeclaring scope (`AppNavbar: { extends:
|
|
188
|
+
// 'Navbar' }`). FA-L24.
|
|
189
|
+
const inheritedFromExtends = element.scope && typeof element.scope === 'object' ? element.scope : null
|
|
168
190
|
const ancestorScope = parent?.scope
|
|
169
191
|
const globalScope =
|
|
170
192
|
(element.context && typeof element.context.globalScope === 'object' && element.context.globalScope) || null
|
|
171
193
|
const baseScope = ancestorScope ||
|
|
172
194
|
(globalScope ? Object.create(globalScope) : null)
|
|
173
|
-
if (definition.scope !== undefined) {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
195
|
+
if (definition.scope !== undefined || inheritedFromExtends) {
|
|
196
|
+
const target = baseScope ? Object.create(baseScope) : {}
|
|
197
|
+
if (inheritedFromExtends) Object.assign(target, inheritedFromExtends)
|
|
198
|
+
if (definition.scope !== undefined) Object.assign(target, definition.scope)
|
|
199
|
+
element.scope = target
|
|
177
200
|
} else if (baseScope) {
|
|
178
201
|
element.scope = Object.create(baseScope)
|
|
179
202
|
}
|
|
@@ -304,15 +327,24 @@ export function create(definition, parent, key, options = {}, attachOptions) {
|
|
|
304
327
|
triggerLifecycle('RenderRouter', element, options)
|
|
305
328
|
|
|
306
329
|
// Start onFrame loop if defined
|
|
330
|
+
// FT-FRAMEWORK-3: Previously this returned early when `!element.node.isConnected`,
|
|
331
|
+
// killing the loop. During an initial top-down mount, the first rAF often
|
|
332
|
+
// fires before the root has been attached to document.body — so deeply
|
|
333
|
+
// nested children's onFrame loops self-terminated and never restarted.
|
|
334
|
+
// Now we reschedule unconditionally and gate only the handler call on
|
|
335
|
+
// `isConnected`. The loop stops only when the element is disposed
|
|
336
|
+
// (`element.node === null`).
|
|
307
337
|
if (typeof element.onFrame === 'function') {
|
|
308
338
|
const frameHandler = element.onFrame
|
|
309
339
|
const frameDsConfig = element.context?.designSystem
|
|
310
340
|
const tick = () => {
|
|
311
|
-
if (!element.node
|
|
312
|
-
if (
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
341
|
+
if (!element.node) return
|
|
342
|
+
if (element.node.isConnected) {
|
|
343
|
+
if (frameDsConfig) pushConfig(frameDsConfig)
|
|
344
|
+
try { frameHandler(element, element.state, element.context) } catch (e) { console.error('[DOMQL] onFrame error:', e) }
|
|
345
|
+
finally {
|
|
346
|
+
if (frameDsConfig) popConfig()
|
|
347
|
+
}
|
|
316
348
|
}
|
|
317
349
|
requestAnimationFrame(tick)
|
|
318
350
|
}
|
|
@@ -473,9 +505,75 @@ function resolveStatePath(path, parent) {
|
|
|
473
505
|
|
|
474
506
|
// --- Reactive Effects ---
|
|
475
507
|
|
|
508
|
+
function wrapCustomPropFunctions(element) {
|
|
509
|
+
const ref = element.__ref
|
|
510
|
+
const tag = element.tag || 'div'
|
|
511
|
+
const ds = element.context?.designSystem
|
|
512
|
+
|
|
513
|
+
// Two-pass: collect first, install second. Lets sibling refs like
|
|
514
|
+
// `a: fn`, `b: (el) => el.a + 1` work regardless of declaration order
|
|
515
|
+
// because every peer is already a value-producing getter once we begin
|
|
516
|
+
// evaluating any of them.
|
|
517
|
+
const targets = []
|
|
518
|
+
const ownProps = Object.keys(element)
|
|
519
|
+
for (let i = 0; i < ownProps.length; i++) {
|
|
520
|
+
const prop = ownProps[i]
|
|
521
|
+
if (typeof element[prop] !== 'function') continue
|
|
522
|
+
if (REGISTRY.has(prop)) continue
|
|
523
|
+
const firstChar = prop.charCodeAt(0)
|
|
524
|
+
if (firstChar >= 65 && firstChar <= 90) continue // child component
|
|
525
|
+
if (isEventHandler(prop)) continue
|
|
526
|
+
if (CSS_PROPS_REGISTRY[prop]) continue // handled by CSS effect loop
|
|
527
|
+
if (DEFAULT_CSS_PROPERTIES_LIST && DEFAULT_CSS_PROPERTIES_LIST.has(prop)) continue
|
|
528
|
+
if (checkAttributeByTagName(tag, prop)) continue // handled by attr effect loop
|
|
529
|
+
targets.push({ prop, fn: element[prop] })
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
for (let i = 0; i < targets.length; i++) {
|
|
533
|
+
const { prop, fn } = targets[i]
|
|
534
|
+
ref.__exec[prop] = fn
|
|
535
|
+
Object.defineProperty(element, prop, {
|
|
536
|
+
get () {
|
|
537
|
+
// Re-run fn on each read. Signal dependency tracking still works:
|
|
538
|
+
// whatever effect is reading el[prop] subscribes to state signals
|
|
539
|
+
// touched inside fn, so it re-fires on state changes. We skip a
|
|
540
|
+
// memoizing layer (createComputed) so fn never sees a peer custom
|
|
541
|
+
// prop in its raw-function form during this pass's eager init.
|
|
542
|
+
if (ds) pushConfig(ds)
|
|
543
|
+
try {
|
|
544
|
+
return fn(element, element.state, element.context)
|
|
545
|
+
} catch (e) {
|
|
546
|
+
return undefined
|
|
547
|
+
} finally {
|
|
548
|
+
if (ds) popConfig()
|
|
549
|
+
}
|
|
550
|
+
},
|
|
551
|
+
set (val) {
|
|
552
|
+
delete ref.__exec[prop]
|
|
553
|
+
Object.defineProperty(element, prop, {
|
|
554
|
+
value: val, writable: true, configurable: true, enumerable: true
|
|
555
|
+
})
|
|
556
|
+
},
|
|
557
|
+
configurable: true,
|
|
558
|
+
enumerable: true
|
|
559
|
+
})
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
476
563
|
function registerEffects(element, definition) {
|
|
477
564
|
const ref = element.__ref
|
|
478
565
|
|
|
566
|
+
// Custom-prop function reactivity (FRAMEWORK-1).
|
|
567
|
+
// Replace user-defined function-valued props (lowercase, not framework-
|
|
568
|
+
// reserved, not CSS, not HTML attribute, not event/lifecycle) with a getter
|
|
569
|
+
// that calls the function on read. Reads via `el.X` — including
|
|
570
|
+
// `el.parent.X` from descendants — return the evaluated value, and signal
|
|
571
|
+
// dependency tracking propagates through dependent effects (`text:`,
|
|
572
|
+
// `width:`, `href:`, etc.). Without this wrap, dependent effects coerce the
|
|
573
|
+
// un-invoked function via `Function.prototype.toString` and ship the
|
|
574
|
+
// arrow-function source into the DOM (e.g. "(el, s) => 8 / 10").
|
|
575
|
+
wrapCustomPropFunctions(element)
|
|
576
|
+
|
|
479
577
|
// Text effect
|
|
480
578
|
if (typeof element.text === 'function') {
|
|
481
579
|
const fn = element.text
|
|
@@ -1190,8 +1288,13 @@ function applyCssInProps(element) {
|
|
|
1190
1288
|
cssStyles[prop] = val
|
|
1191
1289
|
hasStyles = true
|
|
1192
1290
|
}
|
|
1193
|
-
} else if (prop.charCodeAt(0) === 45
|
|
1194
|
-
//
|
|
1291
|
+
} else if (prop.charCodeAt(0) === 45) {
|
|
1292
|
+
// FT-FRAMEWORK-4: any dash-prefixed CSS key — both custom properties
|
|
1293
|
+
// (`--var-name`) and vendor-prefixed properties
|
|
1294
|
+
// (`-webkit-text-stroke`, `-moz-osx-font-smoothing`, …) — pass
|
|
1295
|
+
// straight through the atomic CSS engine. Previously only `--`
|
|
1296
|
+
// matched; `-webkit-text-stroke: 1px darkgrey` at the top level was
|
|
1297
|
+
// silently dropped, forcing consumers to wrap it in `style: { ... }`.
|
|
1195
1298
|
const val = element[prop]
|
|
1196
1299
|
if (val != null && typeof val !== 'object') {
|
|
1197
1300
|
cssStyles[prop] = val
|
package/src/extends.js
CHANGED
|
@@ -5,16 +5,33 @@
|
|
|
5
5
|
|
|
6
6
|
const extendsCache = new WeakMap()
|
|
7
7
|
|
|
8
|
+
// FT-FRAMEWORK-EXTEND-CUSTOM: collect entries from BOTH `extends:`
|
|
9
|
+
// (plural) and `extend:` (singular). Previously the resolver did
|
|
10
|
+
// `definition.extends || definition.extend`, silently dropping one when
|
|
11
|
+
// the other was set. childsafetynet's blog-detail pages used both
|
|
12
|
+
// (`extends: 'Page'` for route-mode + `extend: 'BlogArticle'` for the
|
|
13
|
+
// article shell) and only Page resolved — BlogArticle's child keys
|
|
14
|
+
// never reached the render tree.
|
|
15
|
+
function collectExtendsList (definition) {
|
|
16
|
+
const list = []
|
|
17
|
+
if (definition.extends) {
|
|
18
|
+
if (Array.isArray(definition.extends)) list.push(...definition.extends)
|
|
19
|
+
else list.push(definition.extends)
|
|
20
|
+
}
|
|
21
|
+
if (definition.extend) {
|
|
22
|
+
if (Array.isArray(definition.extend)) list.push(...definition.extend)
|
|
23
|
+
else list.push(definition.extend)
|
|
24
|
+
}
|
|
25
|
+
return list
|
|
26
|
+
}
|
|
27
|
+
|
|
8
28
|
export function applyExtends(element, definition, options) {
|
|
9
|
-
|
|
10
|
-
if (!ext) return
|
|
29
|
+
const ext = collectExtendsList(definition)
|
|
30
|
+
if (!ext.length) return
|
|
11
31
|
|
|
12
32
|
const context = element.context || options?.context || {}
|
|
13
33
|
const components = context.components || {}
|
|
14
34
|
|
|
15
|
-
// Normalize to array
|
|
16
|
-
if (!Array.isArray(ext)) ext = [ext]
|
|
17
|
-
|
|
18
35
|
// Resolve extends chain
|
|
19
36
|
for (let i = 0; i < ext.length; i++) {
|
|
20
37
|
const base = resolveExtend(ext[i], components)
|
|
@@ -48,13 +65,14 @@ function mergeBase(element, base, options, visited = new Set()) {
|
|
|
48
65
|
if (visited.has(base)) return
|
|
49
66
|
visited.add(base)
|
|
50
67
|
|
|
51
|
-
// Recursively resolve base's own extends first
|
|
52
|
-
|
|
53
|
-
|
|
68
|
+
// Recursively resolve base's own extends first — combining both
|
|
69
|
+
// `extends:` (plural) and `extend:` (singular). Mirrors the entry-point
|
|
70
|
+
// collectExtendsList logic so chains like Page → BlogArticle → Flex
|
|
71
|
+
// resolve correctly when intermediate components mix the two forms.
|
|
72
|
+
const baseExt = collectExtendsList(base)
|
|
73
|
+
if (baseExt.length) {
|
|
54
74
|
const context = element.context || options?.context || {}
|
|
55
75
|
const components = context.components || {}
|
|
56
|
-
let baseExt = baseExtends
|
|
57
|
-
if (!Array.isArray(baseExt)) baseExt = [baseExt]
|
|
58
76
|
for (let i = 0; i < baseExt.length; i++) {
|
|
59
77
|
const ancestor = resolveExtend(baseExt[i], components)
|
|
60
78
|
if (ancestor) mergeBase(element, ancestor, options, visited)
|
package/src/methods.js
CHANGED
|
@@ -152,10 +152,20 @@ const elementProto = {
|
|
|
152
152
|
parse(exclude) {
|
|
153
153
|
const result = {}
|
|
154
154
|
const excludeSet = exclude ? new Set(exclude) : null
|
|
155
|
+
const exec = this.__ref?.__exec
|
|
155
156
|
for (const key in this) {
|
|
156
157
|
if (key === 'parent' || key === 'node' || key === '__ref') continue
|
|
157
|
-
if (typeof this[key] === 'function') continue
|
|
158
158
|
if (excludeSet?.has(key)) continue
|
|
159
|
+
// Reactive props (text/html/value/style/attr/css/custom-prop fns) are
|
|
160
|
+
// wrapped at create time; the live value is the evaluated result, but
|
|
161
|
+
// ref.__exec[key] holds the original function (or template). Restore it
|
|
162
|
+
// so reset() / parseDeep() reproduce the reactive definition rather
|
|
163
|
+
// than baking in a snapshot value.
|
|
164
|
+
if (exec && exec[key] !== undefined) {
|
|
165
|
+
result[key] = exec[key]
|
|
166
|
+
continue
|
|
167
|
+
}
|
|
168
|
+
if (typeof this[key] === 'function') continue
|
|
159
169
|
result[key] = this[key]
|
|
160
170
|
}
|
|
161
171
|
return result
|