fusee-framework 1.2.1 → 1.3.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/main.js +35 -9
- package/app/main.ts +30 -7
- package/app/routes/TestDynamic.js +40 -0
- package/app/routes/TestDynamic.ts +40 -0
- package/framework/core/component.js +71 -2
- package/framework/core/directives.js +166 -6
- package/framework/index.js +1 -1
- package/framework/types/component.d.ts +11 -0
- package/framework/types/directives.d.ts +7 -0
- package/index.html +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Fusée Framework 🚀
|
|
2
|
-
**v1.
|
|
2
|
+
**v1.3.0 — Signals-First JS Framework | Atomic Reactivity | Peak Performance**
|
|
3
3
|
|
|
4
4
|
Fusée is a custom, high-performance fine-grained reactive JavaScript framework built for speed and simplicity. It features a recursive, non-greedy compiler, a signals-based reactivity engine, Dependency Injection for components and a comprehensive CLI for instant application scaffolding.
|
|
5
5
|
|
|
@@ -44,20 +44,29 @@ A typical Fusée project looks like this:
|
|
|
44
44
|
## 📝 Example Component
|
|
45
45
|
|
|
46
46
|
```javascript
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
export const Counter = defineComponent({
|
|
47
|
+
export const Welcome = defineComponent({
|
|
50
48
|
setup() {
|
|
49
|
+
const name = signal('World');
|
|
51
50
|
const count = signal(0);
|
|
51
|
+
const double = computed(() => count() * 2);
|
|
52
|
+
|
|
52
53
|
return {
|
|
54
|
+
name,
|
|
53
55
|
count,
|
|
56
|
+
double,
|
|
54
57
|
inc: () => count(count() + 1),
|
|
55
|
-
dec: () => count(count() - 1),
|
|
56
58
|
template: `
|
|
57
|
-
<div class="
|
|
58
|
-
<h2>
|
|
59
|
-
<
|
|
60
|
-
|
|
59
|
+
<div class="card">
|
|
60
|
+
<h2>Hello, {{ name }}!</h2>
|
|
61
|
+
<input f-model="name" placeholder="Name" />
|
|
62
|
+
|
|
63
|
+
<button @click.debounce.300ms="inc">
|
|
64
|
+
Clicked {{ count }} times
|
|
65
|
+
</button>
|
|
66
|
+
|
|
67
|
+
<p f-if="count() > 0">
|
|
68
|
+
Double: <strong>{{ double }}</strong>
|
|
69
|
+
</p>
|
|
61
70
|
</div>
|
|
62
71
|
`
|
|
63
72
|
};
|
|
@@ -71,5 +80,6 @@ export const Counter = defineComponent({
|
|
|
71
80
|
|
|
72
81
|
- [ ] Global Store Management
|
|
73
82
|
- [ ] Advanced file-based routing support
|
|
83
|
+
- [ ] Integrated lightweight compiler which transforms declarative HTML into optimized runtime instructions
|
|
74
84
|
|
|
75
85
|
Built with ❤️ by me, Sebi Somu, a forward-thinking JavaScript Architect.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export const DynamicA = defineComponent({
|
|
2
|
+
props: ['title'],
|
|
3
|
+
setup(props) {
|
|
4
|
+
onMount(() => console.log('✅ DynamicA mounted'))
|
|
5
|
+
onUnmount(() => console.log('❌ DynamicA unmounted'))
|
|
6
|
+
|
|
7
|
+
const localValue = signal('')
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
title: props.title,
|
|
11
|
+
localValue,
|
|
12
|
+
template: `
|
|
13
|
+
<div style="padding: 20px; background: #1e293b; border-radius: 8px; border: 2px solid #3b82f6;">
|
|
14
|
+
<h3 style="color: #3b82f6;">Component A</h3>
|
|
15
|
+
<p>Prop from parent: <strong>{{ title }}</strong></p>
|
|
16
|
+
<div style="margin-top: 10px;">
|
|
17
|
+
<label>Local state (persists with f-keepAlive):</label><br>
|
|
18
|
+
<input f-model="localValue" placeholder="Type something here..." style="background: #0f172a; color: white; border: 1px solid #3b82f6; padding: 5px; width: 100%;">
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
`
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
})
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export const DynamicA = defineComponent({
|
|
2
|
+
props: ['title'],
|
|
3
|
+
setup(props: { title: string }) {
|
|
4
|
+
onMount(() => console.log('✅ DynamicA mounted'))
|
|
5
|
+
onUnmount(() => console.log('❌ DynamicA unmounted'))
|
|
6
|
+
|
|
7
|
+
const localValue = signal('')
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
title: props.title,
|
|
11
|
+
localValue,
|
|
12
|
+
template: `
|
|
13
|
+
<div style="padding: 20px; background: #1e293b; border-radius: 8px; border: 2px solid #3b82f6;">
|
|
14
|
+
<h3 style="color: #3b82f6;">Component A</h3>
|
|
15
|
+
<p>Prop from parent: <strong>{{ title }}</strong></p>
|
|
16
|
+
<div style="margin-top: 10px;">
|
|
17
|
+
<label>Local state (persists with f-keepAlive):</label><br>
|
|
18
|
+
<input f-model="localValue" placeholder="Type something here..." style="background: #0f172a; color: white; border: 1px solid #3b82f6; padding: 5px; width: 100%;">
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
`
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
})
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export const DynamicB = defineComponent({
|
|
2
|
+
props: ['title'],
|
|
3
|
+
setup(props) {
|
|
4
|
+
onMount(() => console.log('✅ DynamicB mounted'))
|
|
5
|
+
onUnmount(() => console.log('❌ DynamicB unmounted'))
|
|
6
|
+
|
|
7
|
+
const localInfo = signal('')
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
title: props.title,
|
|
11
|
+
localInfo,
|
|
12
|
+
template: `
|
|
13
|
+
<div style="padding: 20px; background: #1e293b; border-radius: 8px; border: 2px solid #ec4899;">
|
|
14
|
+
<h3 style="color: #ec4899;">Component B</h3>
|
|
15
|
+
<p>Prop from parent: <strong>{{ title }}</strong></p>
|
|
16
|
+
<div style="margin-top: 10px;">
|
|
17
|
+
<label>Local info (B):</label><br>
|
|
18
|
+
<input f-model="localInfo" placeholder="B settings..." style="background: #1a1a1a; color: white; border: 1px solid #ec4899; padding: 5px; width: 100%;">
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
`
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
})
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export const DynamicB = defineComponent({
|
|
2
|
+
props: ['title'],
|
|
3
|
+
setup(props: { title: string }) {
|
|
4
|
+
onMount(() => console.log('✅ DynamicB mounted'))
|
|
5
|
+
onUnmount(() => console.log('❌ DynamicB unmounted'))
|
|
6
|
+
|
|
7
|
+
const localInfo = signal('')
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
title: props.title,
|
|
11
|
+
localInfo,
|
|
12
|
+
template: `
|
|
13
|
+
<div style="padding: 20px; background: #1e293b; border-radius: 8px; border: 2px solid #ec4899;">
|
|
14
|
+
<h3 style="color: #ec4899;">Component B</h3>
|
|
15
|
+
<p>Prop from parent: <strong>{{ title }}</strong></p>
|
|
16
|
+
<div style="margin-top: 10px;">
|
|
17
|
+
<label>Local info (B):</label><br>
|
|
18
|
+
<input f-model="localInfo" placeholder="B settings..." style="background: #1a1a1a; color: white; border: 1px solid #ec4899; padding: 5px; width: 100%;">
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
`
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
})
|
|
@@ -2,14 +2,13 @@ export const EmitTest = defineComponent({
|
|
|
2
2
|
setup(props, { emit }) {
|
|
3
3
|
const clickCount = signal(0)
|
|
4
4
|
const lastEvent = signal('None')
|
|
5
|
-
|
|
5
|
+
|
|
6
6
|
inspect(clickCount, lastEvent)
|
|
7
|
-
|
|
7
|
+
|
|
8
8
|
return {
|
|
9
9
|
clickCount,
|
|
10
10
|
lastEvent,
|
|
11
|
-
|
|
12
|
-
// Butoane pentru test emit
|
|
11
|
+
|
|
13
12
|
emitSimple: () => {
|
|
14
13
|
clickCount(clickCount() + 1)
|
|
15
14
|
emit('simple-click')
|
|
@@ -22,7 +21,7 @@ export const EmitTest = defineComponent({
|
|
|
22
21
|
lastEvent(`click-with-data: ${count}`)
|
|
23
22
|
},
|
|
24
23
|
emitObject: () => {
|
|
25
|
-
const data = {
|
|
24
|
+
const data = {
|
|
26
25
|
timestamp: Date.now(),
|
|
27
26
|
message: 'Hello from child!',
|
|
28
27
|
counter: clickCount() + 1
|
|
@@ -31,7 +30,7 @@ export const EmitTest = defineComponent({
|
|
|
31
30
|
emit('object-event', data)
|
|
32
31
|
lastEvent(`object-event: ${data.message}`)
|
|
33
32
|
},
|
|
34
|
-
|
|
33
|
+
|
|
35
34
|
template: `
|
|
36
35
|
<div style="background: linear-gradient(135deg, #1e3a5f 0%, #0d1b2a 100%); border: 2px solid #4da6ff; border-radius: 12px; padding: 24px; margin: 20px 0; color: #fff;">
|
|
37
36
|
<h3 style="margin: 0 0 16px 0; color: #4da6ff;">🧪 EMIT EVENTS TEST LAB</h3>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { defineComponent } from '../../framework/index.js'
|
|
2
|
+
|
|
3
|
+
export const Loading = defineComponent({
|
|
4
|
+
setup() {
|
|
5
|
+
return {
|
|
6
|
+
template: `
|
|
7
|
+
<div style="display: flex; justify-content: center; align-items: center; padding: 2rem;">
|
|
8
|
+
<div style="
|
|
9
|
+
width: 40px;
|
|
10
|
+
height: 40px;
|
|
11
|
+
border: 4px solid #f3f3f3;
|
|
12
|
+
border-top: 4px solid #3b82f6;
|
|
13
|
+
border-radius: 50%;
|
|
14
|
+
animation: spin 1s linear infinite;
|
|
15
|
+
"></div>
|
|
16
|
+
<style>
|
|
17
|
+
@keyframes spin {
|
|
18
|
+
0% { transform: rotate(0deg); }
|
|
19
|
+
100% { transform: rotate(360deg); }
|
|
20
|
+
}
|
|
21
|
+
</style>
|
|
22
|
+
</div>
|
|
23
|
+
`
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
})
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { defineComponent } from '../../framework/index.js'
|
|
2
|
+
|
|
3
|
+
export const Loading = defineComponent({
|
|
4
|
+
setup() {
|
|
5
|
+
return {
|
|
6
|
+
template: `
|
|
7
|
+
<div style="display: flex; justify-content: center; align-items: center; padding: 2rem;">
|
|
8
|
+
<div style="
|
|
9
|
+
width: 40px;
|
|
10
|
+
height: 40px;
|
|
11
|
+
border: 4px solid #f3f3f3;
|
|
12
|
+
border-top: 4px solid #3b82f6;
|
|
13
|
+
border-radius: 50%;
|
|
14
|
+
animation: spin 1s linear infinite;
|
|
15
|
+
"></div>
|
|
16
|
+
<style>
|
|
17
|
+
@keyframes spin {
|
|
18
|
+
0% { transform: rotate(0deg); }
|
|
19
|
+
100% { transform: rotate(360deg); }
|
|
20
|
+
}
|
|
21
|
+
</style>
|
|
22
|
+
</div>
|
|
23
|
+
`
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
})
|
package/app/main.js
CHANGED
|
@@ -1,13 +1,39 @@
|
|
|
1
|
-
import { createRouter } from '../framework/index.js'
|
|
2
|
-
import {
|
|
3
|
-
import { About } from './routes/About.js'
|
|
4
|
-
import { TestFor } from './routes/TestFor.js'
|
|
1
|
+
import { createRouter, mountOutlet, defineAsyncComponent } from '../framework/index.js'
|
|
2
|
+
import { Loading } from './components/Loading.js'
|
|
5
3
|
|
|
6
|
-
|
|
7
|
-
{
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
4
|
+
const routes = [
|
|
5
|
+
{
|
|
6
|
+
path: '/',
|
|
7
|
+
component: defineAsyncComponent({
|
|
8
|
+
loader: () => import('./routes/Home.js'),
|
|
9
|
+
loadingComponent: Loading
|
|
10
|
+
})
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
path: '/about',
|
|
14
|
+
component: defineAsyncComponent({
|
|
15
|
+
loader: () => import('./routes/About.js'),
|
|
16
|
+
loadingComponent: Loading
|
|
17
|
+
})
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
path: '/test-for',
|
|
21
|
+
component: defineAsyncComponent({
|
|
22
|
+
loader: () => import('./routes/TestFor.js'),
|
|
23
|
+
loadingComponent: Loading
|
|
24
|
+
})
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
path: '/dynamic',
|
|
28
|
+
component: defineAsyncComponent({
|
|
29
|
+
loader: () => import('./routes/TestDynamic.js'),
|
|
30
|
+
loadingComponent: Loading
|
|
31
|
+
})
|
|
32
|
+
},
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
createRouter(routes)
|
|
11
36
|
|
|
12
37
|
const outlet = document.getElementById('app')
|
|
38
|
+
if (!outlet) throw new Error('[app] #app element not found in DOM')
|
|
13
39
|
mountOutlet(outlet)
|
package/app/main.ts
CHANGED
|
@@ -1,13 +1,36 @@
|
|
|
1
|
-
import { createRouter,
|
|
1
|
+
import { createRouter, mountOutlet, defineAsyncComponent } from '../framework/index.js'
|
|
2
2
|
import type { Route } from '../framework/types/index.js'
|
|
3
|
-
import {
|
|
4
|
-
import { About } from './routes/About.js'
|
|
5
|
-
import { TestFor } from './routes/TestFor.js'
|
|
3
|
+
import { Loading } from './components/Loading.js'
|
|
6
4
|
|
|
7
5
|
const routes: Route[] = [
|
|
8
|
-
{
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
{
|
|
7
|
+
path: '/',
|
|
8
|
+
component: defineAsyncComponent({
|
|
9
|
+
loader: () => import('./routes/Home.js'),
|
|
10
|
+
loadingComponent: Loading
|
|
11
|
+
})
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
path: '/about',
|
|
15
|
+
component: defineAsyncComponent({
|
|
16
|
+
loader: () => import('./routes/About.js'),
|
|
17
|
+
loadingComponent: Loading
|
|
18
|
+
})
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
path: '/test-for',
|
|
22
|
+
component: defineAsyncComponent({
|
|
23
|
+
loader: () => import('./routes/TestFor.js'),
|
|
24
|
+
loadingComponent: Loading
|
|
25
|
+
})
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
path: '/dynamic',
|
|
29
|
+
component: defineAsyncComponent({
|
|
30
|
+
loader: () => import('./routes/TestDynamic.js'),
|
|
31
|
+
loadingComponent: Loading
|
|
32
|
+
})
|
|
33
|
+
},
|
|
11
34
|
]
|
|
12
35
|
|
|
13
36
|
createRouter(routes)
|
|
@@ -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
|
+
})
|
|
@@ -187,15 +187,84 @@ export function defineComponent(options) {
|
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
export function provide(key, value) {
|
|
190
|
-
if(!currentInstance) return
|
|
190
|
+
if (!currentInstance) return
|
|
191
191
|
currentInstance._provides[key] = value
|
|
192
192
|
}
|
|
193
193
|
|
|
194
194
|
export function inject(key) {
|
|
195
195
|
let parent = currentInstance?._parent
|
|
196
|
-
while(parent) {
|
|
196
|
+
while (parent) {
|
|
197
197
|
if (key in parent._provides) return parent._provides[key];
|
|
198
198
|
parent = parent._parent;
|
|
199
199
|
}
|
|
200
200
|
return null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function defineAsyncComponent(loaderOrOptions) {
|
|
204
|
+
const options = typeof loaderOrOptions === 'function'
|
|
205
|
+
? { loader: loaderOrOptions }
|
|
206
|
+
: loaderOrOptions
|
|
207
|
+
|
|
208
|
+
return function AsyncComponentFactory(props = {}, { listeners = {}, slots = {}, parent = null } = {}) {
|
|
209
|
+
const instance = {
|
|
210
|
+
props,
|
|
211
|
+
_mountHooks: [],
|
|
212
|
+
_unmountHooks: [],
|
|
213
|
+
_effects: [],
|
|
214
|
+
_element: null,
|
|
215
|
+
_provides: {},
|
|
216
|
+
_parent: parent,
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
let childApi = null
|
|
220
|
+
let loadingApi = null
|
|
221
|
+
let isUnmounted = false
|
|
222
|
+
|
|
223
|
+
function render(container) {
|
|
224
|
+
instance._element = container
|
|
225
|
+
|
|
226
|
+
if (options.loadingComponent) {
|
|
227
|
+
loadingApi = options.loadingComponent({}, { parent })
|
|
228
|
+
loadingApi.render(container)
|
|
229
|
+
} else {
|
|
230
|
+
container.innerHTML = '<!-- async component boundary -->'
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
Promise.resolve(options.loader())
|
|
234
|
+
.then(comp => {
|
|
235
|
+
if (isUnmounted) return
|
|
236
|
+
let ComponentFn = comp.default || comp
|
|
237
|
+
|
|
238
|
+
if (typeof ComponentFn === 'object' && ComponentFn !== null) {
|
|
239
|
+
ComponentFn = Object.values(ComponentFn).find(v => typeof v === 'function') || ComponentFn
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (loadingApi) {
|
|
243
|
+
loadingApi.unmount()
|
|
244
|
+
loadingApi = null
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
childApi = ComponentFn(props, { listeners, slots, parent })
|
|
248
|
+
|
|
249
|
+
container.innerHTML = ''
|
|
250
|
+
childApi.render(container)
|
|
251
|
+
})
|
|
252
|
+
.catch(err => {
|
|
253
|
+
console.error('[framework] Failed to load async component:', err)
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
return instance
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function unmount() {
|
|
260
|
+
isUnmounted = true
|
|
261
|
+
if (loadingApi) loadingApi.unmount()
|
|
262
|
+
if (childApi) childApi.unmount()
|
|
263
|
+
if (instance._element && !childApi && !loadingApi) {
|
|
264
|
+
instance._element.innerHTML = ''
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return { render, unmount, instance }
|
|
269
|
+
}
|
|
201
270
|
}
|
|
@@ -5,6 +5,7 @@ import { compileNode } from './compiler.js'
|
|
|
5
5
|
export function processDirectives(root, context, components, effects) {
|
|
6
6
|
if (processFor(root, context, components, effects)) return true
|
|
7
7
|
if (processIf(root, context, components, effects)) return true
|
|
8
|
+
if (processIs(root, context, components, effects)) return true
|
|
8
9
|
|
|
9
10
|
processShow(root, context, effects)
|
|
10
11
|
processModel(root, context, effects)
|
|
@@ -275,6 +276,110 @@ export function processIf(el, context, components, effects) {
|
|
|
275
276
|
return true
|
|
276
277
|
}
|
|
277
278
|
|
|
279
|
+
export function processIs(el, context, components, effects) {
|
|
280
|
+
if (el.nodeType !== 1 || !el.hasAttribute('f-is')) return false
|
|
281
|
+
|
|
282
|
+
// 1. Prepare directive state
|
|
283
|
+
const expr = el.getAttribute('f-is')
|
|
284
|
+
const keepAlive = processKeepAlive(el)
|
|
285
|
+
|
|
286
|
+
el.removeAttribute('f-is')
|
|
287
|
+
|
|
288
|
+
const anchor = document.createComment('f-keepAlive-anchor')
|
|
289
|
+
el.parentNode.insertBefore(anchor, el)
|
|
290
|
+
|
|
291
|
+
// Keep a clone to reuse attributes (props/@events) for every dynamic instance
|
|
292
|
+
const templateNode = el.cloneNode(true)
|
|
293
|
+
el.remove()
|
|
294
|
+
|
|
295
|
+
const cache = new Map()
|
|
296
|
+
let activeKey = null
|
|
297
|
+
let activeCleanup = []
|
|
298
|
+
|
|
299
|
+
// 2. Reactive loop
|
|
300
|
+
effects.push(effect(() => {
|
|
301
|
+
const rawValue = evaluateExpression(expr, context)
|
|
302
|
+
|
|
303
|
+
// Skip if same component is already active
|
|
304
|
+
if (rawValue === activeKey) return
|
|
305
|
+
|
|
306
|
+
// 3. Handle exiting component
|
|
307
|
+
if (activeKey !== null) {
|
|
308
|
+
const currentEntry = cache.get(activeKey)
|
|
309
|
+
if (currentEntry) {
|
|
310
|
+
if (keepAlive) {
|
|
311
|
+
currentEntry.node.remove()
|
|
312
|
+
} else {
|
|
313
|
+
for (const cleanup of activeCleanup) if (typeof cleanup === 'function') cleanup()
|
|
314
|
+
currentEntry.node.remove()
|
|
315
|
+
cache.delete(activeKey)
|
|
316
|
+
activeCleanup = []
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (!rawValue) {
|
|
322
|
+
activeKey = null
|
|
323
|
+
return
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// 4. Handle entering component
|
|
327
|
+
let entry = cache.get(rawValue)
|
|
328
|
+
|
|
329
|
+
if (entry && keepAlive) {
|
|
330
|
+
// Restore from cache
|
|
331
|
+
anchor.parentNode.insertBefore(entry.node, anchor.nextSibling)
|
|
332
|
+
activeCleanup = entry.cleanup
|
|
333
|
+
} else {
|
|
334
|
+
// Create new instance
|
|
335
|
+
let ComponentFn = typeof rawValue === 'string' ? components[rawValue] : rawValue
|
|
336
|
+
|
|
337
|
+
if (typeof ComponentFn !== 'function') {
|
|
338
|
+
console.warn(`[framework] f-is: Component "${rawValue}" not found or invalid.`)
|
|
339
|
+
return
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const compEl = templateNode.cloneNode(true)
|
|
343
|
+
compEl._isDynamicComponent = true
|
|
344
|
+
|
|
345
|
+
const tempName = '__dynamic_component__'
|
|
346
|
+
compEl.setAttribute('data-component', tempName)
|
|
347
|
+
|
|
348
|
+
anchor.parentNode.insertBefore(compEl, anchor.nextSibling)
|
|
349
|
+
|
|
350
|
+
const childCleanup = []
|
|
351
|
+
const dynamicComponents = { ...components, [tempName]: ComponentFn }
|
|
352
|
+
|
|
353
|
+
// Trigger the internal compiler to bind props and events
|
|
354
|
+
compileNode(compEl, context, dynamicComponents, childCleanup)
|
|
355
|
+
|
|
356
|
+
entry = { node: compEl, cleanup: childCleanup }
|
|
357
|
+
cache.set(rawValue, entry)
|
|
358
|
+
activeCleanup = childCleanup
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
activeKey = rawValue
|
|
362
|
+
}))
|
|
363
|
+
|
|
364
|
+
// Cleanup all cached components when this directive is destroyed
|
|
365
|
+
effects.push(() => {
|
|
366
|
+
for (const entry of cache.values()) {
|
|
367
|
+
for (const c of entry.cleanup) if (typeof c === 'function') c()
|
|
368
|
+
entry.node.remove()
|
|
369
|
+
}
|
|
370
|
+
cache.clear()
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
return true
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function processKeepAlive(el) {
|
|
377
|
+
if (el.nodeType !== 1) return false
|
|
378
|
+
const hasKeepAlive = el.hasAttribute('f-keepAlive')
|
|
379
|
+
if (hasKeepAlive) el.removeAttribute('f-keepAlive')
|
|
380
|
+
return hasKeepAlive
|
|
381
|
+
}
|
|
382
|
+
|
|
278
383
|
export function processShow(el, context, effects) {
|
|
279
384
|
if (el.nodeType !== 1 || !el.hasAttribute('f-show')) return
|
|
280
385
|
|
|
@@ -304,6 +409,7 @@ export function processHtml(el, context, effects) {
|
|
|
304
409
|
el.innerHTML = String(val ?? '')
|
|
305
410
|
}))
|
|
306
411
|
}
|
|
412
|
+
|
|
307
413
|
export function processEvents(el, context, effects) {
|
|
308
414
|
if (el.nodeType !== 1 || !el.attributes) return
|
|
309
415
|
|
|
@@ -317,6 +423,39 @@ export function processEvents(el, context, effects) {
|
|
|
317
423
|
|
|
318
424
|
el.removeAttribute(attr.name)
|
|
319
425
|
|
|
426
|
+
let timeoutId
|
|
427
|
+
let lastRun = 0
|
|
428
|
+
let throttleTimeoutId
|
|
429
|
+
|
|
430
|
+
let debounceMs = 0
|
|
431
|
+
let throttleMs = 0
|
|
432
|
+
|
|
433
|
+
const parseDuration = (idx) => {
|
|
434
|
+
const next = modifiers[idx + 1]
|
|
435
|
+
if (next && /^\d/.test(next)) {
|
|
436
|
+
if (next.endsWith('ms')) return parseInt(next) || 250
|
|
437
|
+
if (next.endsWith('s')) return (parseFloat(next) || 0.25) * 1000
|
|
438
|
+
}
|
|
439
|
+
return 250 // Default
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const debounceIdx = modifiers.indexOf('debounce')
|
|
443
|
+
if (debounceIdx > -1) debounceMs = parseDuration(debounceIdx)
|
|
444
|
+
|
|
445
|
+
const throttleIdx = modifiers.indexOf('throttle')
|
|
446
|
+
if (throttleIdx > -1) throttleMs = parseDuration(throttleIdx)
|
|
447
|
+
|
|
448
|
+
const executeLogic = (e) => {
|
|
449
|
+
batch(() => {
|
|
450
|
+
const potentialFn = context[expr]
|
|
451
|
+
if (typeof potentialFn === 'function' && !potentialFn.isSignal) {
|
|
452
|
+
potentialFn(e)
|
|
453
|
+
} else {
|
|
454
|
+
evaluateExpression(expr, context, { $event: e }, false)
|
|
455
|
+
}
|
|
456
|
+
})
|
|
457
|
+
}
|
|
458
|
+
|
|
320
459
|
const handler = (e) => {
|
|
321
460
|
if (modifiers.includes('prevent')) e.preventDefault()
|
|
322
461
|
if (modifiers.includes('stop')) e.stopPropagation()
|
|
@@ -336,14 +475,33 @@ export function processEvents(el, context, effects) {
|
|
|
336
475
|
}
|
|
337
476
|
}
|
|
338
477
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
478
|
+
// Throttle logic (ensures execution at most once every X ms)
|
|
479
|
+
if (throttleMs > 0) {
|
|
480
|
+
const now = Date.now()
|
|
481
|
+
const remaining = throttleMs - (now - lastRun)
|
|
482
|
+
|
|
483
|
+
if (remaining <= 0) {
|
|
484
|
+
clearTimeout(throttleTimeoutId)
|
|
485
|
+
lastRun = now
|
|
486
|
+
executeLogic(e)
|
|
343
487
|
} else {
|
|
344
|
-
|
|
488
|
+
// Standard throttle with trailing edge support
|
|
489
|
+
clearTimeout(throttleTimeoutId)
|
|
490
|
+
throttleTimeoutId = setTimeout(() => {
|
|
491
|
+
lastRun = Date.now()
|
|
492
|
+
executeLogic(e)
|
|
493
|
+
}, remaining)
|
|
345
494
|
}
|
|
346
|
-
|
|
495
|
+
return
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Debounce logic (waits for X ms of inactivity)
|
|
499
|
+
if (debounceMs > 0) {
|
|
500
|
+
clearTimeout(timeoutId)
|
|
501
|
+
timeoutId = setTimeout(() => executeLogic(e), debounceMs)
|
|
502
|
+
} else {
|
|
503
|
+
executeLogic(e)
|
|
504
|
+
}
|
|
347
505
|
}
|
|
348
506
|
|
|
349
507
|
const targetNode = modifiers.includes('window') ? window : (modifiers.includes('document') ? document : el)
|
|
@@ -356,6 +514,8 @@ export function processEvents(el, context, effects) {
|
|
|
356
514
|
if (effects) {
|
|
357
515
|
effects.push(() => {
|
|
358
516
|
targetNode.removeEventListener(eventName, handler, options)
|
|
517
|
+
if (timeoutId) clearTimeout(timeoutId)
|
|
518
|
+
if (throttleTimeoutId) clearTimeout(throttleTimeoutId)
|
|
359
519
|
})
|
|
360
520
|
}
|
|
361
521
|
}
|
package/framework/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// ─── Framework Public API ─────────────────────────────────────────────────────
|
|
2
2
|
export { signal, computed, effect, batch, untrack, inspect } from './core/signal.js'
|
|
3
|
-
export { defineComponent, onMount, onUnmount, parseSlots, provide, inject } from './core/component.js'
|
|
3
|
+
export { defineComponent, defineAsyncComponent, 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
6
|
|
|
@@ -70,3 +70,14 @@ export declare function inject<T>(key: string): T | null
|
|
|
70
70
|
export declare function defineComponent<TProps = EmptyProps>(
|
|
71
71
|
options: ComponentOptions<TProps>
|
|
72
72
|
): ComponentFactory<TProps>
|
|
73
|
+
|
|
74
|
+
export type AsyncComponentLoader<TProps = ComponentProps> = () => Promise<ComponentFactory<TProps> | { default: ComponentFactory<TProps> } | Record<string, any>>
|
|
75
|
+
|
|
76
|
+
export type AsyncComponentOptions<TProps = ComponentProps> = {
|
|
77
|
+
loader: AsyncComponentLoader<TProps>
|
|
78
|
+
loadingComponent?: ComponentFactory<any>
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export declare function defineAsyncComponent<TProps = ComponentProps>(
|
|
82
|
+
loaderOrOptions: AsyncComponentLoader<TProps> | AsyncComponentOptions<TProps>
|
|
83
|
+
): ComponentFactory<TProps>
|
|
@@ -25,6 +25,13 @@ export function processIf(
|
|
|
25
25
|
effects: (() => void)[]
|
|
26
26
|
): void
|
|
27
27
|
|
|
28
|
+
export function processIs(
|
|
29
|
+
root: HTMLElement,
|
|
30
|
+
context: ComponentResult,
|
|
31
|
+
components: Record<string, ComponentFactory>,
|
|
32
|
+
effects: (() => void)[]
|
|
33
|
+
): void
|
|
34
|
+
|
|
28
35
|
export function processShow(root: HTMLElement, context: ComponentResult, effects: (() => void)[]): void
|
|
29
36
|
export function processHtml(root: HTMLElement, context: ComponentResult, effects: (() => void)[]): void
|
|
30
37
|
export function processText(root: HTMLElement, context: ComponentResult, effects: (() => void)[]): void
|
package/index.html
CHANGED
|
@@ -167,7 +167,8 @@
|
|
|
167
167
|
<nav>
|
|
168
168
|
<a href="#/">Home</a>
|
|
169
169
|
<a href="#/about">About</a>
|
|
170
|
-
<a href="#/test-for">Test
|
|
170
|
+
<a href="#/test-for">Loops Test</a>
|
|
171
|
+
<a href="#/dynamic" style="color: #ec4899;">Dynamic Components</a>
|
|
171
172
|
</nav>
|
|
172
173
|
<div id="app"></div>
|
|
173
174
|
<script type="module" src="./app/main.js"></script>
|