fusee-framework 2.7.2 → 2.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,311 +1,323 @@
1
- import { effect, batch } from './signal.js'
2
- import { processDirectives } from './directives.js'
3
- import { parseSlots } from './component-core.js'
4
- import { resolveElementProperty } from './element-props.js'
5
- import {
6
- evaluateExpression,
7
- parseInterpolation,
8
- sanitizeAttr,
9
- MUSTACHE_RE
10
- } from './evaluator.js'
11
-
12
- export function mountTemplate(template, container, context, components) {
13
- const effects = []
14
-
15
- // Lower component tags before the browser parses the template as HTML.
16
- let processed = transformComponentTags(template, components)
17
-
18
- for (const name of Object.keys(components)) {
19
- const escapedName = escapeRegExp(name)
20
- const reWithSlot = new RegExp(
21
- `\\{\\{\\s*${escapedName}(\\s[^}]*?|\\s*)\\}\\}([\\s\\S]*?)\\{\\{\\s*\\/${escapedName}\\s*\\}\\}`,
22
- 'g'
23
- )
24
- const reNoSlot = new RegExp(`\\{\\{\\s*${escapedName}(\\s+.*?|\\s*)\\}\\}`, 'g')
25
-
26
- processed = processed.replace(reWithSlot, (match, propsStr, slotContent) => {
27
- const attrs = parseComponentAttrs(propsStr || '')
28
- const encodedSlot = encodeURIComponent(slotContent.trim())
29
- return `<div data-component="${name}"${attrs} data-slot="${encodedSlot}"></div>`
30
- })
31
-
32
- processed = processed.replace(reNoSlot, (match, propsStr) => {
33
- const attrs = parseComponentAttrs(propsStr || '')
34
- return `<div data-component="${name}"${attrs}></div>`
35
- })
36
- }
37
-
38
- container.innerHTML = rewriteBindingAttrs(processed)
39
- compileNode(container, context, components, effects)
40
-
41
- return { effects }
42
- }
43
-
44
- const TAG_RE = /<[a-zA-Z][a-zA-Z0-9-]*(?:"[^"]*"|'[^']*'|[^'">])*>/g
45
- const BINDING_IN_TAG_RE = /("[^"]*"|'[^']*')|(\s):([a-zA-Z][a-zA-Z0-9-]*)=/g
46
-
47
- /**
48
- * Rewrites every `:attr=` inside an element tag to `data-bind-attr=`, since `:` is not a
49
- * valid attribute name for the HTML parser. Quoted values are skipped, so expressions
50
- * containing ` :x=` or `>` are left intact. Runs after component tags are lowered, so
51
- * component props keep their own `data-bind-prop-` encoding.
52
- */
53
- function rewriteBindingAttrs(template) {
54
- return template.replace(TAG_RE, tag => tag.replace(BINDING_IN_TAG_RE, (match, quoted, space, name) =>
55
- quoted ? quoted : `${space}data-bind-${name}=`
56
- ))
57
- }
58
-
59
- function transformComponentTags(template, components) {
60
- let processed = template
61
- const componentNames = Object.keys(components).sort((a, b) => b.length - a.length)
62
-
63
- for (const name of componentNames) {
64
- const escapedName = escapeRegExp(name)
65
- const withChildren = new RegExp(
66
- `<${escapedName}(\\s[^>]*)?>([\\s\\S]*?)<\\/${escapedName}\\s*>`,
67
- 'gi'
68
- )
69
- const selfClosing = new RegExp(
70
- `<${escapedName}(\\s[^>]*?)?\\s*/>`,
71
- 'gi'
72
- )
73
-
74
- processed = processed.replace(withChildren, (match, propsStr, slotContent) => {
75
- const attrs = parseComponentAttrs(propsStr || '')
76
- const encodedSlot = encodeURIComponent(slotContent.trim())
77
- return `<div data-component="${name}"${attrs} data-slot="${encodedSlot}"></div>`
78
- })
79
-
80
- processed = processed.replace(selfClosing, (match, propsStr) => {
81
- const attrs = parseComponentAttrs(propsStr || '')
82
- return `<div data-component="${name}"${attrs}></div>`
83
- })
84
- }
85
-
86
- return processed
87
- }
88
-
89
- function escapeRegExp(value) {
90
- return value.replace(/[.*+?^${}()|[\\]\\]/g, '\\$&')
91
- }
92
-
93
- function parseComponentAttrs(propsStr) {
94
- let attrs = ''
95
- if (!propsStr || !propsStr.trim()) return attrs
96
-
97
- const attrRegex = /(@?:?[a-zA-Z0-9-]+)="([^"]*)"/g
98
- let attrMatch
99
- while ((attrMatch = attrRegex.exec(propsStr)) !== null) {
100
- const attrName = attrMatch[1]
101
- const attrValue = attrMatch[2]
102
-
103
- if (attrName.startsWith('@')) {
104
- attrs += ` data-on-${attrName.slice(1).toLowerCase()}="${attrValue}"`
105
- } else if (attrName.startsWith(':')) {
106
- attrs += ` data-bind-prop-${attrName.slice(1).toLowerCase()}="${attrValue}"`
107
- } else {
108
- attrs += ` data-prop-${attrName.toLowerCase()}="${attrValue}"`
109
- }
110
- }
111
- return attrs
112
- }
113
-
114
- export function compileNode(node, context, components, effects) {
115
- if (node.nodeType === 1 && node.hasAttribute('f-once')) {
116
- node.removeAttribute('f-once')
117
- const temporaryEffects = []
118
-
119
- processDirectives(node, context, components, temporaryEffects)
120
- processBindingAttrs(node, context, temporaryEffects)
121
- processMustaches(node, context, temporaryEffects)
122
- bindComponents(node, components, context, temporaryEffects)
123
-
124
- let child = node.firstChild
125
- while (child) {
126
- compileOnce(child, context, components, temporaryEffects)
127
- child = child.nextSibling
128
- }
129
-
130
- for (let i = temporaryEffects.length - 1; i >= 0; i--) {
131
- const cleanup = temporaryEffects[i]
132
- if (typeof cleanup === 'function') cleanup()
133
- }
134
- return
135
- }
136
-
137
- if (node.nodeType === 1) {
138
- if (processDirectives(node, context, components, effects)) return
139
- processBindingAttrs(node, context, effects)
140
- bindComponents(node, components, context, effects)
141
- } else if (node.nodeType === 3) {
142
- processMustaches(node, context, effects)
143
- }
144
-
145
- let child = node.firstChild
146
- while (child) {
147
- compileNode(child, context, components, effects)
148
- child = child.nextSibling
149
- }
150
- }
151
-
152
- function compileOnce(node, context, components, effects) {
153
- if (node.nodeType === 1) {
154
- if (processDirectives(node, context, components, effects)) return
155
- processBindingAttrs(node, context, effects)
156
- bindComponents(node, components, context, effects)
157
- } else if (node.nodeType === 3) {
158
- processMustaches(node, context, effects)
159
- }
160
-
161
- let child = node.firstChild
162
- while (child) {
163
- compileOnce(child, context, components, effects)
164
- child = child.nextSibling
165
- }
166
- }
167
-
168
- function processMustaches(node, context, effects) {
169
- if (node.nodeType !== 3) return
170
- const text = node.textContent
171
- if (!MUSTACHE_RE.test(text)) return
172
-
173
- const parts = parseInterpolation(text)
174
- const fragment = document.createDocumentFragment()
175
-
176
- for (const part of parts) {
177
- if (part.type === 'static') {
178
- fragment.appendChild(document.createTextNode(part.value))
179
- } else {
180
- const reactiveNode = document.createTextNode('')
181
- fragment.appendChild(reactiveNode)
182
- const key = part.key
183
- const e = effect(() => {
184
- const resolved = evaluateExpression(key, context)
185
- reactiveNode.textContent = String(resolved ?? '')
186
- })
187
- effects.push(e)
188
- }
189
- }
190
- node.parentNode.replaceChild(fragment, node)
191
- }
192
-
193
- function processBindingAttrs(el, context, effects) {
194
- if (el.nodeType !== 1) return
195
- for (const attr of [...el.attributes]) {
196
- if (attr.name.startsWith('data-bind-') && !attr.name.startsWith('data-bind-prop-')) {
197
- const realAttr = attr.name.slice(10)
198
- const expr = attr.value
199
- el.removeAttribute(attr.name)
200
-
201
- const e = effect(() => {
202
- let resolved = evaluateExpression(expr, context)
203
- const elementProp = realAttr !== 'class' && realAttr !== 'style'
204
- ? resolveElementProperty(el, realAttr, resolved)
205
- : null
206
- if (elementProp) {
207
- el[elementProp] = resolved
208
- } else if (realAttr === 'class') {
209
- if (!el._staticClass) el._staticClass = el.getAttribute('class') || ''
210
- let dynamicClass = ''
211
- if (Array.isArray(resolved)) dynamicClass = resolved.filter(Boolean).join(' ')
212
- else if (resolved !== null && typeof resolved === 'object') {
213
- dynamicClass = Object.entries(resolved).filter(([_, v]) => !!v).map(([k]) => k).join(' ')
214
- } else dynamicClass = String(resolved ?? '')
215
- el.className = (el._staticClass + ' ' + dynamicClass).trim()
216
- } else if (realAttr === 'style') {
217
- if (resolved !== null && typeof resolved === 'object') {
218
- for (const k in resolved) el.style[k] = resolved[k]
219
- } else el.style.cssText = String(resolved ?? '')
220
- } else {
221
- if (typeof resolved === 'boolean') {
222
- resolved ? el.setAttribute(realAttr, '') : el.removeAttribute(realAttr)
223
- } else el.setAttribute(realAttr, sanitizeAttr(realAttr, String(resolved)))
224
- }
225
- })
226
- effects.push(e)
227
- continue
228
- }
229
-
230
- if (MUSTACHE_RE.test(attr.value)) {
231
- const attrName = attr.name
232
- const parts = parseInterpolation(attr.value)
233
- el.removeAttribute(attrName)
234
- const e = effect(() => {
235
- let result = ''
236
- for (const part of parts) {
237
- result += part.type === 'static' ? part.value : String(evaluateExpression(part.key, context) ?? '')
238
- }
239
- el.setAttribute(attrName, sanitizeAttr(attrName, result))
240
- })
241
- effects.push(e)
242
- }
243
- }
244
- }
245
-
246
- function bindComponents(el, components, context, effects) {
247
- if (el.nodeType !== 1) return
248
- const name = el.getAttribute('data-component')
249
- if (!name) return
250
-
251
- el.removeAttribute('data-component')
252
-
253
- const ComponentFn = components[name]
254
- if (!ComponentFn) return
255
-
256
- const props = {}
257
- const listeners = {}
258
-
259
- for (const attr of [...el.attributes]) {
260
- const attrName = attr.name.toLowerCase()
261
-
262
- if (attrName.startsWith('data-prop-')) {
263
- props[attrName.slice(10)] = attr.value
264
-
265
- } else if (attrName.startsWith('data-bind-prop-')) {
266
- const propName = attrName.slice(15)
267
- const expression = attr.value.trim()
268
-
269
- if (expression in context) {
270
- Object.defineProperty(props, propName, {
271
- get() {
272
- const val = context[expression]
273
- return typeof val === 'function' && val.isSignal ? val() : val
274
- },
275
- enumerable: true,
276
- configurable: true
277
- })
278
- } else {
279
- Object.defineProperty(props, propName, {
280
- get() { return evaluateExpression(expression, context) },
281
- enumerable: true,
282
- configurable: true
283
- })
284
- }
285
-
286
- } else if (attrName.startsWith('data-on-')) {
287
- const eventName = attrName.slice(8)
288
- const handlerExpr = attr.value.trim()
289
-
290
- listeners[eventName] = (...args) => {
291
- const handler = context[handlerExpr]
292
- if (typeof handler === 'function') {
293
- handler(...args)
294
- } else {
295
- evaluateExpression(handlerExpr, context)
296
- }
297
- }
298
- }
299
- }
300
-
301
- const rawSlot = el.getAttribute('data-slot')
302
- const slots = rawSlot ? parseSlots(decodeURIComponent(rawSlot)) : {}
303
- el.removeAttribute('data-slot')
304
-
305
- const childComponent = ComponentFn(props, { listeners, slots, parent: context._instance })
306
- childComponent.render(el)
307
-
308
- if (effects) {
309
- effects.push(() => childComponent.unmount())
310
- }
1
+ import { effect, batch } from './signal.js'
2
+ import { processDirectives } from './directives.js'
3
+ import { parseSlots } from './component-core.js'
4
+ import { resolveElementProperty } from './element-props.js'
5
+ import {
6
+ evaluateExpression,
7
+ parseInterpolation,
8
+ sanitizeAttr,
9
+ MUSTACHE_RE
10
+ } from './evaluator.js'
11
+
12
+ export function mountTemplate(template, container, context, components) {
13
+ const effects = []
14
+
15
+ // Lower component tags before the browser parses the template as HTML.
16
+ let processed = transformComponentTags(template, components)
17
+
18
+ for (const name of Object.keys(components)) {
19
+ const escapedName = escapeRegExp(name)
20
+ const reWithSlot = new RegExp(
21
+ `\\{\\{\\s*${escapedName}(\\s[^}]*?|\\s*)\\}\\}([\\s\\S]*?)\\{\\{\\s*\\/${escapedName}\\s*\\}\\}`,
22
+ 'g'
23
+ )
24
+ const reNoSlot = new RegExp(`\\{\\{\\s*${escapedName}(\\s+.*?|\\s*)\\}\\}`, 'g')
25
+
26
+ processed = processed.replace(reWithSlot, (match, propsStr, slotContent) => {
27
+ const attrs = parseComponentAttrs(propsStr || '')
28
+ const encodedSlot = encodeURIComponent(slotContent.trim())
29
+ return `<div data-component="${name}"${attrs} data-slot="${encodedSlot}"></div>`
30
+ })
31
+
32
+ processed = processed.replace(reNoSlot, (match, propsStr) => {
33
+ const attrs = parseComponentAttrs(propsStr || '')
34
+ return `<div data-component="${name}"${attrs}></div>`
35
+ })
36
+ }
37
+
38
+ container.innerHTML = rewriteBindingAttrs(processed)
39
+ compileNode(container, context, components, effects)
40
+
41
+ return { effects }
42
+ }
43
+
44
+ const TAG_RE = /<[a-zA-Z][a-zA-Z0-9-]*(?:"[^"]*"|'[^']*'|[^'">])*>/g
45
+ const BINDING_IN_TAG_RE = /("[^"]*"|'[^']*')|(\s):([a-zA-Z][a-zA-Z0-9-]*)=/g
46
+
47
+ /**
48
+ * Rewrites every `:attr=` inside an element tag to `data-bind-attr=`, since `:` is not a
49
+ * valid attribute name for the HTML parser. Quoted values are skipped, so expressions
50
+ * containing ` :x=` or `>` are left intact. Runs after component tags are lowered, so
51
+ * component props keep their own `data-bind-prop-` encoding.
52
+ */
53
+ function rewriteBindingAttrs(template) {
54
+ return template.replace(TAG_RE, tag => tag.replace(BINDING_IN_TAG_RE, (match, quoted, space, name) =>
55
+ quoted ? quoted : `${space}data-bind-${name}=`
56
+ ))
57
+ }
58
+
59
+ function transformComponentTags(template, components) {
60
+ let processed = template
61
+ const componentNames = Object.keys(components).sort((a, b) => b.length - a.length)
62
+
63
+ for (const name of componentNames) {
64
+ const escapedName = escapeRegExp(name)
65
+ const withChildren = new RegExp(
66
+ `<${escapedName}(\\s[^>]*)?>([\\s\\S]*?)<\\/${escapedName}\\s*>`,
67
+ 'gi'
68
+ )
69
+ const selfClosing = new RegExp(
70
+ `<${escapedName}(\\s[^>]*?)?\\s*/>`,
71
+ 'gi'
72
+ )
73
+
74
+ processed = processed.replace(withChildren, (match, propsStr, slotContent) => {
75
+ const attrs = parseComponentAttrs(propsStr || '')
76
+ const encodedSlot = encodeURIComponent(slotContent.trim())
77
+ return `<div data-component="${name}"${attrs} data-slot="${encodedSlot}"></div>`
78
+ })
79
+
80
+ processed = processed.replace(selfClosing, (match, propsStr) => {
81
+ const attrs = parseComponentAttrs(propsStr || '')
82
+ return `<div data-component="${name}"${attrs}></div>`
83
+ })
84
+ }
85
+
86
+ return processed
87
+ }
88
+
89
+ function escapeRegExp(value) {
90
+ return value.replace(/[.*+?^${}()|[\\]\\]/g, '\\$&')
91
+ }
92
+
93
+ function parseComponentAttrs(propsStr) {
94
+ let attrs = ''
95
+ if (!propsStr || !propsStr.trim()) return attrs
96
+
97
+ const attrRegex = /(@?:?[a-zA-Z0-9-]+)="([^"]*)"/g
98
+ let attrMatch
99
+ while ((attrMatch = attrRegex.exec(propsStr)) !== null) {
100
+ const attrName = attrMatch[1]
101
+ const attrValue = attrMatch[2]
102
+
103
+ if (attrName.startsWith('@')) {
104
+ attrs += ` data-on-${attrName.slice(1).toLowerCase()}="${attrValue}"`
105
+ } else if (attrName.startsWith(':')) {
106
+ attrs += ` data-bind-prop-${attrName.slice(1).toLowerCase()}="${attrValue}"`
107
+ } else {
108
+ attrs += ` data-prop-${attrName.toLowerCase()}="${attrValue}"`
109
+ }
110
+ }
111
+ return attrs
112
+ }
113
+
114
+ export function compileNode(node, context, components, effects) {
115
+ if (node.nodeType === 1 && node.hasAttribute('f-once')) {
116
+ node.removeAttribute('f-once')
117
+ const temporaryEffects = []
118
+ const ownsContent = setsOwnContent(node)
119
+
120
+ processDirectives(node, context, components, temporaryEffects)
121
+ processBindingAttrs(node, context, temporaryEffects)
122
+ processMustaches(node, context, temporaryEffects)
123
+ bindComponents(node, components, context, temporaryEffects)
124
+
125
+ if (!ownsContent) forEachChild(node, child => compileOnce(child, context, components, temporaryEffects))
126
+
127
+ for (let i = temporaryEffects.length - 1; i >= 0; i--) {
128
+ const cleanup = temporaryEffects[i]
129
+ if (typeof cleanup === 'function') cleanup()
130
+ }
131
+ return
132
+ }
133
+
134
+ if (node.nodeType === 1) {
135
+ const ownsContent = setsOwnContent(node)
136
+ if (processDirectives(node, context, components, effects)) return
137
+ processBindingAttrs(node, context, effects)
138
+ bindComponents(node, components, context, effects)
139
+ if (ownsContent) return
140
+ } else if (node.nodeType === 3) {
141
+ processMustaches(node, context, effects)
142
+ }
143
+
144
+ forEachChild(node, child => compileNode(child, context, components, effects))
145
+ }
146
+
147
+ /**
148
+ * f-text / f-html write the element's content from data. That content is output, not
149
+ * template: compiling it would evaluate any {{ }} or directives a value happens to contain.
150
+ */
151
+ function setsOwnContent(node) {
152
+ return node.hasAttribute('f-text') || node.hasAttribute('f-html')
153
+ }
154
+
155
+ /**
156
+ * Visits the children present when the walk starts. Compiling a child can replace it
157
+ * (interpolated text becomes a fragment) or detach later siblings (an f-if group), so
158
+ * following child.nextSibling would stop early or visit detached nodes.
159
+ */
160
+ function forEachChild(node, visit) {
161
+ for (const child of Array.from(node.childNodes)) {
162
+ if (child.parentNode === node) visit(child)
163
+ }
164
+ }
165
+
166
+ function compileOnce(node, context, components, effects) {
167
+ if (node.nodeType === 1) {
168
+ const ownsContent = setsOwnContent(node)
169
+ if (processDirectives(node, context, components, effects)) return
170
+ processBindingAttrs(node, context, effects)
171
+ bindComponents(node, components, context, effects)
172
+ if (ownsContent) return
173
+ } else if (node.nodeType === 3) {
174
+ processMustaches(node, context, effects)
175
+ }
176
+
177
+ forEachChild(node, child => compileOnce(child, context, components, effects))
178
+ }
179
+
180
+ function processMustaches(node, context, effects) {
181
+ if (node.nodeType !== 3) return
182
+ const text = node.textContent
183
+ if (!MUSTACHE_RE.test(text)) return
184
+
185
+ const parts = parseInterpolation(text)
186
+ const fragment = document.createDocumentFragment()
187
+
188
+ for (const part of parts) {
189
+ if (part.type === 'static') {
190
+ fragment.appendChild(document.createTextNode(part.value))
191
+ } else {
192
+ const reactiveNode = document.createTextNode('')
193
+ fragment.appendChild(reactiveNode)
194
+ const key = part.key
195
+ const e = effect(() => {
196
+ const resolved = evaluateExpression(key, context)
197
+ reactiveNode.textContent = String(resolved ?? '')
198
+ })
199
+ effects.push(e)
200
+ }
201
+ }
202
+ node.parentNode.replaceChild(fragment, node)
203
+ }
204
+
205
+ function processBindingAttrs(el, context, effects) {
206
+ if (el.nodeType !== 1) return
207
+ for (const attr of [...el.attributes]) {
208
+ if (attr.name.startsWith('data-bind-') && !attr.name.startsWith('data-bind-prop-')) {
209
+ const realAttr = attr.name.slice(10)
210
+ const expr = attr.value
211
+ el.removeAttribute(attr.name)
212
+
213
+ const e = effect(() => {
214
+ let resolved = evaluateExpression(expr, context)
215
+ const elementProp = realAttr !== 'class' && realAttr !== 'style'
216
+ ? resolveElementProperty(el, realAttr, resolved)
217
+ : null
218
+ if (elementProp) {
219
+ el[elementProp] = resolved
220
+ } else if (realAttr === 'class') {
221
+ if (!el._staticClass) el._staticClass = el.getAttribute('class') || ''
222
+ let dynamicClass = ''
223
+ if (Array.isArray(resolved)) dynamicClass = resolved.filter(Boolean).join(' ')
224
+ else if (resolved !== null && typeof resolved === 'object') {
225
+ dynamicClass = Object.entries(resolved).filter(([_, v]) => !!v).map(([k]) => k).join(' ')
226
+ } else dynamicClass = String(resolved ?? '')
227
+ el.className = (el._staticClass + ' ' + dynamicClass).trim()
228
+ } else if (realAttr === 'style') {
229
+ if (resolved !== null && typeof resolved === 'object') {
230
+ for (const k in resolved) el.style[k] = resolved[k]
231
+ } else el.style.cssText = String(resolved ?? '')
232
+ } else {
233
+ if (typeof resolved === 'boolean') {
234
+ resolved ? el.setAttribute(realAttr, '') : el.removeAttribute(realAttr)
235
+ } else el.setAttribute(realAttr, sanitizeAttr(realAttr, String(resolved)))
236
+ }
237
+ })
238
+ effects.push(e)
239
+ continue
240
+ }
241
+
242
+ if (MUSTACHE_RE.test(attr.value)) {
243
+ const attrName = attr.name
244
+ const parts = parseInterpolation(attr.value)
245
+ el.removeAttribute(attrName)
246
+ const e = effect(() => {
247
+ let result = ''
248
+ for (const part of parts) {
249
+ result += part.type === 'static' ? part.value : String(evaluateExpression(part.key, context) ?? '')
250
+ }
251
+ el.setAttribute(attrName, sanitizeAttr(attrName, result))
252
+ })
253
+ effects.push(e)
254
+ }
255
+ }
256
+ }
257
+
258
+ function bindComponents(el, components, context, effects) {
259
+ if (el.nodeType !== 1) return
260
+ const name = el.getAttribute('data-component')
261
+ if (!name) return
262
+
263
+ el.removeAttribute('data-component')
264
+
265
+ const ComponentFn = components[name]
266
+ if (!ComponentFn) return
267
+
268
+ const props = {}
269
+ const listeners = {}
270
+
271
+ for (const attr of [...el.attributes]) {
272
+ const attrName = attr.name.toLowerCase()
273
+
274
+ if (attrName.startsWith('data-prop-')) {
275
+ props[attrName.slice(10)] = attr.value
276
+
277
+ } else if (attrName.startsWith('data-bind-prop-')) {
278
+ const propName = attrName.slice(15)
279
+ const expression = attr.value.trim()
280
+
281
+ if (expression in context) {
282
+ Object.defineProperty(props, propName, {
283
+ get() {
284
+ const val = context[expression]
285
+ return typeof val === 'function' && val.isSignal ? val() : val
286
+ },
287
+ enumerable: true,
288
+ configurable: true
289
+ })
290
+ } else {
291
+ Object.defineProperty(props, propName, {
292
+ get() { return evaluateExpression(expression, context) },
293
+ enumerable: true,
294
+ configurable: true
295
+ })
296
+ }
297
+
298
+ } else if (attrName.startsWith('data-on-')) {
299
+ const eventName = attrName.slice(8)
300
+ const handlerExpr = attr.value.trim()
301
+
302
+ listeners[eventName] = (...args) => {
303
+ const handler = context[handlerExpr]
304
+ if (typeof handler === 'function') {
305
+ handler(...args)
306
+ } else {
307
+ evaluateExpression(handlerExpr, context)
308
+ }
309
+ }
310
+ }
311
+ }
312
+
313
+ const rawSlot = el.getAttribute('data-slot')
314
+ const slots = rawSlot ? parseSlots(decodeURIComponent(rawSlot)) : {}
315
+ el.removeAttribute('data-slot')
316
+
317
+ const childComponent = ComponentFn(props, { listeners, slots, parent: context._instance })
318
+ childComponent.render(el)
319
+
320
+ if (effects) {
321
+ effects.push(() => childComponent.unmount())
322
+ }
311
323
  }
@@ -0,0 +1,9 @@
1
+ // fusee-framework/di — injection tokens and the root injector.
2
+ // Component-level provide() / inject() stay in the core entry.
3
+ export {
4
+ InjectionToken,
5
+ provideGlobal,
6
+ EnvironmentInjector,
7
+ rootInjector,
8
+ runInContext
9
+ } from '../core/di.js'