fusee-framework 0.1.0-alpha.1 → 1.0.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 +75 -8
- package/app/components/Counter.js +111 -34
- package/app/components/Counter.ts +99 -0
- package/app/components/CounterSection.js +34 -0
- package/app/components/CounterSection.ts +45 -0
- package/app/components/UserForm.js +31 -0
- package/app/components/UserForm.ts +38 -0
- package/app/main.js +4 -2
- package/app/main.ts +15 -0
- package/app/routes/About.js +17 -17
- package/app/routes/About.ts +29 -0
- package/app/routes/Home.js +22 -17
- package/app/routes/Home.ts +34 -0
- package/app/routes/TestFor.js +43 -0
- package/framework/bin/create-fusee.js +147 -0
- package/framework/core/compiler.js +214 -0
- package/framework/core/component.js +98 -107
- package/framework/core/directives.js +369 -0
- package/framework/core/evaluator.js +70 -0
- package/framework/core/signal.js +125 -40
- package/framework/index.d.ts +3 -0
- package/framework/index.js +1 -1
- package/framework/router/router.js +52 -43
- package/framework/types/compiler.d.ts +25 -0
- package/framework/types/component.d.ts +44 -0
- package/framework/types/directives.d.ts +32 -0
- package/framework/types/evaluator.d.ts +15 -0
- package/framework/types/index.d.ts +9 -0
- package/framework/types/router.d.ts +17 -0
- package/framework/types/signal.d.ts +30 -0
- package/index.html +76 -8
- package/package.json +14 -3
- package/tsconfig.json +38 -0
- package/vite.config.js +30 -0
package/README.md
CHANGED
|
@@ -1,8 +1,75 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
# Fusée Framework 🚀
|
|
2
|
+
**v1.0.1 — Signals-First JS Framework | Atomic Reactivity | Peak Performance**
|
|
3
|
+
|
|
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, and a comprehensive CLI for instant application scaffolding.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## ⚡ Quick Start
|
|
9
|
+
|
|
10
|
+
The fastest way to get started with Fusée is via the CLI:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
# 1. Install Fusée globally
|
|
14
|
+
npm install -g fusee-framework
|
|
15
|
+
|
|
16
|
+
# 2. Scaffold your first project
|
|
17
|
+
create-fusee-app .
|
|
18
|
+
```
|
|
19
|
+
Follow the interactive prompt to choose your template (**JavaScript** or **TypeScript**) and launch your app with `npm run dev`!
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 🔥 Key Features
|
|
24
|
+
|
|
25
|
+
- **Signals-First Reactivity**: Modern atomic updates that ensure only the modified parts of the DOM are touched.
|
|
26
|
+
- **Recursive Hybrid Compiler**: A robust architectural approach to node traversal, eliminating rendering bugs while maintaining "greedy" directive processing for speed.
|
|
27
|
+
- **Strict Component System**: Use `defineComponent` for prop validation, lifecycle hooks (`onMount`, `onUnmount`), and automated cleanup.
|
|
28
|
+
- **Optimized Directives**: Native support for `f-if`, `f-for`, `f-model`, `f-text`, `f-cloak` and more.
|
|
29
|
+
- **Performance Shield (`f-once`)**: Isolate and stabilize static subtrees to prevent unnecessary reactivity.
|
|
30
|
+
- **Vite Integration**: Full support for the fastest development workflow and Hot Module Replacement (HMR).
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## 🛠 Project Structure
|
|
35
|
+
|
|
36
|
+
A typical Fusée project looks like this:
|
|
37
|
+
|
|
38
|
+
- **`framework/`**: The core reactive engine.
|
|
39
|
+
- **`app/`**: Your application logic (routes, components).
|
|
40
|
+
- **`index.html`**: The entry point of your reactive world.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 📝 Example Component
|
|
45
|
+
|
|
46
|
+
```javascript
|
|
47
|
+
import { defineComponent, signal } from './framework/core/index.js';
|
|
48
|
+
|
|
49
|
+
export const Counter = defineComponent({
|
|
50
|
+
setup() {
|
|
51
|
+
const count = signal(0);
|
|
52
|
+
return {
|
|
53
|
+
count,
|
|
54
|
+
inc: () => count(count() + 1),
|
|
55
|
+
dec: () => count(count() - 1),
|
|
56
|
+
template: `
|
|
57
|
+
<div class="counter-card">
|
|
58
|
+
<h2>Counter: {{ count }}</h2>
|
|
59
|
+
<button @click="dec">-</button>
|
|
60
|
+
<button @click="inc">+</button>
|
|
61
|
+
</div>
|
|
62
|
+
`
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## 🚀 Future Roadmap
|
|
71
|
+
|
|
72
|
+
- [ ] Global Store Management
|
|
73
|
+
- [ ] Advanced file-based routing support
|
|
74
|
+
|
|
75
|
+
Built with ❤️ by me, Sebi Somu, a forward-thinking JavaScript Architect.
|
|
@@ -1,38 +1,115 @@
|
|
|
1
|
-
import { defineComponent, signal, onMount, onUnmount } from '../../framework/index.js'
|
|
1
|
+
import { defineComponent, signal, computed, effect, batch, untrack, onMount, onUnmount } from '../../framework/index.js'
|
|
2
2
|
|
|
3
3
|
export const Counter = defineComponent({
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
4
|
+
props: {
|
|
5
|
+
initialValue: { type: String, default: '0' },
|
|
6
|
+
parentTitle: { type: String, default: '' }
|
|
7
|
+
},
|
|
8
|
+
setup(props) {
|
|
9
|
+
const count = signal(Number(props.initialValue) || 0)
|
|
10
|
+
const multiplier = signal(1)
|
|
11
|
+
|
|
12
|
+
const double = computed(() => {
|
|
13
|
+
console.log('[Counter] computing double...')
|
|
14
|
+
return count() * 2
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
const isZero = computed(() => count() === 0)
|
|
18
|
+
|
|
19
|
+
const counterTitle = computed(() =>
|
|
20
|
+
`Counter: ${count()} (×${multiplier()})`
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
const displayStyle = computed(() => ({
|
|
24
|
+
fontSize: '16rem',
|
|
25
|
+
fontWeight: '900',
|
|
26
|
+
lineHeight: '1',
|
|
27
|
+
margin: '0',
|
|
28
|
+
color: count() >= 0 ? '#3b82f6' : '#ef4444',
|
|
29
|
+
transition: 'all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275)',
|
|
30
|
+
transform: `scale(${1 + (Math.abs(count()) % 3) * 0.1})`
|
|
31
|
+
}))
|
|
32
|
+
|
|
33
|
+
effect(() => {
|
|
34
|
+
console.log(`[Counter Effect] Count: ${count()} | Step ignored in tracking: ${untrack(() => multiplier())}`)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
function updateMultiple() {
|
|
38
|
+
batch(() => {
|
|
39
|
+
count(count() + 5)
|
|
40
|
+
multiplier(multiplier() + 1)
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function increment() { count(count() + 1) }
|
|
45
|
+
function decrement() { count(count() - 1) }
|
|
46
|
+
function reset() { count(0) }
|
|
47
|
+
|
|
48
|
+
onMount(() => console.log('[Counter] mounted, initial:', count()))
|
|
49
|
+
onUnmount(() => console.log('[Counter] unmounted'))
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
count,
|
|
53
|
+
multiplier,
|
|
54
|
+
double,
|
|
55
|
+
isZero,
|
|
56
|
+
counterTitle,
|
|
57
|
+
increment,
|
|
58
|
+
decrement,
|
|
59
|
+
reset,
|
|
60
|
+
updateMultiple,
|
|
61
|
+
get parentTitle() { return props.parentTitle },
|
|
62
|
+
displayStyle,
|
|
63
|
+
template: `
|
|
64
|
+
<div class="counter" :title="counterTitle" @keydown.arrowup.window="increment" @keydown.arrowdown.window="decrement">
|
|
65
|
+
<h2>Counter</h2>
|
|
66
|
+
<p style="font-size: 0.8rem; color: #888;">Message from Home: {{ parentTitle }}</p>
|
|
67
|
+
|
|
68
|
+
<div class="counter-display" style="text-align: center; margin: 60px 0;">
|
|
69
|
+
<span style="display: block; font-size: 1.2rem; text-transform: uppercase; letter-spacing: 3px; color: #888;">Current Value</span>
|
|
70
|
+
<p class="value"
|
|
71
|
+
:class="{ positive: count >= 0, negative: count < 0 }"
|
|
72
|
+
:style="displayStyle">
|
|
73
|
+
{{ count }}
|
|
74
|
+
</p>
|
|
75
|
+
<span style="font-size: 1.2rem; color: #aaa;">(Multiplier step: {{ multiplier }})</span>
|
|
76
|
+
</div>
|
|
77
|
+
<p class="derived">double: {{ double }}</p>
|
|
78
|
+
|
|
79
|
+
<div class="actions">
|
|
80
|
+
<button @click.prevent="decrement">−</button>
|
|
81
|
+
<button @click.once="reset" :disabled="isZero">Reset (Once only demo)</button>
|
|
82
|
+
<button @click="increment">+</button>
|
|
83
|
+
<button @click="updateMultiple">+5 & Step (Batched)</button>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<div class="extras" style="margin-top: 24px; border-top: 1px solid rgba(0,0,0,0.08); padding-top: 20px;">
|
|
87
|
+
<p style="font-weight: 600; font-size: 0.9rem; margin-bottom: 12px; color: #444;">Advanced Controls</p>
|
|
88
|
+
|
|
89
|
+
<div style="display: flex; gap: 10px; margin-bottom: 15px;">
|
|
90
|
+
<input
|
|
91
|
+
type="text"
|
|
92
|
+
placeholder="Set count..."
|
|
93
|
+
style="flex: 1; padding: 10px 14px; border: 1px solid #ddd; border-radius: 8px; font-size: 0.9rem; outline: none; transition: border-color 0.2s;"
|
|
94
|
+
onfocus="this.style.borderColor = '#3b82f6'"
|
|
95
|
+
onblur="this.style.borderColor = '#ddd'"
|
|
96
|
+
@keyup.enter="count(Number($event.target.value) || 0); $event.target.value = ''"
|
|
97
|
+
>
|
|
98
|
+
<button
|
|
99
|
+
style="padding: 10px 16px; background: #3b82f6; color: white; border: none; border-radius: 8px; cursor: pointer; font-weight: 500; font-size: 0.9rem; transition: opacity 0.2s;"
|
|
100
|
+
onmouseover="this.style.opacity = '0.9'"
|
|
101
|
+
onmouseout="this.style.opacity = '1'"
|
|
102
|
+
@click="console.log('Clicked at: ' + $event.clientX + 'x' + $event.clientY)"
|
|
103
|
+
>
|
|
104
|
+
Log Position
|
|
105
|
+
</button>
|
|
106
|
+
</div>
|
|
107
|
+
<p style="font-size: 0.75rem; color: #888;">
|
|
108
|
+
Tip: Use Up/Down arrows to nudge globally, or Enter in the box above.
|
|
109
|
+
</p>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
`
|
|
113
|
+
}
|
|
36
114
|
}
|
|
37
|
-
}
|
|
38
115
|
})
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { defineComponent, signal, computed, effect, batch, untrack, onMount, onUnmount, type Signal, type Computed } from '../../framework/index.js'
|
|
2
|
+
|
|
3
|
+
type CounterProps = {
|
|
4
|
+
initialValue?: string
|
|
5
|
+
parentTitle?: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
type CounterResult = {
|
|
9
|
+
count: Signal<number>
|
|
10
|
+
multiplier: Signal<number>
|
|
11
|
+
double: Computed<number>
|
|
12
|
+
valueClass: Computed<string>
|
|
13
|
+
isZero: Computed<boolean>
|
|
14
|
+
counterTitle: Computed<string>
|
|
15
|
+
increment: () => void
|
|
16
|
+
decrement: () => void
|
|
17
|
+
reset: () => void
|
|
18
|
+
updateMultiple: () => void
|
|
19
|
+
parentTitle: string
|
|
20
|
+
template: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const Counter = defineComponent({
|
|
24
|
+
props: {
|
|
25
|
+
initialValue: { type: String, default: '0' },
|
|
26
|
+
parentTitle: { type: String, default: '' }
|
|
27
|
+
},
|
|
28
|
+
setup(props: CounterProps): CounterResult {
|
|
29
|
+
const count = signal<number>(Number(props.initialValue) || 0)
|
|
30
|
+
const multiplier = signal<number>(1)
|
|
31
|
+
|
|
32
|
+
const double = computed(() => count() * 2)
|
|
33
|
+
|
|
34
|
+
const valueClass = computed(() =>
|
|
35
|
+
count() >= 0 ? 'value positive' : 'value negative'
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
const isZero = computed(() => count() === 0)
|
|
39
|
+
|
|
40
|
+
const counterTitle = computed(() =>
|
|
41
|
+
`Counter: ${count()} (×${multiplier()})`
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
effect(() => {
|
|
45
|
+
console.log(`[Counter Effect] Count: ${count()} | Step ignored in tracking: ${untrack(() => multiplier())}`)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
function updateMultiple(): void {
|
|
49
|
+
batch(() => {
|
|
50
|
+
count(count() + 5)
|
|
51
|
+
multiplier(multiplier() + 1)
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function increment(): void {
|
|
56
|
+
count(count() + 1)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function decrement(): void {
|
|
60
|
+
count(count() - 1)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function reset(): void {
|
|
64
|
+
count(0)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
onMount(() => console.log('[Counter] mounted, initial:', count()))
|
|
68
|
+
onUnmount(() => console.log('[Counter] unmounted'))
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
count,
|
|
72
|
+
multiplier,
|
|
73
|
+
double,
|
|
74
|
+
valueClass,
|
|
75
|
+
isZero,
|
|
76
|
+
counterTitle,
|
|
77
|
+
increment,
|
|
78
|
+
decrement,
|
|
79
|
+
reset,
|
|
80
|
+
updateMultiple,
|
|
81
|
+
get parentTitle() { return props.parentTitle || '' },
|
|
82
|
+
template: `
|
|
83
|
+
<div class="counter" :title="counterTitle">
|
|
84
|
+
<h2>Counter</h2>
|
|
85
|
+
<p style="font-size: 0.8rem; color: #888;">Message from Home: {{ parentTitle }}</p>
|
|
86
|
+
<p class="{{ valueClass }}">Count: {{ count }} (Multiplier step: {{ multiplier }})</p>
|
|
87
|
+
<p class="derived">double: {{ double }}</p>
|
|
88
|
+
<div class="actions">
|
|
89
|
+
<button @click="decrement">−</button>
|
|
90
|
+
<button @click="reset" :disabled="isZero">reset</button>
|
|
91
|
+
<button @click="increment">+</button>
|
|
92
|
+
<button @click="updateMultiple">+5 & Step (Batched)</button>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
`
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { defineComponent, signal } from '../../framework/index.js'
|
|
2
|
+
import { Counter } from './Counter.js'
|
|
3
|
+
|
|
4
|
+
export const CounterSection = defineComponent({
|
|
5
|
+
props: {
|
|
6
|
+
pageTitle: { type: String, default: '' }
|
|
7
|
+
},
|
|
8
|
+
components: { Counter },
|
|
9
|
+
setup(props) {
|
|
10
|
+
const showCounter = signal(true)
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
showCounter,
|
|
14
|
+
get title() { return props.pageTitle },
|
|
15
|
+
toggleCounter: () => showCounter(!showCounter()),
|
|
16
|
+
template: `
|
|
17
|
+
<div style="margin-top: 30px; border-top: 1px solid #333; padding-top: 20px;">
|
|
18
|
+
<h3>Counter Section</h3>
|
|
19
|
+
<button @click="toggleCounter" class="primary-btn" style="margin: 10px 0;">Toggle Counter (f-if)</button>
|
|
20
|
+
|
|
21
|
+
<div f-if="showCounter">
|
|
22
|
+
{{ Counter initialValue="10" :parentTitle="title" }}
|
|
23
|
+
</div>
|
|
24
|
+
<div f-elif="title === 'Welcome!'">
|
|
25
|
+
<p style="color: yellow;">Counter is hidden, but title is "Welcome!"</p>
|
|
26
|
+
</div>
|
|
27
|
+
<div f-else>
|
|
28
|
+
<p style="color: red;">Counter is hidden and title is changed.</p>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
`
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
})
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { defineComponent, signal, type SignalAccessor } from '../../framework/index.js'
|
|
2
|
+
import { Counter } from './Counter.js'
|
|
3
|
+
|
|
4
|
+
type CounterSectionProps = {
|
|
5
|
+
pageTitle: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
type CounterSectionResult = {
|
|
9
|
+
showCounter: SignalAccessor<boolean>
|
|
10
|
+
title: string
|
|
11
|
+
toggleCounter: () => void
|
|
12
|
+
template: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const CounterSection = defineComponent({
|
|
16
|
+
props: {
|
|
17
|
+
pageTitle: { type: String, default: '' }
|
|
18
|
+
},
|
|
19
|
+
components: { Counter },
|
|
20
|
+
setup(props: CounterSectionProps): CounterSectionResult {
|
|
21
|
+
const showCounter = signal(true)
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
showCounter,
|
|
25
|
+
get title() { return props.pageTitle },
|
|
26
|
+
toggleCounter: () => showCounter(!showCounter()),
|
|
27
|
+
template: `
|
|
28
|
+
<div style="margin-top: 30px; border-top: 1px solid #333; padding-top: 20px;">
|
|
29
|
+
<h3>Counter Section</h3>
|
|
30
|
+
<button @click="toggleCounter" class="primary-btn" style="margin: 10px 0;">Toggle Counter (f-if)</button>
|
|
31
|
+
|
|
32
|
+
<div f-if="showCounter">
|
|
33
|
+
{{ Counter initialValue="10" :parentTitle="title" }}
|
|
34
|
+
</div>
|
|
35
|
+
<div f-elif="title === 'Welcome!'">
|
|
36
|
+
<p style="color: yellow;">Counter is hidden, but title is "Welcome!"</p>
|
|
37
|
+
</div>
|
|
38
|
+
<div f-else>
|
|
39
|
+
<p style="color: red;">Counter is hidden and title is changed.</p>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
`
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
})
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { defineComponent, signal } from '../../framework/index.js'
|
|
2
|
+
|
|
3
|
+
export const UserForm = defineComponent({
|
|
4
|
+
setup() {
|
|
5
|
+
const userName = signal('Guest')
|
|
6
|
+
const inputRef = signal(null)
|
|
7
|
+
|
|
8
|
+
return {
|
|
9
|
+
userName,
|
|
10
|
+
inputRef,
|
|
11
|
+
focusInput: () => {
|
|
12
|
+
const el = inputRef()
|
|
13
|
+
if (el) {
|
|
14
|
+
console.log('[UserForm] Focused input via f-ref:', el)
|
|
15
|
+
el.focus()
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
template: `
|
|
19
|
+
<div style="margin: 20px 0; background: #1a1a24; padding: 15px; border-radius: 8px;">
|
|
20
|
+
<h2 style="color: #fff;">Hello, {{ userName || 'Guest' }}!</h2>
|
|
21
|
+
<label style="display: block; margin-bottom: 8px; color: #8b8bff;">Two-way binding (f-model):</label>
|
|
22
|
+
<div style="display: flex; gap: 10px;">
|
|
23
|
+
<input type="text" f-ref="inputRef" f-model="userName" placeholder="Enter your name..."
|
|
24
|
+
style="padding: 8px 12px; border-radius: 4px; border: 1px solid #3a3a5a; background: #0f0f1a; color: #fff; width: 100%; max-width: 300px;" />
|
|
25
|
+
<button @click="focusInput" class="primary-btn">Focus Field</button>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
`
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
})
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { defineComponent, signal, type SignalAccessor } from '../../framework/index.js'
|
|
2
|
+
|
|
3
|
+
type UserFormResult = {
|
|
4
|
+
userName: SignalAccessor<string>
|
|
5
|
+
inputRef: SignalAccessor<HTMLInputElement | null>
|
|
6
|
+
focusInput: () => void
|
|
7
|
+
template: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const UserForm = defineComponent({
|
|
11
|
+
setup(): UserFormResult {
|
|
12
|
+
const userName = signal('Guest')
|
|
13
|
+
const inputRef = signal<HTMLInputElement | null>(null)
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
userName,
|
|
17
|
+
inputRef,
|
|
18
|
+
focusInput: () => {
|
|
19
|
+
const el = inputRef()
|
|
20
|
+
if (el) {
|
|
21
|
+
console.log('[UserForm] Focused input via f-ref:', el)
|
|
22
|
+
el.focus()
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
template: `
|
|
26
|
+
<div style="margin: 20px 0; background: #1a1a24; padding: 15px; border-radius: 8px;">
|
|
27
|
+
<h2 style="color: #fff;">Hello, {{ userName || 'Guest' }}!</h2>
|
|
28
|
+
<label style="display: block; margin-bottom: 8px; color: #8b8bff;">Two-way binding (f-model):</label>
|
|
29
|
+
<div style="display: flex; gap: 10px;">
|
|
30
|
+
<input type="text" f-ref="inputRef" f-model="userName" placeholder="Enter your name..."
|
|
31
|
+
style="padding: 8px 12px; border-radius: 4px; border: 1px solid #3a3a5a; background: #0f0f1a; color: #fff; width: 100%; max-width: 300px;" />
|
|
32
|
+
<button @click="focusInput" class="primary-btn">Focus Field</button>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
`
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
})
|
package/app/main.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { createRouter, mountOutlet } from '../framework/index.js'
|
|
2
2
|
import { Home } from './routes/Home.js'
|
|
3
3
|
import { About } from './routes/About.js'
|
|
4
|
+
import { TestFor } from './routes/TestFor.js'
|
|
4
5
|
|
|
5
6
|
createRouter([
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
{ path: '/', component: Home },
|
|
8
|
+
{ path: '/about', component: About },
|
|
9
|
+
{ path: '/test-for', component: TestFor },
|
|
8
10
|
])
|
|
9
11
|
|
|
10
12
|
const outlet = document.getElementById('app')
|
package/app/main.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createRouter, mountOutlet } from '../framework/index.js'
|
|
2
|
+
import type { Route } from '../framework/types/index.js'
|
|
3
|
+
import { Home } from './routes/Home.js'
|
|
4
|
+
import { About } from './routes/About.js'
|
|
5
|
+
|
|
6
|
+
const routes: Route[] = [
|
|
7
|
+
{ path: '/', component: Home },
|
|
8
|
+
{ path: '/about', component: About },
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
createRouter(routes)
|
|
12
|
+
|
|
13
|
+
const outlet = document.getElementById('app')
|
|
14
|
+
if (!outlet) throw new Error('[app] #app element not found in DOM')
|
|
15
|
+
mountOutlet(outlet)
|
package/app/routes/About.js
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
import { defineComponent, signal, onMount } from '../../framework/index.js'
|
|
2
2
|
|
|
3
3
|
export const About = defineComponent({
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
props: [],
|
|
5
|
+
setup() {
|
|
6
|
+
const loaded = signal(false)
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
onMount(() => {
|
|
9
|
+
setTimeout(() => loaded(true), 300)
|
|
10
|
+
})
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
12
|
+
return {
|
|
13
|
+
loaded,
|
|
14
|
+
template: `
|
|
15
|
+
<div class="page">
|
|
16
|
+
<h1>About</h1>
|
|
17
|
+
<p>Framework custom — SPA prototipe</p>
|
|
18
|
+
<p><strong>Features:</strong> signals, defineComponent, hash router, lifecycle hooks.</p>
|
|
19
|
+
<p class="status">{{ loaded }}</p>
|
|
20
|
+
</div>
|
|
21
|
+
`
|
|
22
|
+
}
|
|
22
23
|
}
|
|
23
|
-
}
|
|
24
24
|
})
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { defineComponent, signal, onMount, type Signal, type EmptyProps } from '../../framework/index.js'
|
|
2
|
+
|
|
3
|
+
type AboutResult = {
|
|
4
|
+
loaded: Signal<boolean>
|
|
5
|
+
template: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const About = defineComponent({
|
|
9
|
+
props: [],
|
|
10
|
+
setup(): AboutResult {
|
|
11
|
+
const loaded = signal<boolean>(false)
|
|
12
|
+
|
|
13
|
+
onMount(() => {
|
|
14
|
+
setTimeout(() => loaded(true), 300)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
loaded,
|
|
19
|
+
template: `
|
|
20
|
+
<div class="page">
|
|
21
|
+
<h1>About</h1>
|
|
22
|
+
<p>Framework custom — prototip SPA.</p>
|
|
23
|
+
<p><strong>Features:</strong> signals, defineComponent, hash router, lifecycle hooks.</p>
|
|
24
|
+
<p class="status">{{ loaded }}</p>
|
|
25
|
+
</div>
|
|
26
|
+
`
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
})
|
package/app/routes/Home.js
CHANGED
|
@@ -1,22 +1,27 @@
|
|
|
1
1
|
import { defineComponent, signal } from '../../framework/index.js'
|
|
2
|
-
import {
|
|
2
|
+
import { UserForm } from '../components/UserForm.js'
|
|
3
|
+
import { CounterSection } from '../components/CounterSection.js'
|
|
3
4
|
|
|
4
5
|
export const Home = defineComponent({
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
6
|
+
components: { UserForm, CounterSection },
|
|
7
|
+
setup() {
|
|
8
|
+
const title = signal('Welcome!')
|
|
9
|
+
return {
|
|
10
|
+
title,
|
|
11
|
+
changeTitle: () => title('Changed Title!'),
|
|
12
|
+
template: `
|
|
13
|
+
<div class="page">
|
|
14
|
+
<h1>{{ title.toUpperCase() }}</h1>
|
|
15
|
+
<p>This is an SPA built with my own JavaScript custom framework!</p>
|
|
16
|
+
<p>Made with passion by Sebastian Șomu</p>
|
|
17
|
+
|
|
18
|
+
<button @click="changeTitle" class="primary-btn" style="margin-bottom: 20px;">Change App Title</button>
|
|
19
|
+
|
|
20
|
+
{{ UserForm }}
|
|
21
|
+
|
|
22
|
+
{{ CounterSection :pageTitle="title" }}
|
|
23
|
+
</div>
|
|
24
|
+
`
|
|
25
|
+
}
|
|
20
26
|
}
|
|
21
|
-
}
|
|
22
27
|
})
|