fusee-framework 1.1.1 → 1.2.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 +2 -2
- package/app/components/Card.js +32 -0
- package/app/components/Card.ts +41 -0
- package/app/components/Counter.js +4 -5
- package/app/components/Counter.ts +2 -4
- package/app/components/CounterSection.js +10 -4
- package/app/components/EmitTest.js +77 -0
- package/app/components/EmitTest.ts +90 -0
- package/app/components/UserForm.js +1 -0
- package/app/components/UserForm.ts +0 -2
- package/app/main.ts +3 -1
- package/app/providers/ChildComponent.js +44 -0
- package/app/providers/ChildComponent.ts +47 -0
- package/app/providers/GrandchildComponent.js +41 -0
- package/app/providers/GrandchildComponent.ts +48 -0
- package/app/providers/ParentComponent.js +48 -0
- package/app/providers/ParentComponent.ts +53 -0
- package/app/providers/index.js +3 -0
- package/app/providers/index.ts +3 -0
- package/app/routes/About.js +1 -0
- package/app/routes/About.ts +1 -1
- package/app/routes/Home.js +46 -2
- package/app/routes/TestFor.js +1 -0
- package/auto-imports.d.ts +4 -0
- package/framework/auto-import-preset.js +2 -2
- package/framework/core/compiler.js +100 -56
- package/framework/core/component.js +85 -3
- package/framework/core/evaluator.js +4 -1
- package/framework/core/signal.js +22 -0
- package/framework/index.js +5 -2
- package/framework/types/component.d.ts +31 -3
- package/framework/types/signal.d.ts +1 -0
- package/index.html +0 -4
- package/package.json +1 -1
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { provide, inject } from '../../framework/index.js'
|
|
2
|
+
import { ChildComponent } from './ChildComponent.js'
|
|
3
|
+
|
|
4
|
+
interface UserData {
|
|
5
|
+
name: string
|
|
6
|
+
age: number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// ParentComponent - provides values to child components
|
|
10
|
+
export const ParentComponent = defineComponent({
|
|
11
|
+
components: { ChildComponent },
|
|
12
|
+
setup() {
|
|
13
|
+
const count = signal(42)
|
|
14
|
+
const message = signal('Hello from Parent!')
|
|
15
|
+
const userData: UserData = { name: 'John', age: 30 }
|
|
16
|
+
|
|
17
|
+
// Provide values to descendant components
|
|
18
|
+
provide('count', count)
|
|
19
|
+
provide('message', message)
|
|
20
|
+
provide('userData', userData)
|
|
21
|
+
provide('appVersion', '1.0.0')
|
|
22
|
+
|
|
23
|
+
function updateCount(): void {
|
|
24
|
+
count(count() + 10)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
count,
|
|
29
|
+
message,
|
|
30
|
+
userData,
|
|
31
|
+
updateCount,
|
|
32
|
+
template: `
|
|
33
|
+
<div style="padding: 20px; border: 2px solid #4CAF50; border-radius: 8px; margin: 20px 0; color: #000;">
|
|
34
|
+
<h2 style="color: #4CAF50;">Parent Component</h2>
|
|
35
|
+
<p>I'm providing these values to my children:</p>
|
|
36
|
+
<ul style="background: #333; padding: 15px; border-radius: 4px; color: #fff;">
|
|
37
|
+
<li><strong>count:</strong> {{ count }}</li>
|
|
38
|
+
<li><strong>message:</strong> {{ message }}</li>
|
|
39
|
+
<li><strong>userData:</strong> {{ userData.name }} ({{ userData.age }} years)</li>
|
|
40
|
+
<li><strong>appVersion:</strong> 1.0.0</li>
|
|
41
|
+
</ul>
|
|
42
|
+
<button @click="updateCount" style="margin-top: 10px; padding: 8px 16px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer;">
|
|
43
|
+
Update Count (+10)
|
|
44
|
+
</button>
|
|
45
|
+
|
|
46
|
+
<div style="margin-top: 20px;">
|
|
47
|
+
{{ ChildComponent }}
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
`
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
})
|
package/app/routes/About.js
CHANGED
package/app/routes/About.ts
CHANGED
package/app/routes/Home.js
CHANGED
|
@@ -1,13 +1,29 @@
|
|
|
1
1
|
import { UserForm } from '../components/UserForm.js'
|
|
2
2
|
import { CounterSection } from '../components/CounterSection.js'
|
|
3
|
+
import { Card } from '../components/Card.js'
|
|
4
|
+
import { EmitTest } from '../components/EmitTest.js'
|
|
5
|
+
import { ParentComponent } from '../providers/index.js'
|
|
3
6
|
|
|
4
7
|
export const Home = defineComponent({
|
|
5
|
-
components: { UserForm, CounterSection },
|
|
8
|
+
components: { UserForm, CounterSection, Card, EmitTest, ParentComponent },
|
|
6
9
|
setup() {
|
|
7
10
|
const title = signal('Welcome!')
|
|
11
|
+
inspect(title)
|
|
12
|
+
|
|
8
13
|
return {
|
|
9
14
|
title,
|
|
10
15
|
changeTitle: () => title('Changed Title!'),
|
|
16
|
+
|
|
17
|
+
// 🧪 Emit Event Handlers
|
|
18
|
+
handleCounterChange: (value) => console.log('📢 Counter changed to:', value),
|
|
19
|
+
handleCounterReset: () => console.log('🔄 Counter reset requested!'),
|
|
20
|
+
handleCustomEvent: (count, multiplier) => console.log('🎯 Custom event:', { count, multiplier }),
|
|
21
|
+
|
|
22
|
+
// 🧪 Emit Test Handlers
|
|
23
|
+
handleSimpleClick: () => console.log('✅ SIMPLE CLICK received from child!'),
|
|
24
|
+
handleClickWithData: (count, label, timestamp) => console.log('✅ CLICK WITH DATA:', { count, label, timestamp }),
|
|
25
|
+
handleObjectEvent: (data) => console.log('✅ OBJECT EVENT received:', data),
|
|
26
|
+
|
|
11
27
|
template: `
|
|
12
28
|
<div class="page">
|
|
13
29
|
<h1>{{ title.toUpperCase() }}</h1>
|
|
@@ -18,7 +34,35 @@ export const Home = defineComponent({
|
|
|
18
34
|
|
|
19
35
|
{{ UserForm }}
|
|
20
36
|
|
|
21
|
-
{{ CounterSection :pageTitle="title" }}
|
|
37
|
+
{{ CounterSection @counter-change="handleCounterChange" @counter-reset="handleCounterReset" @custom-event="handleCustomEvent" :pageTitle="title" }}
|
|
38
|
+
|
|
39
|
+
<!-- 🧪 EMIT TEST - NEW SEPARATE COMPONENT -->
|
|
40
|
+
<h3 style="margin-top: 40px; margin-bottom: 20px; color: #4da6ff;">🧪 EMIT EVENTS TEST LAB</h3>
|
|
41
|
+
{{ EmitTest @simple-click="handleSimpleClick" @click-with-data="handleClickWithData" @object-event="handleObjectEvent" }}
|
|
42
|
+
|
|
43
|
+
<!-- 🧪 Slots Test -->
|
|
44
|
+
<h3 style="margin-top: 40px; margin-bottom: 20px;">🎴 Slots Test</h3>
|
|
45
|
+
|
|
46
|
+
{{ Card title="Custom Card" }}
|
|
47
|
+
<template slot="header">
|
|
48
|
+
<p style="color: #10b981; font-weight: bold;">🎯 Custom header from parent!</p>
|
|
49
|
+
</template>
|
|
50
|
+
<p style="color: #f59e0b;">This is custom content passed through the default slot.</p>
|
|
51
|
+
<p>You can pass any HTML content here!</p>
|
|
52
|
+
<template slot="footer">
|
|
53
|
+
<button style="background: #3b82f6; color: white; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer;">
|
|
54
|
+
Custom Footer Button
|
|
55
|
+
</button>
|
|
56
|
+
</template>
|
|
57
|
+
{{ /Card }}
|
|
58
|
+
|
|
59
|
+
{{ Card title="Default Slots Card" }}
|
|
60
|
+
<p>This card uses only the default slot - no named slots provided.</p>
|
|
61
|
+
{{ /Card }}
|
|
62
|
+
|
|
63
|
+
<!-- 🧪 PROVIDE / INJECT TEST -->
|
|
64
|
+
<h3 style="margin-top: 40px; margin-bottom: 20px; color: #4CAF50;">🔗 PROVIDE / INJECT TEST</h3>
|
|
65
|
+
{{ ParentComponent }}
|
|
22
66
|
</div>
|
|
23
67
|
`
|
|
24
68
|
}
|
package/app/routes/TestFor.js
CHANGED
package/auto-imports.d.ts
CHANGED
|
@@ -10,10 +10,14 @@ declare global {
|
|
|
10
10
|
const computed: typeof import('fusee-framework').computed
|
|
11
11
|
const defineComponent: typeof import('fusee-framework').defineComponent
|
|
12
12
|
const effect: typeof import('fusee-framework').effect
|
|
13
|
+
const emit: typeof import('fusee-framework').emit
|
|
14
|
+
const inject: typeof import('fusee-framework').inject
|
|
15
|
+
const inspect: typeof import('fusee-framework').inspect
|
|
13
16
|
const mountOutlet: typeof import('fusee-framework').mountOutlet
|
|
14
17
|
const mountTemplate: typeof import('fusee-framework').mountTemplate
|
|
15
18
|
const onMount: typeof import('fusee-framework').onMount
|
|
16
19
|
const onUnmount: typeof import('fusee-framework').onUnmount
|
|
20
|
+
const provide: typeof import('fusee-framework').provide
|
|
17
21
|
const signal: typeof import('fusee-framework').signal
|
|
18
22
|
const untrack: typeof import('fusee-framework').untrack
|
|
19
23
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export const FuseePreset = {
|
|
2
2
|
'fusee-framework': [
|
|
3
3
|
'signal', 'computed', 'effect',
|
|
4
|
-
'batch', 'untrack',
|
|
4
|
+
'batch', 'untrack', 'inspect',
|
|
5
5
|
'defineComponent', 'onMount', 'onUnmount',
|
|
6
|
-
'mountOutlet', 'mountTemplate'
|
|
6
|
+
'mountOutlet', 'mountTemplate', 'emit'
|
|
7
7
|
]
|
|
8
8
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { effect, batch } from './signal.js'
|
|
2
2
|
import { processDirectives } from './directives.js'
|
|
3
|
+
import { parseSlots } from './component.js'
|
|
3
4
|
import {
|
|
4
5
|
evaluateExpression,
|
|
5
6
|
parseInterpolation,
|
|
@@ -12,24 +13,21 @@ export function mountTemplate(template, container, context, components) {
|
|
|
12
13
|
let processed = template.replace(/(<[a-zA-Z0-9-]+\s[^>]*?)\s:([a-zA-Z][a-zA-Z0-9-]*)=/g, '$1 data-bind-$2=')
|
|
13
14
|
|
|
14
15
|
for (const name of Object.keys(components)) {
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
return `<div data-component="${name}"${attrs}></div>`
|
|
16
|
+
const reWithSlot = new RegExp(
|
|
17
|
+
`\\{\\{\\s*${name}(\\s[^}]*?|\\s*)\\}\\}([\\s\\S]*?)\\{\\{\\s*\\/${name}\\s*\\}\\}`,
|
|
18
|
+
'g'
|
|
19
|
+
)
|
|
20
|
+
const reNoSlot = new RegExp(`\\{\\{\\s*${name}(\\s+.*?|\\s*)\\}\\}`, 'g')
|
|
21
|
+
|
|
22
|
+
processed = processed.replace(reWithSlot, (match, propsStr, slotContent) => {
|
|
23
|
+
const attrs = parseComponentAttrs(propsStr || '')
|
|
24
|
+
const encodedSlot = encodeURIComponent(slotContent.trim())
|
|
25
|
+
return `<div data-component="${name}"${attrs} data-slot="${encodedSlot}"></div>`
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
processed = processed.replace(reNoSlot, (match, propsStr) => {
|
|
29
|
+
const attrs = parseComponentAttrs(propsStr || '')
|
|
30
|
+
return `<div data-component="${name}"${attrs}></div>`
|
|
33
31
|
})
|
|
34
32
|
}
|
|
35
33
|
|
|
@@ -39,26 +37,47 @@ export function mountTemplate(template, container, context, components) {
|
|
|
39
37
|
return { effects }
|
|
40
38
|
}
|
|
41
39
|
|
|
40
|
+
// Parses props string from component mustache into HTML attr string
|
|
41
|
+
function parseComponentAttrs(propsStr) {
|
|
42
|
+
let attrs = ''
|
|
43
|
+
if (!propsStr || !propsStr.trim()) return attrs
|
|
44
|
+
|
|
45
|
+
const attrRegex = /(@?:?[a-zA-Z0-9-]+)="([^"]*)"/g
|
|
46
|
+
let attrMatch
|
|
47
|
+
while ((attrMatch = attrRegex.exec(propsStr)) !== null) {
|
|
48
|
+
const attrName = attrMatch[1]
|
|
49
|
+
const attrValue = attrMatch[2]
|
|
50
|
+
|
|
51
|
+
if (attrName.startsWith('@')) {
|
|
52
|
+
// Event listener: @change="handler" → data-on-change="handler"
|
|
53
|
+
attrs += ` data-on-${attrName.slice(1).toLowerCase()}="${attrValue}"`
|
|
54
|
+
} else if (attrName.startsWith(':')) {
|
|
55
|
+
// Dynamic prop
|
|
56
|
+
attrs += ` data-bind-prop-${attrName.slice(1).toLowerCase()}="${attrValue}"`
|
|
57
|
+
} else {
|
|
58
|
+
// Static prop
|
|
59
|
+
attrs += ` data-prop-${attrName.toLowerCase()}="${attrValue}"`
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return attrs
|
|
63
|
+
}
|
|
64
|
+
|
|
42
65
|
export function compileNode(node, context, components, effects) {
|
|
43
|
-
// Handle f-once (render once and discard reactive tracking for this subtree)
|
|
44
66
|
if (node.nodeType === 1 && node.hasAttribute('f-once')) {
|
|
45
67
|
node.removeAttribute('f-once')
|
|
46
68
|
const temporaryEffects = []
|
|
47
69
|
|
|
48
|
-
// Process ONLY this node and its children ONCE
|
|
49
70
|
processDirectives(node, context, components, temporaryEffects)
|
|
50
71
|
processBindingAttrs(node, context, temporaryEffects)
|
|
51
72
|
processMustaches(node, context, temporaryEffects)
|
|
52
73
|
bindComponents(node, components, context, temporaryEffects)
|
|
53
74
|
|
|
54
|
-
// Recurse children but with temporary effects
|
|
55
75
|
let child = node.firstChild
|
|
56
76
|
while (child) {
|
|
57
77
|
compileOnce(child, context, components, temporaryEffects)
|
|
58
78
|
child = child.nextSibling
|
|
59
79
|
}
|
|
60
80
|
|
|
61
|
-
// Cleanup immediately
|
|
62
81
|
for (const cleanup of temporaryEffects) if (typeof cleanup === 'function') cleanup()
|
|
63
82
|
return
|
|
64
83
|
}
|
|
@@ -172,46 +191,71 @@ function bindComponents(el, components, context, effects) {
|
|
|
172
191
|
const name = el.getAttribute('data-component')
|
|
173
192
|
if (!name) return
|
|
174
193
|
|
|
175
|
-
// Remove the attribute immediately to prevent recursive compilers from
|
|
176
|
-
// re-identifying this element as a component placeholder.
|
|
177
194
|
el.removeAttribute('data-component')
|
|
178
195
|
|
|
179
196
|
const ComponentFn = components[name]
|
|
180
|
-
if (ComponentFn)
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
197
|
+
if (!ComponentFn) return
|
|
198
|
+
|
|
199
|
+
// ── Props ────────────────────────────────────────────────────────────────
|
|
200
|
+
const props = {}
|
|
201
|
+
const listeners = {}
|
|
202
|
+
|
|
203
|
+
for (const attr of [...el.attributes]) {
|
|
204
|
+
const attrName = attr.name.toLowerCase()
|
|
205
|
+
|
|
206
|
+
if (attrName.startsWith('data-prop-')) {
|
|
207
|
+
props[attrName.slice(10)] = attr.value
|
|
208
|
+
|
|
209
|
+
} else if (attrName.startsWith('data-bind-prop-')) {
|
|
210
|
+
const propName = attrName.slice(15)
|
|
211
|
+
const expression = attr.value.trim()
|
|
212
|
+
|
|
213
|
+
if (expression in context) {
|
|
214
|
+
Object.defineProperty(props, propName, {
|
|
215
|
+
get() {
|
|
216
|
+
const val = context[expression]
|
|
217
|
+
return typeof val === 'function' && val.isSignal ? val() : val
|
|
218
|
+
},
|
|
219
|
+
enumerable: true,
|
|
220
|
+
configurable: true
|
|
221
|
+
})
|
|
222
|
+
} else {
|
|
223
|
+
Object.defineProperty(props, propName, {
|
|
224
|
+
get() { return evaluateExpression(expression, context) },
|
|
225
|
+
enumerable: true,
|
|
226
|
+
configurable: true
|
|
227
|
+
})
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
} else if (attrName.startsWith('data-on-')) {
|
|
231
|
+
// ── Emit listeners ───────────────────────────────────────────────
|
|
232
|
+
// @change="handler" → data-on-change="handler"
|
|
233
|
+
// When child emits 'change', calls context.handler(value)
|
|
234
|
+
const eventName = attrName.slice(8) // "data-on-" → eventName
|
|
235
|
+
const handlerExpr = attr.value.trim()
|
|
236
|
+
|
|
237
|
+
listeners[eventName] = (...args) => {
|
|
238
|
+
const handler = context[handlerExpr]
|
|
239
|
+
if (typeof handler === 'function') {
|
|
240
|
+
handler(...args)
|
|
200
241
|
} else {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
return evaluateExpression(expression, context)
|
|
204
|
-
},
|
|
205
|
-
enumerable: true,
|
|
206
|
-
configurable: true
|
|
207
|
-
})
|
|
242
|
+
// Support inline expressions: @change="count(count() + 1)"
|
|
243
|
+
evaluateExpression(handlerExpr, context)
|
|
208
244
|
}
|
|
209
245
|
}
|
|
210
246
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ── Slots ────────────────────────────────────────────────────────────────
|
|
250
|
+
// data-slot attribute contains URI-encoded slot HTML from parent
|
|
251
|
+
const rawSlot = el.getAttribute('data-slot')
|
|
252
|
+
const slots = rawSlot ? parseSlots(decodeURIComponent(rawSlot)) : {}
|
|
253
|
+
el.removeAttribute('data-slot')
|
|
254
|
+
|
|
255
|
+
const childComponent = ComponentFn(props, { listeners, slots, parent: context._instance })
|
|
256
|
+
childComponent.render(el)
|
|
257
|
+
|
|
258
|
+
if (effects) {
|
|
259
|
+
effects.push(() => childComponent.unmount())
|
|
216
260
|
}
|
|
217
261
|
}
|
|
@@ -68,6 +68,60 @@ function resolveProps(schema, received) {
|
|
|
68
68
|
return resolved
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
// ── Emit ──────────────────────────────────────────────────────────────────────
|
|
72
|
+
// listeners: { change: fn, submit: fn, ... }
|
|
73
|
+
// emit('change', 42) → calls listeners.change(42)
|
|
74
|
+
function createEmit(listeners) {
|
|
75
|
+
return function emit(eventName, ...args) {
|
|
76
|
+
const handler = listeners[eventName]
|
|
77
|
+
if (typeof handler === 'function') {
|
|
78
|
+
batch(() => handler(...args))
|
|
79
|
+
} else if (handler !== undefined) {
|
|
80
|
+
console.warn(`[framework] emit("${eventName}"): listener is not a function`)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ── Slots ─────────────────────────────────────────────────────────────────────
|
|
86
|
+
// Parses slot content from parent HTML string.
|
|
87
|
+
// Named slots: <template slot="header">...</template>
|
|
88
|
+
// Default slot: everything else
|
|
89
|
+
export function parseSlots(slotHTML) {
|
|
90
|
+
const slots = { default: '' }
|
|
91
|
+
if (!slotHTML || !slotHTML.trim()) return slots
|
|
92
|
+
|
|
93
|
+
const namedSlotRe = /<template\s+slot="([^"]+)"[^>]*>([\s\S]*?)<\/template>/gi
|
|
94
|
+
let remaining = slotHTML
|
|
95
|
+
let match
|
|
96
|
+
|
|
97
|
+
while ((match = namedSlotRe.exec(slotHTML)) !== null) {
|
|
98
|
+
slots[match[1]] = match[2].trim()
|
|
99
|
+
remaining = remaining.replace(match[0], '')
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const defaultContent = remaining.trim()
|
|
103
|
+
if (defaultContent) slots.default = defaultContent
|
|
104
|
+
|
|
105
|
+
return slots
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Replaces <slot> and <slot name="x"> in template with actual slot content
|
|
109
|
+
function resolveSlots(template, slots) {
|
|
110
|
+
// Handle named slots with any content between tags (including default content)
|
|
111
|
+
let result = template.replace(/<slot\s+name="([^"]+)"\s*>[\s\S]*?<\/slot>/gi, (match, name) => {
|
|
112
|
+
return slots[name] ?? ''
|
|
113
|
+
})
|
|
114
|
+
// Handle default slots with any content between tags
|
|
115
|
+
result = result.replace(/<slot\s*>[\s\S]*?<\/slot>/gi, () => {
|
|
116
|
+
return slots.default ?? ''
|
|
117
|
+
})
|
|
118
|
+
// Handle self-closing named slots
|
|
119
|
+
result = result.replace(/<slot\s+name="([^"]+)"\s*\/>/gi, (_, name) => slots[name] ?? '')
|
|
120
|
+
// Handle self-closing default slots
|
|
121
|
+
result = result.replace(/<slot\s*\/>/gi, () => slots.default ?? '')
|
|
122
|
+
return result
|
|
123
|
+
}
|
|
124
|
+
|
|
71
125
|
export function onMount(fn) {
|
|
72
126
|
if (currentInstance) currentInstance._mountHooks.push(fn)
|
|
73
127
|
}
|
|
@@ -77,24 +131,38 @@ export function onUnmount(fn) {
|
|
|
77
131
|
}
|
|
78
132
|
|
|
79
133
|
export function defineComponent(options) {
|
|
80
|
-
|
|
134
|
+
// ComponentFactory now accepts props + optional { listeners, slots, parent }
|
|
135
|
+
// listeners: event handlers from parent @eventName="handler"
|
|
136
|
+
// slots: parsed slot HTML from parent
|
|
137
|
+
// parent: parent component instance for provide/inject
|
|
138
|
+
return function ComponentFactory(props = {}, { listeners = {}, slots = {}, parent = null } = {}) {
|
|
81
139
|
const instance = {
|
|
82
140
|
props: options.props ? resolveProps(options.props, props) : props,
|
|
83
141
|
_mountHooks: [],
|
|
84
142
|
_unmountHooks: [],
|
|
85
143
|
_effects: [],
|
|
86
144
|
_element: null,
|
|
145
|
+
_provides: {},
|
|
146
|
+
_parent: parent,
|
|
87
147
|
}
|
|
88
148
|
|
|
149
|
+
const emit = createEmit(listeners)
|
|
150
|
+
|
|
89
151
|
currentInstance = instance
|
|
90
|
-
|
|
152
|
+
// setup() receives (props, { emit, slots })
|
|
153
|
+
const result = options.setup(instance.props, { emit, slots })
|
|
154
|
+
// Store instance reference on result for child component access
|
|
155
|
+
result._instance = instance
|
|
91
156
|
currentInstance = null
|
|
92
157
|
|
|
93
158
|
function render(container) {
|
|
94
159
|
instance._element = container
|
|
95
160
|
|
|
161
|
+
// Resolve slot placeholders before mounting
|
|
162
|
+
const resolvedTemplate = resolveSlots(result.template, slots)
|
|
163
|
+
|
|
96
164
|
const { effects } = mountTemplate(
|
|
97
|
-
|
|
165
|
+
resolvedTemplate,
|
|
98
166
|
container,
|
|
99
167
|
result,
|
|
100
168
|
options.components || {}
|
|
@@ -116,4 +184,18 @@ export function defineComponent(options) {
|
|
|
116
184
|
|
|
117
185
|
return { render, unmount, instance }
|
|
118
186
|
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function provide(key, value) {
|
|
190
|
+
if(!currentInstance) return
|
|
191
|
+
currentInstance._provides[key] = value
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function inject(key) {
|
|
195
|
+
let parent = currentInstance?._parent
|
|
196
|
+
while(parent) {
|
|
197
|
+
if (key in parent._provides) return parent._provides[key];
|
|
198
|
+
parent = parent._parent;
|
|
199
|
+
}
|
|
200
|
+
return null;
|
|
119
201
|
}
|
|
@@ -6,7 +6,10 @@ const DANGEROUS_SCHEMES = /^(javascript|data|vbscript|file):/i
|
|
|
6
6
|
export function evaluateExpression(expr, context, extraContext = {}, unwrapSignals = true) {
|
|
7
7
|
const keys = []
|
|
8
8
|
const values = []
|
|
9
|
-
|
|
9
|
+
|
|
10
|
+
// Remove string literals before extracting identifiers to avoid matching words inside strings
|
|
11
|
+
const exprWithoutStrings = expr.replace(/'[^']*'|"[^"]*"/g, ' ')
|
|
12
|
+
const identifiers = new Set(exprWithoutStrings.match(/[a-zA-Z_$][a-zA-Z0-9_$]*/g) || [])
|
|
10
13
|
|
|
11
14
|
// Map global context (only what is used in expression)
|
|
12
15
|
for (const k in context) {
|
package/framework/core/signal.js
CHANGED
|
@@ -84,6 +84,8 @@ export function computed(fn) {
|
|
|
84
84
|
const computedNode = () => {
|
|
85
85
|
if (!dirty) {
|
|
86
86
|
dirty = true
|
|
87
|
+
for (const dep of computedNode.deps) dep.delete(computedNode)
|
|
88
|
+
computedNode.deps.clear()
|
|
87
89
|
for (const sub of [...subscribers]) scheduleEffect(sub)
|
|
88
90
|
}
|
|
89
91
|
}
|
|
@@ -135,3 +137,23 @@ export function untrack(fn) {
|
|
|
135
137
|
currentEffect = prevEffect
|
|
136
138
|
}
|
|
137
139
|
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Reactive log helper similar to Svelte 5's $inspect.
|
|
143
|
+
* Watches signals or functions and logs their current values on change.
|
|
144
|
+
* Only active in development mode.
|
|
145
|
+
*/
|
|
146
|
+
export function inspect(...args) {
|
|
147
|
+
const isDev = typeof import.meta.env !== 'undefined' ? !!import.meta.env.DEV : true
|
|
148
|
+
if (!isDev) return
|
|
149
|
+
|
|
150
|
+
return effect(() => {
|
|
151
|
+
const values = args.map(arg => {
|
|
152
|
+
if (typeof arg === 'function') {
|
|
153
|
+
return arg()
|
|
154
|
+
}
|
|
155
|
+
return arg
|
|
156
|
+
})
|
|
157
|
+
console.log('[inspect]', ...values)
|
|
158
|
+
})
|
|
159
|
+
}
|
package/framework/index.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
// ─── Framework Public API ─────────────────────────────────────────────────────
|
|
2
|
-
export { signal, computed, effect, batch, untrack } from './core/signal.js'
|
|
3
|
-
export { defineComponent, onMount, onUnmount } from './core/component.js'
|
|
2
|
+
export { signal, computed, effect, batch, untrack, inspect } from './core/signal.js'
|
|
3
|
+
export { defineComponent, onMount, onUnmount, parseSlots, provide, inject } from './core/component.js'
|
|
4
4
|
export { createRouter, navigate, mountOutlet } from './router/router.js'
|
|
5
5
|
export { mountTemplate } from './core/compiler.js'
|
|
6
|
+
|
|
7
|
+
// Emit is a placeholder - actual emit function is created per component
|
|
8
|
+
export const emit = () => console.warn('[framework] emit() can only be used inside component setup()')
|
|
@@ -12,12 +12,29 @@ export type PropConfig = {
|
|
|
12
12
|
required?: boolean
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
// ── Emit ──────────────────────────────────────────────────────────────────────
|
|
16
|
+
// emit('change', value) — calls parent listener registered with @change="handler"
|
|
17
|
+
export type EmitFn = (eventName: string, ...args: any[]) => void
|
|
18
|
+
|
|
19
|
+
// ── Slots ─────────────────────────────────────────────────────────────────────
|
|
20
|
+
// slots.default — default slot HTML string
|
|
21
|
+
// slots.header — named slot HTML string
|
|
22
|
+
export type Slots = Record<string, string>
|
|
23
|
+
|
|
24
|
+
// ── Setup context ─────────────────────────────────────────────────────────────
|
|
25
|
+
export type SetupContext = {
|
|
26
|
+
emit: EmitFn
|
|
27
|
+
slots: Slots
|
|
28
|
+
}
|
|
29
|
+
|
|
15
30
|
export type ComponentInstance = {
|
|
16
31
|
props: ComponentProps
|
|
17
32
|
_mountHooks: (() => void)[]
|
|
18
33
|
_unmountHooks: (() => void)[]
|
|
19
34
|
_effects: import('./signal').EffectRunner[]
|
|
20
35
|
_element: HTMLElement | null
|
|
36
|
+
_provides: Record<string, any>
|
|
37
|
+
_parent: ComponentInstance | null
|
|
21
38
|
}
|
|
22
39
|
|
|
23
40
|
export type ComponentResult = {
|
|
@@ -28,10 +45,13 @@ export type ComponentResult = {
|
|
|
28
45
|
export type ComponentOptions<TProps = ComponentProps> = {
|
|
29
46
|
props?: PropSchema
|
|
30
47
|
components?: Record<string, ComponentFactory<any>>
|
|
31
|
-
setup: (props: TProps) => ComponentResult
|
|
48
|
+
setup: (props: TProps, ctx: SetupContext) => ComponentResult
|
|
32
49
|
}
|
|
33
50
|
|
|
34
|
-
export type ComponentFactory<TProps = ComponentProps> = (
|
|
51
|
+
export type ComponentFactory<TProps = ComponentProps> = (
|
|
52
|
+
props?: TProps,
|
|
53
|
+
options?: { listeners?: Record<string, Function>, slots?: Slots, parent?: ComponentInstance }
|
|
54
|
+
) => ComponentApi
|
|
35
55
|
|
|
36
56
|
export type ComponentApi = {
|
|
37
57
|
render: (container: HTMLElement) => ComponentInstance
|
|
@@ -41,4 +61,12 @@ export type ComponentApi = {
|
|
|
41
61
|
|
|
42
62
|
export declare function onMount(fn: () => void): void
|
|
43
63
|
export declare function onUnmount(fn: () => void): void
|
|
44
|
-
export declare function
|
|
64
|
+
export declare function parseSlots(slotHTML: string): Slots
|
|
65
|
+
|
|
66
|
+
// ── Provide / Inject ───────────────────────────────────────────────────────────
|
|
67
|
+
export declare function provide<T>(key: string, value: T): void
|
|
68
|
+
export declare function inject<T>(key: string): T | null
|
|
69
|
+
|
|
70
|
+
export declare function defineComponent<TProps = EmptyProps>(
|
|
71
|
+
options: ComponentOptions<TProps>
|
|
72
|
+
): ComponentFactory<TProps>
|
|
@@ -25,6 +25,7 @@ export declare function effect(fn: () => void): () => void
|
|
|
25
25
|
export declare function computed<T>(fn: () => T): Computed<T>
|
|
26
26
|
export declare function batch<T>(fn: () => T): T
|
|
27
27
|
export declare function untrack<T>(fn: () => T): T
|
|
28
|
+
export declare function inspect(...args: any[]): (() => void) | void
|
|
28
29
|
|
|
29
30
|
declare function setEffectHook(fn: (eff: any) => void): void
|
|
30
31
|
|
package/index.html
CHANGED
|
@@ -31,7 +31,6 @@
|
|
|
31
31
|
|
|
32
32
|
nav a {
|
|
33
33
|
color: #ff3333;
|
|
34
|
-
/* Link-urile sunt acum roșii */
|
|
35
34
|
text-decoration: none;
|
|
36
35
|
font-weight: 500;
|
|
37
36
|
}
|
|
@@ -172,9 +171,6 @@
|
|
|
172
171
|
</nav>
|
|
173
172
|
<div id="app"></div>
|
|
174
173
|
<script type="module" src="./app/main.js"></script>
|
|
175
|
-
<!-- For TypeScript: compile with `npx tsc` then change to: -->
|
|
176
|
-
<!-- <script type="module" src="./dist/app/main.js"></script> -->
|
|
177
|
-
|
|
178
174
|
</body>
|
|
179
175
|
|
|
180
176
|
</html>
|