fusee-framework 1.2.1 → 1.4.0
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 +19 -9
- package/app/components/DynamicA.js +24 -0
- package/app/components/DynamicA.ts +24 -0
- package/app/components/DynamicB.js +24 -0
- package/app/components/DynamicB.ts +24 -0
- package/app/components/EmitTest.js +5 -6
- package/app/components/Loading.js +26 -0
- package/app/components/Loading.ts +26 -0
- package/app/composables/demo.js +29 -0
- package/app/composables/demo.ts +30 -0
- package/app/composables/useFetch.js +53 -0
- package/app/composables/useFetch.ts +63 -0
- package/app/composables/useLocalStorage.js +31 -0
- package/app/composables/useLocalStorage.ts +26 -0
- package/app/main.js +58 -9
- package/app/main.ts +53 -7
- package/app/routes/TestComposables.js +158 -0
- package/app/routes/TestComposables.ts +158 -0
- package/app/routes/TestDynamic.js +40 -0
- package/app/routes/TestDynamic.ts +40 -0
- package/app/routes/TestFor.js +2 -3
- package/app/routes/TestFor.ts +2 -3
- package/auto-imports.d.ts +1 -0
- package/framework/auto-import-preset.js +1 -0
- package/framework/core/component.js +76 -21
- package/framework/core/composable.js +27 -0
- package/framework/core/directives.js +234 -6
- package/framework/core/signal.js +58 -0
- package/framework/index.js +4 -2
- package/framework/types/component.d.ts +11 -0
- package/framework/types/composable.d.ts +14 -0
- package/framework/types/directives.d.ts +22 -0
- package/framework/types/index.d.ts +5 -4
- package/framework/types/signal.d.ts +15 -2
- package/index.html +137 -2
- package/package.json +1 -1
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { counterLogic, mouseTracker } from '../composables/demo.js'
|
|
2
|
+
import { useFetch } from '../composables/useFetch.js'
|
|
3
|
+
import { useLocalStorage } from '../composables/useLocalStorage.js'
|
|
4
|
+
|
|
5
|
+
export const TestComposables = defineComponent({
|
|
6
|
+
setup() {
|
|
7
|
+
// ── 1. counterLogic — pure signal composable, no context needed ──────
|
|
8
|
+
const counter = counterLogic(0, 1)
|
|
9
|
+
const bigCounter = counterLogic(100, 10)
|
|
10
|
+
|
|
11
|
+
// ── 2. mouseTracker — lifecycle composable (needs setup context) ─────
|
|
12
|
+
const mouse = mouseTracker()
|
|
13
|
+
|
|
14
|
+
// ── 3. useLocalStorage — persists across page reloads ────────────────
|
|
15
|
+
const theme = useLocalStorage('demo-theme', 'dark')
|
|
16
|
+
const nickname = useLocalStorage('demo-nickname', '')
|
|
17
|
+
|
|
18
|
+
const toggleTheme = () => theme(theme() === 'dark' ? 'light' : 'dark')
|
|
19
|
+
|
|
20
|
+
// ── 4. useFetch — reactive data fetching ─────────────────────────────
|
|
21
|
+
const { data: posts, loading, error } = useFetch(
|
|
22
|
+
'https://jsonplaceholder.typicode.com/posts?_limit=3'
|
|
23
|
+
)
|
|
24
|
+
// ── 5. Custom Directives Test ────────────────────────────────────────
|
|
25
|
+
const hColor = signal('#8b8bff33')
|
|
26
|
+
const colors = ['#8b8bff33', '#10b98133', '#f59e0b33', '#ef444433']
|
|
27
|
+
let colorIdx = 0
|
|
28
|
+
const nextColor = () => hColor(colors[++colorIdx % colors.length])
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
hColor,
|
|
32
|
+
nextColor,
|
|
33
|
+
|
|
34
|
+
// counter
|
|
35
|
+
count: counter.count,
|
|
36
|
+
doubled: counter.doubled,
|
|
37
|
+
isEven: counter.isEven,
|
|
38
|
+
increment: counter.increment,
|
|
39
|
+
decrement: counter.decrement,
|
|
40
|
+
reset: counter.reset,
|
|
41
|
+
|
|
42
|
+
// big counter
|
|
43
|
+
bigCount: bigCounter.count,
|
|
44
|
+
bigIncrement: bigCounter.increment,
|
|
45
|
+
bigDecrement: bigCounter.decrement,
|
|
46
|
+
|
|
47
|
+
// mouse
|
|
48
|
+
mouseX: mouse.x,
|
|
49
|
+
mouseY: mouse.y,
|
|
50
|
+
|
|
51
|
+
// localStorage
|
|
52
|
+
theme,
|
|
53
|
+
nickname,
|
|
54
|
+
toggleTheme,
|
|
55
|
+
|
|
56
|
+
// fetch
|
|
57
|
+
posts,
|
|
58
|
+
loading,
|
|
59
|
+
error,
|
|
60
|
+
|
|
61
|
+
template: `
|
|
62
|
+
<div class="page">
|
|
63
|
+
<h1>🧩 Composables Demo</h1>
|
|
64
|
+
<p style="color:#888; margin-bottom: 2rem;">
|
|
65
|
+
Each section below is powered by a <code style="color:#ff3333">defineComposable()</code> function.
|
|
66
|
+
No naming rules — just reactive logic.
|
|
67
|
+
</p>
|
|
68
|
+
|
|
69
|
+
<!-- ── 1. counterLogic ─────────────────────────────────────── -->
|
|
70
|
+
<section class="demo-card">
|
|
71
|
+
<h2>📦 counterLogic(0, step=1)</h2>
|
|
72
|
+
<p class="demo-desc">Pure signal composable — works without setup() context.</p>
|
|
73
|
+
<div class="counter-display">
|
|
74
|
+
<span class="big-num" :class="isEven ? 'even' : 'odd'">{{ count }}</span>
|
|
75
|
+
<span class="derived-label">× 2 = {{ doubled }}</span>
|
|
76
|
+
</div>
|
|
77
|
+
<div class="btn-row">
|
|
78
|
+
<button @click="decrement" class="cmp-btn minus">−</button>
|
|
79
|
+
<button @click="reset" class="cmp-btn reset">↺</button>
|
|
80
|
+
<button @click="increment" class="cmp-btn plus">+</button>
|
|
81
|
+
</div>
|
|
82
|
+
<p class="badge" :class="isEven ? 'badge-blue' : 'badge-red'">{{ isEven ? 'Even' : 'Odd' }}</p>
|
|
83
|
+
</section>
|
|
84
|
+
|
|
85
|
+
<!-- ── 1b. Second independent counter (step=10) ───────────── -->
|
|
86
|
+
<section class="demo-card">
|
|
87
|
+
<h2>📦 counterLogic(100, step=10)</h2>
|
|
88
|
+
<p class="demo-desc">Same composable, different instance. Fully isolated state.</p>
|
|
89
|
+
<div class="counter-display">
|
|
90
|
+
<span class="big-num">{{ bigCount }}</span>
|
|
91
|
+
</div>
|
|
92
|
+
<div class="btn-row">
|
|
93
|
+
<button @click="bigDecrement" class="cmp-btn minus">−10</button>
|
|
94
|
+
<button @click="bigIncrement" class="cmp-btn plus">+10</button>
|
|
95
|
+
</div>
|
|
96
|
+
</section>
|
|
97
|
+
|
|
98
|
+
<!-- ── 2. mouseTracker ────────────────────────────────────── -->
|
|
99
|
+
<section class="demo-card">
|
|
100
|
+
<h2>🖱️ mouseTracker()</h2>
|
|
101
|
+
<p class="demo-desc">Lifecycle composable — registers window listener on mount, cleans up on unmount.</p>
|
|
102
|
+
<div class="mouse-display">
|
|
103
|
+
<span>X: <strong>{{ mouseX }}</strong></span>
|
|
104
|
+
<span>Y: <strong>{{ mouseY }}</strong></span>
|
|
105
|
+
</div>
|
|
106
|
+
</section>
|
|
107
|
+
|
|
108
|
+
<!-- ── 3. useLocalStorage ─────────────────────────────────── -->
|
|
109
|
+
<section class="demo-card">
|
|
110
|
+
<h2>💾 useLocalStorage()</h2>
|
|
111
|
+
<p class="demo-desc">State persists across page reloads. Try changing the nickname, then refresh.</p>
|
|
112
|
+
<div style="margin-bottom: 12px;">
|
|
113
|
+
<input f-model="nickname"
|
|
114
|
+
class="cmp-input"
|
|
115
|
+
placeholder="Your nickname…" />
|
|
116
|
+
<p style="margin-top: 8px; color: #888; font-size: 0.85rem;">
|
|
117
|
+
Stored: <strong style="color:#ff3333">{{ nickname || '(empty)' }}</strong>
|
|
118
|
+
</p>
|
|
119
|
+
</div>
|
|
120
|
+
<button @click="toggleTheme" class="cmp-btn reset">
|
|
121
|
+
Theme: {{ theme }}
|
|
122
|
+
</button>
|
|
123
|
+
</section>
|
|
124
|
+
|
|
125
|
+
<!-- ── 4. useFetch ────────────────────────────────────────── -->
|
|
126
|
+
<section class="demo-card">
|
|
127
|
+
<h2>🌐 useFetch()</h2>
|
|
128
|
+
<p class="demo-desc">Fetches from JSONPlaceholder. Loading state handled reactively.</p>
|
|
129
|
+
<div f-if="loading">
|
|
130
|
+
<p style="color: #888;">⏳ Loading posts…</p>
|
|
131
|
+
</div>
|
|
132
|
+
<div f-elif="error">
|
|
133
|
+
<p style="color: #ff6b6b;">❌ {{ error }}</p>
|
|
134
|
+
</div>
|
|
135
|
+
<div f-else>
|
|
136
|
+
<div f-for="post in posts" style="margin-bottom: 10px; padding: 10px; background:#1a1a24; border-radius:6px; border-left: 3px solid #ff3333;">
|
|
137
|
+
<strong style="color:#ff9999">{{ post.title }}</strong>
|
|
138
|
+
<p style="margin-top: 4px; font-size: 0.8rem; color: #666;">{{ post.body }}</p>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
</section>
|
|
142
|
+
|
|
143
|
+
<!-- ── 6. Custom Directives ────────────────────────────────────── -->
|
|
144
|
+
<section class="demo-card">
|
|
145
|
+
<h2>🛠️ Custom Directives</h2>
|
|
146
|
+
<p class="demo-desc">Testing <code>f-focus</code> and <code>f-highlight</code>.</p>
|
|
147
|
+
|
|
148
|
+
<div f-highlight="hColor" style="padding: 15px; border-radius: 8px; margin-bottom: 10px;">
|
|
149
|
+
<input f-focus class="cmp-input" placeholder="I have f-focus!" />
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<button @click="nextColor" class="cmp-btn plus">Change Highlight Color</button>
|
|
153
|
+
</section>
|
|
154
|
+
</div>
|
|
155
|
+
`
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
})
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { defineComponent } from '../../framework/index.js'
|
|
2
|
+
import { counterLogic, mouseTracker } from '../composables/demo.js'
|
|
3
|
+
import { useFetch } from '../composables/useFetch.js'
|
|
4
|
+
import { useLocalStorage } from '../composables/useLocalStorage.js'
|
|
5
|
+
|
|
6
|
+
export const TestComposables = defineComponent({
|
|
7
|
+
setup() {
|
|
8
|
+
// ── 1. counterLogic — pure signal composable, no context needed ──────
|
|
9
|
+
const counter = counterLogic(0, 1)
|
|
10
|
+
const bigCounter = counterLogic(100, 10)
|
|
11
|
+
|
|
12
|
+
// ── 2. mouseTracker — lifecycle composable (needs setup context) ─────
|
|
13
|
+
const mouse = mouseTracker()
|
|
14
|
+
|
|
15
|
+
// ── 3. useLocalStorage — persists across page reloads ────────────────
|
|
16
|
+
const theme = useLocalStorage<string>('demo-theme', 'dark')
|
|
17
|
+
const nickname = useLocalStorage<string>('demo-nickname', '')
|
|
18
|
+
|
|
19
|
+
const toggleTheme = () => theme(theme() === 'dark' ? 'light' : 'dark')
|
|
20
|
+
|
|
21
|
+
// ── 4. useFetch — reactive data fetching ─────────────────────────────
|
|
22
|
+
const { data: posts, loading, error } = useFetch(
|
|
23
|
+
'https://jsonplaceholder.typicode.com/posts?_limit=3'
|
|
24
|
+
)
|
|
25
|
+
// ── 5. Custom Directives Test ────────────────────────────────────────
|
|
26
|
+
const hColor = signal('#8b8bff33')
|
|
27
|
+
const colors = ['#8b8bff33', '#10b98133', '#f59e0b33', '#ef444433']
|
|
28
|
+
let colorIdx = 0
|
|
29
|
+
const nextColor = () => hColor(colors[++colorIdx % colors.length])
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
hColor,
|
|
33
|
+
nextColor,
|
|
34
|
+
|
|
35
|
+
// counter
|
|
36
|
+
count: counter.count,
|
|
37
|
+
doubled: counter.doubled,
|
|
38
|
+
isEven: counter.isEven,
|
|
39
|
+
increment: counter.increment,
|
|
40
|
+
decrement: counter.decrement,
|
|
41
|
+
reset: counter.reset,
|
|
42
|
+
|
|
43
|
+
// big counter
|
|
44
|
+
bigCount: bigCounter.count,
|
|
45
|
+
bigIncrement: bigCounter.increment,
|
|
46
|
+
bigDecrement: bigCounter.decrement,
|
|
47
|
+
|
|
48
|
+
// mouse
|
|
49
|
+
mouseX: mouse.x,
|
|
50
|
+
mouseY: mouse.y,
|
|
51
|
+
|
|
52
|
+
// localStorage
|
|
53
|
+
theme,
|
|
54
|
+
nickname,
|
|
55
|
+
toggleTheme,
|
|
56
|
+
|
|
57
|
+
// fetch
|
|
58
|
+
posts,
|
|
59
|
+
loading,
|
|
60
|
+
error,
|
|
61
|
+
|
|
62
|
+
template: `
|
|
63
|
+
<div class="page">
|
|
64
|
+
<h1>🧩 Composables Demo</h1>
|
|
65
|
+
<p style="color:#888; margin-bottom: 2rem;">
|
|
66
|
+
Each section below is powered by a <code style="color:#ff3333">defineComposable()</code> function.
|
|
67
|
+
No naming rules — just reactive logic.
|
|
68
|
+
</p>
|
|
69
|
+
|
|
70
|
+
<!-- ── 1. counterLogic ─────────────────────────────────────── -->
|
|
71
|
+
<section class="demo-card">
|
|
72
|
+
<h2>📦 counterLogic(0, step=1)</h2>
|
|
73
|
+
<p class="demo-desc">Pure signal composable — works without setup() context.</p>
|
|
74
|
+
<div class="counter-display">
|
|
75
|
+
<span class="big-num" :class="isEven ? 'even' : 'odd'">{{ count }}</span>
|
|
76
|
+
<span class="derived-label">× 2 = {{ doubled }}</span>
|
|
77
|
+
</div>
|
|
78
|
+
<div class="btn-row">
|
|
79
|
+
<button @click="decrement" class="cmp-btn minus">−</button>
|
|
80
|
+
<button @click="reset" class="cmp-btn reset">↺</button>
|
|
81
|
+
<button @click="increment" class="cmp-btn plus">+</button>
|
|
82
|
+
</div>
|
|
83
|
+
<p class="badge" :class="isEven ? 'badge-blue' : 'badge-red'">{{ isEven ? 'Even' : 'Odd' }}</p>
|
|
84
|
+
</section>
|
|
85
|
+
|
|
86
|
+
<!-- ── 1b. Second independent counter (step=10) ───────────── -->
|
|
87
|
+
<section class="demo-card">
|
|
88
|
+
<h2>📦 counterLogic(100, step=10)</h2>
|
|
89
|
+
<p class="demo-desc">Same composable, different instance. Fully isolated state.</p>
|
|
90
|
+
<div class="counter-display">
|
|
91
|
+
<span class="big-num">{{ bigCount }}</span>
|
|
92
|
+
</div>
|
|
93
|
+
<div class="btn-row">
|
|
94
|
+
<button @click="bigDecrement" class="cmp-btn minus">−10</button>
|
|
95
|
+
<button @click="bigIncrement" class="cmp-btn plus">+10</button>
|
|
96
|
+
</div>
|
|
97
|
+
</section>
|
|
98
|
+
|
|
99
|
+
<!-- ── 2. mouseTracker ────────────────────────────────────── -->
|
|
100
|
+
<section class="demo-card">
|
|
101
|
+
<h2>🖱️ mouseTracker()</h2>
|
|
102
|
+
<p class="demo-desc">Lifecycle composable — registers window listener on mount, cleans up on unmount.</p>
|
|
103
|
+
<div class="mouse-display">
|
|
104
|
+
<span>X: <strong>{{ mouseX }}</strong></span>
|
|
105
|
+
<span>Y: <strong>{{ mouseY }}</strong></span>
|
|
106
|
+
</div>
|
|
107
|
+
</section>
|
|
108
|
+
|
|
109
|
+
<!-- ── 3. useLocalStorage ─────────────────────────────────── -->
|
|
110
|
+
<section class="demo-card">
|
|
111
|
+
<h2>💾 useLocalStorage()</h2>
|
|
112
|
+
<p class="demo-desc">State persists across page reloads. Try changing the nickname, then refresh.</p>
|
|
113
|
+
<div style="margin-bottom: 12px;">
|
|
114
|
+
<input f-model="nickname"
|
|
115
|
+
class="cmp-input"
|
|
116
|
+
placeholder="Your nickname…" />
|
|
117
|
+
<p style="margin-top: 8px; color: #888; font-size: 0.85rem;">
|
|
118
|
+
Stored: <strong style="color:#ff3333">{{ nickname || '(empty)' }}</strong>
|
|
119
|
+
</p>
|
|
120
|
+
</div>
|
|
121
|
+
<button @click="toggleTheme" class="cmp-btn reset">
|
|
122
|
+
Theme: {{ theme }}
|
|
123
|
+
</button>
|
|
124
|
+
</section>
|
|
125
|
+
|
|
126
|
+
<!-- ── 4. useFetch ────────────────────────────────────────── -->
|
|
127
|
+
<section class="demo-card">
|
|
128
|
+
<h2>🌐 useFetch()</h2>
|
|
129
|
+
<p class="demo-desc">Fetches from JSONPlaceholder. Loading state handled reactively.</p>
|
|
130
|
+
<div f-if="loading">
|
|
131
|
+
<p style="color: #888;">⏳ Loading posts…</p>
|
|
132
|
+
</div>
|
|
133
|
+
<div f-elif="error">
|
|
134
|
+
<p style="color: #ff6b6b;">❌ {{ error }}</p>
|
|
135
|
+
</div>
|
|
136
|
+
<div f-else>
|
|
137
|
+
<div f-for="post in posts" style="margin-bottom: 10px; padding: 10px; background:#1a1a24; border-radius:6px; border-left: 3px solid #ff3333;">
|
|
138
|
+
<strong style="color:#ff9999">{{ post.title }}</strong>
|
|
139
|
+
<p style="margin-top: 4px; font-size: 0.8rem; color: #666;">{{ post.body }}</p>
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
</section>
|
|
143
|
+
<!-- ── 6. Custom Directives ────────────────────────────────────── -->
|
|
144
|
+
<section class="demo-card">
|
|
145
|
+
<h2>🛠️ Custom Directives</h2>
|
|
146
|
+
<p class="demo-desc">Testing <code>f-focus</code> and <code>f-highlight</code>.</p>
|
|
147
|
+
|
|
148
|
+
<div f-highlight="hColor" style="padding: 15px; border-radius: 8px; margin-bottom: 10px;">
|
|
149
|
+
<input f-focus class="cmp-input" placeholder="I have f-focus!" />
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<button @click="nextColor" class="cmp-btn plus">Change Highlight Color</button>
|
|
153
|
+
</section>
|
|
154
|
+
</div>
|
|
155
|
+
`
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
})
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { DynamicA } from '../components/DynamicA.js'
|
|
2
|
+
import { DynamicB } from '../components/DynamicB.js'
|
|
3
|
+
|
|
4
|
+
export const TestDynamic = defineComponent({
|
|
5
|
+
components: { DynamicA, DynamicB },
|
|
6
|
+
setup() {
|
|
7
|
+
const currentType = signal('DynamicA')
|
|
8
|
+
const sharedTitle = signal('Test Dinamic active')
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
currentType,
|
|
12
|
+
sharedTitle,
|
|
13
|
+
toggle: () => {
|
|
14
|
+
currentType(currentType() === 'DynamicA' ? 'DynamicB' : 'DynamicA')
|
|
15
|
+
},
|
|
16
|
+
template: `
|
|
17
|
+
<div class="page">
|
|
18
|
+
<h1>Dynamic Components System (Vue-style)</h1>
|
|
19
|
+
<p>Using the custom <code>f-is</code> directive.</p>
|
|
20
|
+
|
|
21
|
+
<div style="margin: 20px 0; display: flex; gap: 10px; align-items: center;">
|
|
22
|
+
<button @click="toggle" class="primary-btn">Toggle Component</button>
|
|
23
|
+
<span>Active: <strong>{{ currentType }}</strong></span>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<input f-model="sharedTitle" style="margin-bottom: 20px; padding: 8px; border-radius: 4px; border: 1px solid #444; background: #222; color: white; width: 100%;">
|
|
27
|
+
|
|
28
|
+
<hr style="border: 0; border-top: 1px solid #444; margin: 30px 0;">
|
|
29
|
+
|
|
30
|
+
<div f-is="currentType" f-keepAlive :title="sharedTitle"></div>
|
|
31
|
+
|
|
32
|
+
<p style="margin-top: 40px; color: #888; font-size: 0.9em;">
|
|
33
|
+
Note: The component above changes reactively when you click the button.
|
|
34
|
+
The <code>:title</code> prop is automatically passed to the active component.
|
|
35
|
+
</p>
|
|
36
|
+
</div>
|
|
37
|
+
`
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
})
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { DynamicA } from '../components/DynamicA.js'
|
|
2
|
+
import { DynamicB } from '../components/DynamicB.js'
|
|
3
|
+
|
|
4
|
+
export const TestDynamic = defineComponent({
|
|
5
|
+
components: { DynamicA, DynamicB },
|
|
6
|
+
setup() {
|
|
7
|
+
const currentType = signal('DynamicA')
|
|
8
|
+
const sharedTitle = signal('Dynamic Test Active')
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
currentType,
|
|
12
|
+
sharedTitle,
|
|
13
|
+
toggle: () => {
|
|
14
|
+
currentType(currentType() === 'DynamicA' ? 'DynamicB' : 'DynamicA')
|
|
15
|
+
},
|
|
16
|
+
template: `
|
|
17
|
+
<div class="page">
|
|
18
|
+
<h1>Dynamic Components System (Vue-style)</h1>
|
|
19
|
+
<p>Using the custom <code>f-is</code> directive.</p>
|
|
20
|
+
|
|
21
|
+
<div style="margin: 20px 0; display: flex; gap: 10px; align-items: center;">
|
|
22
|
+
<button @click="toggle" class="primary-btn">Toggle Component</button>
|
|
23
|
+
<span>Active: <strong>{{ currentType }}</strong></span>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<input f-model="sharedTitle" style="margin-bottom: 20px; padding: 8px; border-radius: 4px; border: 1px solid #444; background: #222; color: white; width: 100%;">
|
|
27
|
+
|
|
28
|
+
<hr style="border: 0; border-top: 1px solid #444; margin: 30px 0;">
|
|
29
|
+
|
|
30
|
+
<div f-is="currentType" :title="sharedTitle"></div>
|
|
31
|
+
|
|
32
|
+
<p style="margin-top: 40px; color: #888; font-size: 0.9em;">
|
|
33
|
+
Note: The component above changes reactively when you click the button.
|
|
34
|
+
The <code>:title</code> prop is automatically passed to the active component.
|
|
35
|
+
</p>
|
|
36
|
+
</div>
|
|
37
|
+
`
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
})
|
package/app/routes/TestFor.js
CHANGED
|
@@ -11,11 +11,10 @@ export const TestFor = defineComponent({
|
|
|
11
11
|
items,
|
|
12
12
|
addItem: () => {
|
|
13
13
|
const nextId = Math.max(0, ...items().map(i => i.id)) + 1
|
|
14
|
-
items(
|
|
14
|
+
items.push({ id: nextId, name: `Item ${nextId}` })
|
|
15
15
|
},
|
|
16
16
|
shuffleItems: () => {
|
|
17
|
-
|
|
18
|
-
items(shuffled)
|
|
17
|
+
items.sort(() => Math.random() - 0.5)
|
|
19
18
|
},
|
|
20
19
|
template: `
|
|
21
20
|
<div class="page">
|
package/app/routes/TestFor.ts
CHANGED
|
@@ -23,11 +23,10 @@ export const TestFor = defineComponent({
|
|
|
23
23
|
addItem: () => {
|
|
24
24
|
const nextIds = items().map(i => i.id)
|
|
25
25
|
const nextId = nextIds.length > 0 ? Math.max(...nextIds) + 1 : 1
|
|
26
|
-
items(
|
|
26
|
+
items.push({ id: nextId, name: `Item ${nextId}` })
|
|
27
27
|
},
|
|
28
28
|
shuffleItems: () => {
|
|
29
|
-
|
|
30
|
-
items(shuffled)
|
|
29
|
+
items.sort(() => Math.random() - 0.5)
|
|
31
30
|
},
|
|
32
31
|
template: `
|
|
33
32
|
<div class="page">
|
package/auto-imports.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ declare global {
|
|
|
9
9
|
const batch: typeof import('fusee-framework').batch
|
|
10
10
|
const computed: typeof import('fusee-framework').computed
|
|
11
11
|
const defineComponent: typeof import('fusee-framework').defineComponent
|
|
12
|
+
const directive: typeof import('fusee-framework').directive
|
|
12
13
|
const effect: typeof import('fusee-framework').effect
|
|
13
14
|
const emit: typeof import('fusee-framework').emit
|
|
14
15
|
const inject: typeof import('fusee-framework').inject
|
|
@@ -7,6 +7,10 @@ setEffectHook(eff => {
|
|
|
7
7
|
if (currentInstance) currentInstance._effects.push(eff)
|
|
8
8
|
})
|
|
9
9
|
|
|
10
|
+
export function getCurrentInstance() {
|
|
11
|
+
return currentInstance
|
|
12
|
+
}
|
|
13
|
+
|
|
10
14
|
function resolveProps(schema, received) {
|
|
11
15
|
const isArray = Array.isArray(schema)
|
|
12
16
|
const resolved = {}
|
|
@@ -68,9 +72,6 @@ function resolveProps(schema, received) {
|
|
|
68
72
|
return resolved
|
|
69
73
|
}
|
|
70
74
|
|
|
71
|
-
// ── Emit ──────────────────────────────────────────────────────────────────────
|
|
72
|
-
// listeners: { change: fn, submit: fn, ... }
|
|
73
|
-
// emit('change', 42) → calls listeners.change(42)
|
|
74
75
|
function createEmit(listeners) {
|
|
75
76
|
return function emit(eventName, ...args) {
|
|
76
77
|
const handler = listeners[eventName]
|
|
@@ -82,10 +83,6 @@ function createEmit(listeners) {
|
|
|
82
83
|
}
|
|
83
84
|
}
|
|
84
85
|
|
|
85
|
-
// ── Slots ─────────────────────────────────────────────────────────────────────
|
|
86
|
-
// Parses slot content from parent HTML string.
|
|
87
|
-
// Named slots: <template slot="header">...</template>
|
|
88
|
-
// Default slot: everything else
|
|
89
86
|
export function parseSlots(slotHTML) {
|
|
90
87
|
const slots = { default: '' }
|
|
91
88
|
if (!slotHTML || !slotHTML.trim()) return slots
|
|
@@ -105,19 +102,15 @@ export function parseSlots(slotHTML) {
|
|
|
105
102
|
return slots
|
|
106
103
|
}
|
|
107
104
|
|
|
108
|
-
// Replaces <slot> and <slot name="x"> in template with actual slot content
|
|
109
105
|
function resolveSlots(template, slots) {
|
|
110
|
-
// Handle named slots with any content between tags (including default content)
|
|
111
106
|
let result = template.replace(/<slot\s+name="([^"]+)"\s*>[\s\S]*?<\/slot>/gi, (match, name) => {
|
|
112
107
|
return slots[name] ?? ''
|
|
113
108
|
})
|
|
114
|
-
// Handle default slots with any content between tags
|
|
115
109
|
result = result.replace(/<slot\s*>[\s\S]*?<\/slot>/gi, () => {
|
|
116
110
|
return slots.default ?? ''
|
|
117
111
|
})
|
|
118
|
-
|
|
112
|
+
|
|
119
113
|
result = result.replace(/<slot\s+name="([^"]+)"\s*\/>/gi, (_, name) => slots[name] ?? '')
|
|
120
|
-
// Handle self-closing default slots
|
|
121
114
|
result = result.replace(/<slot\s*\/>/gi, () => slots.default ?? '')
|
|
122
115
|
return result
|
|
123
116
|
}
|
|
@@ -131,10 +124,6 @@ export function onUnmount(fn) {
|
|
|
131
124
|
}
|
|
132
125
|
|
|
133
126
|
export function defineComponent(options) {
|
|
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
127
|
return function ComponentFactory(props = {}, { listeners = {}, slots = {}, parent = null } = {}) {
|
|
139
128
|
const instance = {
|
|
140
129
|
props: options.props ? resolveProps(options.props, props) : props,
|
|
@@ -149,16 +138,13 @@ export function defineComponent(options) {
|
|
|
149
138
|
const emit = createEmit(listeners)
|
|
150
139
|
|
|
151
140
|
currentInstance = instance
|
|
152
|
-
// setup() receives (props, { emit, slots })
|
|
153
141
|
const result = options.setup(instance.props, { emit, slots })
|
|
154
|
-
// Store instance reference on result for child component access
|
|
155
142
|
result._instance = instance
|
|
156
143
|
currentInstance = null
|
|
157
144
|
|
|
158
145
|
function render(container) {
|
|
159
146
|
instance._element = container
|
|
160
147
|
|
|
161
|
-
// Resolve slot placeholders before mounting
|
|
162
148
|
const resolvedTemplate = resolveSlots(result.template, slots)
|
|
163
149
|
|
|
164
150
|
const { effects } = mountTemplate(
|
|
@@ -187,15 +173,84 @@ export function defineComponent(options) {
|
|
|
187
173
|
}
|
|
188
174
|
|
|
189
175
|
export function provide(key, value) {
|
|
190
|
-
if(!currentInstance) return
|
|
176
|
+
if (!currentInstance) return
|
|
191
177
|
currentInstance._provides[key] = value
|
|
192
178
|
}
|
|
193
179
|
|
|
194
180
|
export function inject(key) {
|
|
195
181
|
let parent = currentInstance?._parent
|
|
196
|
-
while(parent) {
|
|
182
|
+
while (parent) {
|
|
197
183
|
if (key in parent._provides) return parent._provides[key];
|
|
198
184
|
parent = parent._parent;
|
|
199
185
|
}
|
|
200
186
|
return null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function defineAsyncComponent(loaderOrOptions) {
|
|
190
|
+
const options = typeof loaderOrOptions === 'function'
|
|
191
|
+
? { loader: loaderOrOptions }
|
|
192
|
+
: loaderOrOptions
|
|
193
|
+
|
|
194
|
+
return function AsyncComponentFactory(props = {}, { listeners = {}, slots = {}, parent = null } = {}) {
|
|
195
|
+
const instance = {
|
|
196
|
+
props,
|
|
197
|
+
_mountHooks: [],
|
|
198
|
+
_unmountHooks: [],
|
|
199
|
+
_effects: [],
|
|
200
|
+
_element: null,
|
|
201
|
+
_provides: {},
|
|
202
|
+
_parent: parent,
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
let childApi = null
|
|
206
|
+
let loadingApi = null
|
|
207
|
+
let isUnmounted = false
|
|
208
|
+
|
|
209
|
+
function render(container) {
|
|
210
|
+
instance._element = container
|
|
211
|
+
|
|
212
|
+
if (options.loadingComponent) {
|
|
213
|
+
loadingApi = options.loadingComponent({}, { parent })
|
|
214
|
+
loadingApi.render(container)
|
|
215
|
+
} else {
|
|
216
|
+
container.innerHTML = '<!-- async component boundary -->'
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
Promise.resolve(options.loader())
|
|
220
|
+
.then(comp => {
|
|
221
|
+
if (isUnmounted) return
|
|
222
|
+
let ComponentFn = comp.default || comp
|
|
223
|
+
|
|
224
|
+
if (typeof ComponentFn === 'object' && ComponentFn !== null) {
|
|
225
|
+
ComponentFn = Object.values(ComponentFn).find(v => typeof v === 'function') || ComponentFn
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (loadingApi) {
|
|
229
|
+
loadingApi.unmount()
|
|
230
|
+
loadingApi = null
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
childApi = ComponentFn(props, { listeners, slots, parent })
|
|
234
|
+
|
|
235
|
+
container.innerHTML = ''
|
|
236
|
+
childApi.render(container)
|
|
237
|
+
})
|
|
238
|
+
.catch(err => {
|
|
239
|
+
console.error('[framework] Failed to load async component:', err)
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
return instance
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function unmount() {
|
|
246
|
+
isUnmounted = true
|
|
247
|
+
if (loadingApi) loadingApi.unmount()
|
|
248
|
+
if (childApi) childApi.unmount()
|
|
249
|
+
if (instance._element && !childApi && !loadingApi) {
|
|
250
|
+
instance._element.innerHTML = ''
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return { render, unmount, instance }
|
|
255
|
+
}
|
|
201
256
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { getCurrentInstance } from './component.js'
|
|
2
|
+
|
|
3
|
+
export function defineComposable(fn) {
|
|
4
|
+
const composable = function (...args) {
|
|
5
|
+
return fn(...args)
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
composable._isComposable = true
|
|
9
|
+
|
|
10
|
+
Object.defineProperty(composable, 'name', {
|
|
11
|
+
value: fn.name || 'composable',
|
|
12
|
+
configurable: true
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
return composable
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function assertSetupContext(name) {
|
|
19
|
+
const instance = getCurrentInstance()
|
|
20
|
+
if (!instance) {
|
|
21
|
+
console.warn(
|
|
22
|
+
`[framework] ${name ? name + ' ' : ''}composable was called outside of setup(). ` +
|
|
23
|
+
`Lifecycle hooks (onMount, onUnmount) and injections will not work.`
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
return instance
|
|
27
|
+
}
|