@ersbeth/picoflow 0.2.3 → 1.0.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/.cursor/plans/update-js-e795d61b.plan.md +567 -0
- package/.gitlab-ci.yml +24 -0
- package/.vscode/settings.json +3 -3
- package/CHANGELOG.md +51 -0
- package/IMPLEMENTATION_GUIDE.md +1578 -0
- package/README.md +62 -25
- package/biome.json +32 -32
- package/dist/picoflow.js +557 -1099
- package/dist/types/advanced/array.d.ts +0 -6
- package/dist/types/advanced/array.d.ts.map +1 -1
- package/dist/types/advanced/index.d.ts +5 -5
- package/dist/types/advanced/index.d.ts.map +1 -1
- package/dist/types/advanced/map.d.ts +114 -23
- package/dist/types/advanced/map.d.ts.map +1 -1
- package/dist/types/advanced/resource.d.ts +51 -12
- package/dist/types/advanced/resource.d.ts.map +1 -1
- package/dist/types/advanced/resourceAsync.d.ts +28 -13
- package/dist/types/advanced/resourceAsync.d.ts.map +1 -1
- package/dist/types/advanced/stream.d.ts +74 -16
- package/dist/types/advanced/stream.d.ts.map +1 -1
- package/dist/types/advanced/streamAsync.d.ts +69 -15
- package/dist/types/advanced/streamAsync.d.ts.map +1 -1
- package/dist/types/basic/constant.d.ts +44 -16
- package/dist/types/basic/constant.d.ts.map +1 -1
- package/dist/types/basic/derivation.d.ts +73 -24
- package/dist/types/basic/derivation.d.ts.map +1 -1
- package/dist/types/basic/disposable.d.ts +65 -6
- package/dist/types/basic/disposable.d.ts.map +1 -1
- package/dist/types/basic/effect.d.ts +27 -16
- package/dist/types/basic/effect.d.ts.map +1 -1
- package/dist/types/basic/index.d.ts +7 -8
- package/dist/types/basic/index.d.ts.map +1 -1
- package/dist/types/basic/observable.d.ts +62 -13
- package/dist/types/basic/observable.d.ts.map +1 -1
- package/dist/types/basic/signal.d.ts +35 -6
- package/dist/types/basic/signal.d.ts.map +1 -1
- package/dist/types/basic/state.d.ts +25 -4
- package/dist/types/basic/state.d.ts.map +1 -1
- package/dist/types/basic/trackingContext.d.ts +33 -0
- package/dist/types/basic/trackingContext.d.ts.map +1 -0
- package/dist/types/creators.d.ts +271 -26
- package/dist/types/creators.d.ts.map +1 -1
- package/dist/types/index.d.ts +60 -7
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/solid/converters.d.ts +5 -5
- package/dist/types/solid/converters.d.ts.map +1 -1
- package/dist/types/solid/index.d.ts +2 -2
- package/dist/types/solid/index.d.ts.map +1 -1
- package/dist/types/solid/primitives.d.ts +96 -4
- package/dist/types/solid/primitives.d.ts.map +1 -1
- package/docs/.vitepress/config.mts +110 -0
- package/docs/api/classes/FlowArray.md +489 -0
- package/docs/api/classes/FlowConstant.md +350 -0
- package/docs/api/classes/FlowDerivation.md +334 -0
- package/docs/api/classes/FlowEffect.md +100 -0
- package/docs/api/classes/FlowMap.md +512 -0
- package/docs/api/classes/FlowObservable.md +306 -0
- package/docs/api/classes/FlowResource.md +380 -0
- package/docs/api/classes/FlowResourceAsync.md +362 -0
- package/docs/api/classes/FlowSignal.md +160 -0
- package/docs/api/classes/FlowState.md +368 -0
- package/docs/api/classes/FlowStream.md +367 -0
- package/docs/api/classes/FlowStreamAsync.md +364 -0
- package/docs/api/classes/SolidDerivation.md +75 -0
- package/docs/api/classes/SolidResource.md +91 -0
- package/docs/api/classes/SolidState.md +71 -0
- package/docs/api/classes/TrackingContext.md +33 -0
- package/docs/api/functions/array.md +58 -0
- package/docs/api/functions/constant.md +45 -0
- package/docs/api/functions/derivation.md +53 -0
- package/docs/api/functions/effect.md +49 -0
- package/docs/api/functions/from.md +220 -0
- package/docs/api/functions/isDisposable.md +49 -0
- package/docs/api/functions/map.md +57 -0
- package/docs/api/functions/resource.md +52 -0
- package/docs/api/functions/resourceAsync.md +50 -0
- package/docs/api/functions/signal.md +36 -0
- package/docs/api/functions/state.md +47 -0
- package/docs/api/functions/stream.md +53 -0
- package/docs/api/functions/streamAsync.md +50 -0
- package/docs/api/index.md +118 -0
- package/docs/api/interfaces/FlowDisposable.md +65 -0
- package/docs/api/interfaces/SolidObservable.md +19 -0
- package/docs/api/type-aliases/FlowArrayAction.md +49 -0
- package/docs/api/type-aliases/FlowStreamDisposer.md +15 -0
- package/docs/api/type-aliases/FlowStreamSetter.md +27 -0
- package/docs/api/type-aliases/FlowStreamUpdater.md +32 -0
- package/docs/api/type-aliases/NotPromise.md +18 -0
- package/docs/api/type-aliases/SolidGetter.md +17 -0
- package/docs/api/typedoc-sidebar.json +1 -0
- package/docs/examples/examples.md +2313 -0
- package/docs/examples/patterns.md +649 -0
- package/docs/guide/advanced/disposal.md +426 -0
- package/docs/guide/advanced/solidjs.md +221 -0
- package/docs/guide/advanced/upgrading.md +464 -0
- package/docs/guide/introduction/concepts.md +56 -0
- package/docs/guide/introduction/conventions.md +61 -0
- package/docs/guide/introduction/getting-started.md +134 -0
- package/docs/guide/introduction/lifecycle.md +371 -0
- package/docs/guide/primitives/array.md +400 -0
- package/docs/guide/primitives/constant.md +380 -0
- package/docs/guide/primitives/derivations.md +348 -0
- package/docs/guide/primitives/effects.md +458 -0
- package/docs/guide/primitives/map.md +387 -0
- package/docs/guide/primitives/overview.md +175 -0
- package/docs/guide/primitives/resources.md +858 -0
- package/docs/guide/primitives/signal.md +259 -0
- package/docs/guide/primitives/state.md +368 -0
- package/docs/guide/primitives/streams.md +931 -0
- package/docs/index.md +47 -0
- package/docs/public/logo.svg +1 -0
- package/package.json +57 -41
- package/src/advanced/array.ts +208 -210
- package/src/advanced/index.ts +7 -7
- package/src/advanced/map.ts +178 -68
- package/src/advanced/resource.ts +87 -43
- package/src/advanced/resourceAsync.ts +62 -42
- package/src/advanced/stream.ts +113 -50
- package/src/advanced/streamAsync.ts +120 -61
- package/src/basic/constant.ts +82 -49
- package/src/basic/derivation.ts +128 -84
- package/src/basic/disposable.ts +74 -15
- package/src/basic/effect.ts +85 -77
- package/src/basic/index.ts +7 -8
- package/src/basic/observable.ts +94 -36
- package/src/basic/signal.ts +133 -105
- package/src/basic/state.ts +46 -25
- package/src/basic/trackingContext.ts +45 -0
- package/src/creators.ts +297 -54
- package/src/index.ts +96 -43
- package/src/solid/converters.ts +186 -67
- package/src/solid/index.ts +8 -2
- package/src/solid/primitives.ts +167 -65
- package/test/array.test.ts +592 -612
- package/test/constant.test.ts +31 -33
- package/test/derivation.test.ts +531 -536
- package/test/effect.test.ts +21 -21
- package/test/map.test.ts +233 -137
- package/test/resource.test.ts +119 -121
- package/test/resourceAsync.test.ts +98 -100
- package/test/signal.test.ts +51 -55
- package/test/state.test.ts +186 -168
- package/test/stream.test.ts +189 -189
- package/test/streamAsync.test.ts +186 -186
- package/tsconfig.json +19 -18
- package/typedoc.json +37 -0
- package/vite.config.ts +23 -20
- package/vitest.config.ts +7 -7
- package/api/doc/index.md +0 -31
- package/api/doc/picoflow.array.md +0 -55
- package/api/doc/picoflow.constant.md +0 -55
- package/api/doc/picoflow.derivation.md +0 -55
- package/api/doc/picoflow.effect.md +0 -55
- package/api/doc/picoflow.flowarray._constructor_.md +0 -49
- package/api/doc/picoflow.flowarray._lastaction.md +0 -13
- package/api/doc/picoflow.flowarray.clear.md +0 -17
- package/api/doc/picoflow.flowarray.dispose.md +0 -55
- package/api/doc/picoflow.flowarray.get.md +0 -19
- package/api/doc/picoflow.flowarray.length.md +0 -13
- package/api/doc/picoflow.flowarray.md +0 -273
- package/api/doc/picoflow.flowarray.pop.md +0 -17
- package/api/doc/picoflow.flowarray.push.md +0 -53
- package/api/doc/picoflow.flowarray.set.md +0 -53
- package/api/doc/picoflow.flowarray.setitem.md +0 -69
- package/api/doc/picoflow.flowarray.shift.md +0 -17
- package/api/doc/picoflow.flowarray.splice.md +0 -85
- package/api/doc/picoflow.flowarray.unshift.md +0 -53
- package/api/doc/picoflow.flowarrayaction.md +0 -37
- package/api/doc/picoflow.flowconstant._constructor_.md +0 -49
- package/api/doc/picoflow.flowconstant.get.md +0 -25
- package/api/doc/picoflow.flowconstant.md +0 -88
- package/api/doc/picoflow.flowderivation._constructor_.md +0 -49
- package/api/doc/picoflow.flowderivation.get.md +0 -23
- package/api/doc/picoflow.flowderivation.md +0 -86
- package/api/doc/picoflow.flowdisposable.dispose.md +0 -55
- package/api/doc/picoflow.flowdisposable.md +0 -43
- package/api/doc/picoflow.floweffect._constructor_.md +0 -54
- package/api/doc/picoflow.floweffect.dispose.md +0 -21
- package/api/doc/picoflow.floweffect.disposed.md +0 -13
- package/api/doc/picoflow.floweffect.md +0 -131
- package/api/doc/picoflow.flowgetter.md +0 -15
- package/api/doc/picoflow.flowmap._lastdeleted.md +0 -21
- package/api/doc/picoflow.flowmap._lastset.md +0 -21
- package/api/doc/picoflow.flowmap.delete.md +0 -61
- package/api/doc/picoflow.flowmap.md +0 -133
- package/api/doc/picoflow.flowmap.setat.md +0 -77
- package/api/doc/picoflow.flowobservable.get.md +0 -19
- package/api/doc/picoflow.flowobservable.md +0 -68
- package/api/doc/picoflow.flowobservable.subscribe.md +0 -55
- package/api/doc/picoflow.flowresource._constructor_.md +0 -49
- package/api/doc/picoflow.flowresource.fetch.md +0 -27
- package/api/doc/picoflow.flowresource.get.md +0 -23
- package/api/doc/picoflow.flowresource.md +0 -100
- package/api/doc/picoflow.flowresourceasync._constructor_.md +0 -49
- package/api/doc/picoflow.flowresourceasync.fetch.md +0 -27
- package/api/doc/picoflow.flowresourceasync.get.md +0 -23
- package/api/doc/picoflow.flowresourceasync.md +0 -100
- package/api/doc/picoflow.flowsignal.dispose.md +0 -59
- package/api/doc/picoflow.flowsignal.disposed.md +0 -18
- package/api/doc/picoflow.flowsignal.md +0 -112
- package/api/doc/picoflow.flowsignal.trigger.md +0 -21
- package/api/doc/picoflow.flowstate.md +0 -52
- package/api/doc/picoflow.flowstate.set.md +0 -61
- package/api/doc/picoflow.flowstream._constructor_.md +0 -49
- package/api/doc/picoflow.flowstream.dispose.md +0 -21
- package/api/doc/picoflow.flowstream.get.md +0 -23
- package/api/doc/picoflow.flowstream.md +0 -100
- package/api/doc/picoflow.flowstreamasync._constructor_.md +0 -54
- package/api/doc/picoflow.flowstreamasync.dispose.md +0 -21
- package/api/doc/picoflow.flowstreamasync.get.md +0 -23
- package/api/doc/picoflow.flowstreamasync.md +0 -100
- package/api/doc/picoflow.flowstreamdisposer.md +0 -13
- package/api/doc/picoflow.flowstreamsetter.md +0 -13
- package/api/doc/picoflow.flowstreamupdater.md +0 -19
- package/api/doc/picoflow.flowwatcher.md +0 -15
- package/api/doc/picoflow.from.md +0 -55
- package/api/doc/picoflow.from_1.md +0 -55
- package/api/doc/picoflow.from_2.md +0 -55
- package/api/doc/picoflow.from_3.md +0 -55
- package/api/doc/picoflow.from_4.md +0 -55
- package/api/doc/picoflow.from_5.md +0 -55
- package/api/doc/picoflow.isdisposable.md +0 -55
- package/api/doc/picoflow.map.md +0 -59
- package/api/doc/picoflow.md +0 -544
- package/api/doc/picoflow.resource.md +0 -55
- package/api/doc/picoflow.resourceasync.md +0 -55
- package/api/doc/picoflow.signal.md +0 -19
- package/api/doc/picoflow.solidderivation._constructor_.md +0 -49
- package/api/doc/picoflow.solidderivation.get.md +0 -13
- package/api/doc/picoflow.solidderivation.md +0 -94
- package/api/doc/picoflow.solidgetter.md +0 -13
- package/api/doc/picoflow.solidobservable.get.md +0 -13
- package/api/doc/picoflow.solidobservable.md +0 -57
- package/api/doc/picoflow.solidresource._constructor_.md +0 -49
- package/api/doc/picoflow.solidresource.get.md +0 -13
- package/api/doc/picoflow.solidresource.latest.md +0 -13
- package/api/doc/picoflow.solidresource.md +0 -157
- package/api/doc/picoflow.solidresource.refetch.md +0 -13
- package/api/doc/picoflow.solidresource.state.md +0 -13
- package/api/doc/picoflow.solidstate._constructor_.md +0 -49
- package/api/doc/picoflow.solidstate.get.md +0 -13
- package/api/doc/picoflow.solidstate.md +0 -115
- package/api/doc/picoflow.solidstate.set.md +0 -13
- package/api/doc/picoflow.state.md +0 -55
- package/api/doc/picoflow.stream.md +0 -55
- package/api/doc/picoflow.streamasync.md +0 -55
- package/api/picoflow.public.api.md +0 -244
- package/api-extractor.json +0 -61
|
@@ -0,0 +1,2313 @@
|
|
|
1
|
+
# Examples
|
|
2
|
+
|
|
3
|
+
This page contains complete, real-world examples demonstrating PicoFlow's features working together. Each example is runnable and shows best practices.
|
|
4
|
+
|
|
5
|
+
## Example 1: Todo List Application
|
|
6
|
+
|
|
7
|
+
A complete todo list with filtering, persistence, and statistics.
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import { state, array, derivation, effect } from '@ersbeth/picoflow'
|
|
11
|
+
|
|
12
|
+
// Types
|
|
13
|
+
interface Todo {
|
|
14
|
+
id: number
|
|
15
|
+
text: string
|
|
16
|
+
completed: boolean
|
|
17
|
+
createdAt: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type Filter = 'all' | 'active' | 'completed'
|
|
21
|
+
|
|
22
|
+
// State
|
|
23
|
+
const $todos = array<Todo>([])
|
|
24
|
+
const $filter = state<Filter>('all')
|
|
25
|
+
const $newTodoText = state('')
|
|
26
|
+
|
|
27
|
+
// Derived values
|
|
28
|
+
const $filteredTodos = derivation((t) => {
|
|
29
|
+
const todos = $todos.get(t)
|
|
30
|
+
const filter = $filter.get(t)
|
|
31
|
+
|
|
32
|
+
switch (filter) {
|
|
33
|
+
case 'active':
|
|
34
|
+
return todos.filter(todo => !todo.completed)
|
|
35
|
+
case 'completed':
|
|
36
|
+
return todos.filter(todo => todo.completed)
|
|
37
|
+
default:
|
|
38
|
+
return todos
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
const $activeCount = derivation((t) => {
|
|
43
|
+
return $todos.get(t).filter(todo => !todo.completed).length
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
const $completedCount = derivation((t) => {
|
|
47
|
+
return $todos.get(t).filter(todo => todo.completed).length
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
const $totalCount = derivation((t) => {
|
|
51
|
+
return $todos.get(t).length
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
// Persistence
|
|
55
|
+
effect((t) => {
|
|
56
|
+
const todos = $todos.get(t)
|
|
57
|
+
localStorage.setItem('todos', JSON.stringify(todos))
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
// Load from storage on init
|
|
61
|
+
function loadTodos() {
|
|
62
|
+
const stored = localStorage.getItem('todos')
|
|
63
|
+
if (stored) {
|
|
64
|
+
const todos = JSON.parse(stored)
|
|
65
|
+
todos.forEach((todo: Todo) => $todos.push(todo))
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Operations
|
|
70
|
+
function addTodo() {
|
|
71
|
+
const text = $newTodoText.pick().trim()
|
|
72
|
+
if (!text) return
|
|
73
|
+
|
|
74
|
+
$todos.push({
|
|
75
|
+
id: Date.now(),
|
|
76
|
+
text,
|
|
77
|
+
completed: false,
|
|
78
|
+
createdAt: Date.now()
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
$newTodoText.set('')
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function removeTodo(id: number) {
|
|
85
|
+
const todos = $todos.pick()
|
|
86
|
+
const index = todos.findIndex(t => t.id === id)
|
|
87
|
+
if (index !== -1) {
|
|
88
|
+
$todos.splice(index, 1)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function toggleTodo(id: number) {
|
|
93
|
+
const todos = $todos.pick()
|
|
94
|
+
const index = todos.findIndex(t => t.id === id)
|
|
95
|
+
if (index !== -1) {
|
|
96
|
+
const todo = todos[index]
|
|
97
|
+
$todos.splice(index, 1, {
|
|
98
|
+
...todo,
|
|
99
|
+
completed: !todo.completed
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function clearCompleted() {
|
|
105
|
+
const todos = $todos.pick()
|
|
106
|
+
const activeTodos = todos.filter(t => !t.completed)
|
|
107
|
+
$todos.clear()
|
|
108
|
+
activeTodos.forEach(todo => $todos.push(todo))
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// UI Updates
|
|
112
|
+
effect((t) => {
|
|
113
|
+
const filtered = $filteredTodos.get(t)
|
|
114
|
+
renderTodoList(filtered)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
effect((t) => {
|
|
118
|
+
const active = $activeCount.get(t)
|
|
119
|
+
const completed = $completedCount.get(t)
|
|
120
|
+
const total = $totalCount.get(t)
|
|
121
|
+
|
|
122
|
+
renderStats({
|
|
123
|
+
active,
|
|
124
|
+
completed,
|
|
125
|
+
total
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
// Initialize
|
|
130
|
+
loadTodos()
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Data Flow Diagram
|
|
134
|
+
|
|
135
|
+
```mermaid
|
|
136
|
+
flowchart TD
|
|
137
|
+
A[User Input] --> B[$newTodoText]
|
|
138
|
+
B --> C[addTodo]
|
|
139
|
+
C --> D[$todos array]
|
|
140
|
+
D --> E[$filteredTodos]
|
|
141
|
+
D --> F[$activeCount]
|
|
142
|
+
D --> G[$completedCount]
|
|
143
|
+
E --> H[Render Todo List]
|
|
144
|
+
F --> I[Render Stats]
|
|
145
|
+
G --> I
|
|
146
|
+
D --> J[localStorage Effect]
|
|
147
|
+
|
|
148
|
+
K[$filter state] --> E
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Example 2: Form with Validation
|
|
154
|
+
|
|
155
|
+
A registration form with real-time validation and submission handling.
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
import { state, derivation, effect } from '@ersbeth/picoflow'
|
|
159
|
+
|
|
160
|
+
// Form state
|
|
161
|
+
const $email = state('')
|
|
162
|
+
const $password = state('')
|
|
163
|
+
const $confirmPassword = state('')
|
|
164
|
+
const $agreedToTerms = state(false)
|
|
165
|
+
|
|
166
|
+
// Validation derivations
|
|
167
|
+
const $emailError = derivation((t) => {
|
|
168
|
+
const email = $email.get(t)
|
|
169
|
+
if (!email) return 'Email is required'
|
|
170
|
+
if (!email.includes('@')) return 'Email must contain @'
|
|
171
|
+
if (email.length < 5) return 'Email is too short'
|
|
172
|
+
return null
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
const $passwordError = derivation((t) => {
|
|
176
|
+
const password = $password.get(t)
|
|
177
|
+
if (!password) return 'Password is required'
|
|
178
|
+
if (password.length < 8) return 'Password must be at least 8 characters'
|
|
179
|
+
if (!/[A-Z]/.test(password)) return 'Password must contain uppercase letter'
|
|
180
|
+
if (!/[0-9]/.test(password)) return 'Password must contain a number'
|
|
181
|
+
return null
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
const $confirmError = derivation((t) => {
|
|
185
|
+
const password = $password.get(t)
|
|
186
|
+
const confirm = $confirmPassword.get(t)
|
|
187
|
+
if (!confirm) return 'Please confirm your password'
|
|
188
|
+
if (password !== confirm) return 'Passwords must match'
|
|
189
|
+
return null
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
const $termsError = derivation((t) => {
|
|
193
|
+
const agreed = $agreedToTerms.get(t)
|
|
194
|
+
return agreed ? null : 'You must agree to the terms'
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
// Form validity
|
|
198
|
+
const $isValid = derivation((t) => {
|
|
199
|
+
return !$emailError.get(t) &&
|
|
200
|
+
!$passwordError.get(t) &&
|
|
201
|
+
!$confirmError.get(t) &&
|
|
202
|
+
!$termsError.get(t)
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
// Submission state
|
|
206
|
+
const $submitting = state(false)
|
|
207
|
+
const $submitError = state<string | null>(null)
|
|
208
|
+
const $submitted = state(false)
|
|
209
|
+
|
|
210
|
+
// Update UI with validation errors
|
|
211
|
+
effect((t) => {
|
|
212
|
+
const emailError = $emailError.get(t)
|
|
213
|
+
const passwordError = $passwordError.get(t)
|
|
214
|
+
const confirmError = $confirmError.get(t)
|
|
215
|
+
const termsError = $termsError.get(t)
|
|
216
|
+
|
|
217
|
+
updateValidationUI({
|
|
218
|
+
email: emailError,
|
|
219
|
+
password: passwordError,
|
|
220
|
+
confirm: confirmError,
|
|
221
|
+
terms: termsError
|
|
222
|
+
})
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
// Enable/disable submit button
|
|
226
|
+
effect((t) => {
|
|
227
|
+
const isValid = $isValid.get(t)
|
|
228
|
+
const submitting = $submitting.get(t)
|
|
229
|
+
|
|
230
|
+
const submitButton = document.getElementById('submit') as HTMLButtonElement
|
|
231
|
+
submitButton.disabled = !isValid || submitting
|
|
232
|
+
submitButton.textContent = submitting ? 'Submitting...' : 'Submit'
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
// Handle submission
|
|
236
|
+
async function handleSubmit() {
|
|
237
|
+
if (!$isValid.pick()) return
|
|
238
|
+
|
|
239
|
+
$submitting.set(true)
|
|
240
|
+
$submitError.set(null)
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
const response = await fetch('/api/register', {
|
|
244
|
+
method: 'POST',
|
|
245
|
+
headers: { 'Content-Type': 'application/json' },
|
|
246
|
+
body: JSON.stringify({
|
|
247
|
+
email: $email.pick(),
|
|
248
|
+
password: $password.pick()
|
|
249
|
+
})
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
if (!response.ok) {
|
|
253
|
+
throw new Error('Registration failed')
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
$submitted.set(true)
|
|
257
|
+
} catch (error) {
|
|
258
|
+
$submitError.set(error.message)
|
|
259
|
+
} finally {
|
|
260
|
+
$submitting.set(false)
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Show success message
|
|
265
|
+
effect((t) => {
|
|
266
|
+
const submitted = $submitted.get(t)
|
|
267
|
+
if (submitted) {
|
|
268
|
+
showSuccessMessage('Registration successful!')
|
|
269
|
+
}
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
// Show submission errors
|
|
273
|
+
effect((t) => {
|
|
274
|
+
const error = $submitError.get(t)
|
|
275
|
+
if (error) {
|
|
276
|
+
showErrorMessage(error)
|
|
277
|
+
}
|
|
278
|
+
})
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### Validation Flow
|
|
282
|
+
|
|
283
|
+
```mermaid
|
|
284
|
+
flowchart TD
|
|
285
|
+
A[$email] --> B[$emailError]
|
|
286
|
+
C[$password] --> D[$passwordError]
|
|
287
|
+
E[$confirmPassword] --> F[$confirmError]
|
|
288
|
+
G[$agreedToTerms] --> H[$termsError]
|
|
289
|
+
|
|
290
|
+
B --> I[$isValid]
|
|
291
|
+
D --> I
|
|
292
|
+
F --> I
|
|
293
|
+
H --> I
|
|
294
|
+
|
|
295
|
+
I --> J[Submit Button State]
|
|
296
|
+
J --> K{Valid?}
|
|
297
|
+
K -->|Yes| L[Enable Submit]
|
|
298
|
+
K -->|No| M[Disable Submit]
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
## Example 3: Real-time Data Dashboard
|
|
304
|
+
|
|
305
|
+
A dashboard displaying live data from a WebSocket connection with statistics.
|
|
306
|
+
|
|
307
|
+
```typescript
|
|
308
|
+
import { stream, state, derivation, effect } from '@ersbeth/picoflow'
|
|
309
|
+
|
|
310
|
+
// Types
|
|
311
|
+
interface DataPoint {
|
|
312
|
+
timestamp: number
|
|
313
|
+
value: number
|
|
314
|
+
source: string
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
interface Stats {
|
|
318
|
+
min: number
|
|
319
|
+
max: number
|
|
320
|
+
avg: number
|
|
321
|
+
count: number
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// WebSocket stream
|
|
325
|
+
const $liveData = stream<DataPoint>((set) => {
|
|
326
|
+
const ws = new WebSocket('wss://data.example.com')
|
|
327
|
+
|
|
328
|
+
ws.onopen = () => {
|
|
329
|
+
console.log('Connected to data stream')
|
|
330
|
+
$connectionStatus.set('connected')
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
ws.onmessage = (event) => {
|
|
334
|
+
const data = JSON.parse(event.data)
|
|
335
|
+
set(data)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
ws.onerror = (error) => {
|
|
339
|
+
console.error('WebSocket error:', error)
|
|
340
|
+
$connectionStatus.set('error')
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
ws.onclose = () => {
|
|
344
|
+
console.log('Disconnected from data stream')
|
|
345
|
+
$connectionStatus.set('disconnected')
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return () => ws.close()
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
// State
|
|
352
|
+
const $connectionStatus = state<'connected' | 'disconnected' | 'error'>('disconnected')
|
|
353
|
+
const $dataHistory = state<DataPoint[]>([])
|
|
354
|
+
const $maxHistorySize = state(100)
|
|
355
|
+
|
|
356
|
+
// Add new data to history
|
|
357
|
+
effect((t) => {
|
|
358
|
+
const newData = $liveData.get(t)
|
|
359
|
+
if (!newData) return
|
|
360
|
+
|
|
361
|
+
const history = $dataHistory.pick()
|
|
362
|
+
const maxSize = $maxHistorySize.pick()
|
|
363
|
+
|
|
364
|
+
const updated = [...history, newData]
|
|
365
|
+
if (updated.length > maxSize) {
|
|
366
|
+
updated.shift() // Remove oldest
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
$dataHistory.set(updated)
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
// Derived statistics
|
|
373
|
+
const $stats = derivation<Stats>((t) => {
|
|
374
|
+
const history = $dataHistory.get(t)
|
|
375
|
+
if (history.length === 0) {
|
|
376
|
+
return { min: 0, max: 0, avg: 0, count: 0 }
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const values = history.map(d => d.value)
|
|
380
|
+
const sum = values.reduce((a, b) => a + b, 0)
|
|
381
|
+
|
|
382
|
+
return {
|
|
383
|
+
min: Math.min(...values),
|
|
384
|
+
max: Math.max(...values),
|
|
385
|
+
avg: sum / values.length,
|
|
386
|
+
count: history.length
|
|
387
|
+
}
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
// Recent data (last 10 points)
|
|
391
|
+
const $recentData = derivation((t) => {
|
|
392
|
+
const history = $dataHistory.get(t)
|
|
393
|
+
return history.slice(-10)
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
// Data by source
|
|
397
|
+
const $dataBySource = derivation((t) => {
|
|
398
|
+
const history = $dataHistory.get(t)
|
|
399
|
+
const bySource: Record<string, DataPoint[]> = {}
|
|
400
|
+
|
|
401
|
+
for (const point of history) {
|
|
402
|
+
if (!bySource[point.source]) {
|
|
403
|
+
bySource[point.source] = []
|
|
404
|
+
}
|
|
405
|
+
bySource[point.source].push(point)
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return bySource
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
// UI Updates
|
|
412
|
+
effect((t) => {
|
|
413
|
+
const status = $connectionStatus.get(t)
|
|
414
|
+
updateConnectionIndicator(status)
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
effect((t) => {
|
|
418
|
+
const stats = $stats.get(t)
|
|
419
|
+
renderStatsDisplay(stats)
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
effect((t) => {
|
|
423
|
+
const recent = $recentData.get(t)
|
|
424
|
+
renderRecentDataTable(recent)
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
effect((t) => {
|
|
428
|
+
const history = $dataHistory.get(t)
|
|
429
|
+
renderChart(history)
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
// Alert on high values
|
|
433
|
+
effect((t) => {
|
|
434
|
+
const newData = $liveData.get(t)
|
|
435
|
+
if (newData && newData.value > 100) {
|
|
436
|
+
showAlert(`High value detected: ${newData.value}`)
|
|
437
|
+
}
|
|
438
|
+
})
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
### Dashboard Data Flow
|
|
442
|
+
|
|
443
|
+
```mermaid
|
|
444
|
+
flowchart TD
|
|
445
|
+
A[WebSocket] -->|pushes| B[$liveData stream]
|
|
446
|
+
B --> C[Effect: Add to history]
|
|
447
|
+
C --> D[$dataHistory]
|
|
448
|
+
D --> E[$stats derivation]
|
|
449
|
+
D --> F[$recentData derivation]
|
|
450
|
+
D --> G[$dataBySource derivation]
|
|
451
|
+
|
|
452
|
+
E --> H[Stats Display]
|
|
453
|
+
F --> I[Recent Data Table]
|
|
454
|
+
D --> J[Chart]
|
|
455
|
+
G --> K[Source Breakdown]
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
---
|
|
459
|
+
|
|
460
|
+
## Example 4: Shopping Cart
|
|
461
|
+
|
|
462
|
+
A shopping cart with items, quantities, prices, and calculated totals.
|
|
463
|
+
|
|
464
|
+
```typescript
|
|
465
|
+
import { map, derivation, effect, state } from '@ersbeth/picoflow'
|
|
466
|
+
|
|
467
|
+
// Types
|
|
468
|
+
interface Product {
|
|
469
|
+
id: string
|
|
470
|
+
name: string
|
|
471
|
+
price: number
|
|
472
|
+
image: string
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
interface CartItem {
|
|
476
|
+
product: Product
|
|
477
|
+
quantity: number
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Cart state
|
|
481
|
+
const $cart = map<string, CartItem>()
|
|
482
|
+
const $taxRate = state(0.08) // 8%
|
|
483
|
+
const $shippingThreshold = state(50) // Free shipping over $50
|
|
484
|
+
|
|
485
|
+
// Derived values
|
|
486
|
+
const $subtotal = derivation((t) => {
|
|
487
|
+
const cart = $cart.get(t)
|
|
488
|
+
return Object.values(cart).reduce(
|
|
489
|
+
(sum, item) => sum + item.product.price * item.quantity,
|
|
490
|
+
0
|
|
491
|
+
)
|
|
492
|
+
})
|
|
493
|
+
|
|
494
|
+
const $tax = derivation((t) => {
|
|
495
|
+
const subtotal = $subtotal.get(t)
|
|
496
|
+
const taxRate = $taxRate.get(t)
|
|
497
|
+
return subtotal * taxRate
|
|
498
|
+
})
|
|
499
|
+
|
|
500
|
+
const $shipping = derivation((t) => {
|
|
501
|
+
const subtotal = $subtotal.get(t)
|
|
502
|
+
const threshold = $shippingThreshold.get(t)
|
|
503
|
+
return subtotal >= threshold ? 0 : 5.99
|
|
504
|
+
})
|
|
505
|
+
|
|
506
|
+
const $total = derivation((t) => {
|
|
507
|
+
return $subtotal.get(t) + $tax.get(t) + $shipping.get(t)
|
|
508
|
+
})
|
|
509
|
+
|
|
510
|
+
const $itemCount = derivation((t) => {
|
|
511
|
+
const cart = $cart.get(t)
|
|
512
|
+
return Object.values(cart).reduce(
|
|
513
|
+
(sum, item) => sum + item.quantity,
|
|
514
|
+
0
|
|
515
|
+
)
|
|
516
|
+
})
|
|
517
|
+
|
|
518
|
+
// Operations
|
|
519
|
+
function addToCart(product: Product) {
|
|
520
|
+
const existing = $cart.pick().get(product.id)
|
|
521
|
+
|
|
522
|
+
if (existing) {
|
|
523
|
+
$cart.update(product.id, {
|
|
524
|
+
product,
|
|
525
|
+
quantity: existing.quantity + 1
|
|
526
|
+
})
|
|
527
|
+
} else {
|
|
528
|
+
$cart.add(product.id, {
|
|
529
|
+
product,
|
|
530
|
+
quantity: 1
|
|
531
|
+
})
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function removeFromCart(productId: string) {
|
|
536
|
+
$cart.delete(productId)
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function updateQuantity(productId: string, quantity: number) {
|
|
540
|
+
const item = $cart.pick().get(productId)
|
|
541
|
+
if (!item) return
|
|
542
|
+
|
|
543
|
+
if (quantity <= 0) {
|
|
544
|
+
$cart.delete(productId)
|
|
545
|
+
} else {
|
|
546
|
+
$cart.update(productId, {
|
|
547
|
+
...item,
|
|
548
|
+
quantity
|
|
549
|
+
})
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function clearCart() {
|
|
554
|
+
const items = Array.from($cart.pick().keys())
|
|
555
|
+
items.forEach(id => $cart.delete(id))
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// UI Updates - Add items
|
|
559
|
+
effect((t) => {
|
|
560
|
+
const lastAdded = $cart.$lastAdded.get(t)
|
|
561
|
+
if (lastAdded) {
|
|
562
|
+
addCartItemToUI(lastAdded.value)
|
|
563
|
+
}
|
|
564
|
+
})
|
|
565
|
+
|
|
566
|
+
// UI Updates - Update items
|
|
567
|
+
effect((t) => {
|
|
568
|
+
const lastUpdated = $cart.$lastUpdated.get(t)
|
|
569
|
+
if (lastUpdated) {
|
|
570
|
+
updateCartItemInUI(lastUpdated.value)
|
|
571
|
+
}
|
|
572
|
+
})
|
|
573
|
+
|
|
574
|
+
// UI Updates - Remove items
|
|
575
|
+
effect((t) => {
|
|
576
|
+
const lastDeleted = $cart.$lastDeleted.get(t)
|
|
577
|
+
if (lastDeleted) {
|
|
578
|
+
removeCartItemFromUI(lastDeleted.key)
|
|
579
|
+
}
|
|
580
|
+
})
|
|
581
|
+
|
|
582
|
+
// UI Updates - Totals
|
|
583
|
+
effect((t) => {
|
|
584
|
+
const subtotal = $subtotal.get(t)
|
|
585
|
+
const tax = $tax.get(t)
|
|
586
|
+
const shipping = $shipping.get(t)
|
|
587
|
+
const total = $total.get(t)
|
|
588
|
+
|
|
589
|
+
renderCartTotals({
|
|
590
|
+
subtotal,
|
|
591
|
+
tax,
|
|
592
|
+
shipping,
|
|
593
|
+
total
|
|
594
|
+
})
|
|
595
|
+
})
|
|
596
|
+
|
|
597
|
+
// UI Updates - Item count badge
|
|
598
|
+
effect((t) => {
|
|
599
|
+
const count = $itemCount.get(t)
|
|
600
|
+
updateCartBadge(count)
|
|
601
|
+
})
|
|
602
|
+
|
|
603
|
+
// Persistence
|
|
604
|
+
effect((t) => {
|
|
605
|
+
const cart = $cart.get(t)
|
|
606
|
+
const cartObj = Object.fromEntries(cart)
|
|
607
|
+
localStorage.setItem('cart', JSON.stringify(cartObj))
|
|
608
|
+
})
|
|
609
|
+
|
|
610
|
+
// Load cart on init
|
|
611
|
+
function loadCart() {
|
|
612
|
+
const stored = localStorage.getItem('cart')
|
|
613
|
+
if (stored) {
|
|
614
|
+
const items = JSON.parse(stored)
|
|
615
|
+
for (const [key, item] of Object.entries(items)) {
|
|
616
|
+
const existing = $cart.pick().has(key)
|
|
617
|
+
if (existing) {
|
|
618
|
+
$cart.update(key, item as CartItem)
|
|
619
|
+
} else {
|
|
620
|
+
$cart.add(key, item as CartItem)
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
loadCart()
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
### Shopping Cart Flow
|
|
630
|
+
|
|
631
|
+
```mermaid
|
|
632
|
+
flowchart TD
|
|
633
|
+
A[User Actions] --> B[addToCart/updateQuantity]
|
|
634
|
+
B --> C[$cart map]
|
|
635
|
+
C --> D[$subtotal]
|
|
636
|
+
D --> E[$tax]
|
|
637
|
+
D --> F[$shipping]
|
|
638
|
+
E --> G[$total]
|
|
639
|
+
F --> G
|
|
640
|
+
C --> H[$itemCount]
|
|
641
|
+
|
|
642
|
+
C --> I[$lastAdded effect]
|
|
643
|
+
C --> J[$lastUpdated effect]
|
|
644
|
+
C --> K[$lastDeleted effect]
|
|
645
|
+
I --> L[Add UI Item]
|
|
646
|
+
J --> M[Update UI Item]
|
|
647
|
+
K --> N[Remove UI Item]
|
|
648
|
+
|
|
649
|
+
D --> O[Render Totals]
|
|
650
|
+
E --> O
|
|
651
|
+
F --> O
|
|
652
|
+
G --> O
|
|
653
|
+
H --> P[Update Badge]
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
---
|
|
657
|
+
|
|
658
|
+
## Example 5: Auto-save Feature
|
|
659
|
+
|
|
660
|
+
An auto-save system that debounces changes and handles errors.
|
|
661
|
+
|
|
662
|
+
```typescript
|
|
663
|
+
import { state, effect, signal } from '@ersbeth/picoflow'
|
|
664
|
+
|
|
665
|
+
// Types
|
|
666
|
+
interface Document {
|
|
667
|
+
id: string
|
|
668
|
+
title: string
|
|
669
|
+
content: string
|
|
670
|
+
lastModified: number
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
type SaveStatus = 'saved' | 'saving' | 'unsaved' | 'error'
|
|
674
|
+
|
|
675
|
+
// State
|
|
676
|
+
const $document = state<Document>({
|
|
677
|
+
id: '123',
|
|
678
|
+
title: 'Untitled',
|
|
679
|
+
content: '',
|
|
680
|
+
lastModified: Date.now()
|
|
681
|
+
})
|
|
682
|
+
|
|
683
|
+
const $saveStatus = state<SaveStatus>('saved')
|
|
684
|
+
const $lastError = state<string | null>(null)
|
|
685
|
+
const $manualSave = signal()
|
|
686
|
+
|
|
687
|
+
// Debounced auto-save
|
|
688
|
+
let saveTimeout: ReturnType<typeof setTimeout> | null = null
|
|
689
|
+
const SAVE_DELAY = 2000 // 2 seconds
|
|
690
|
+
|
|
691
|
+
effect((t) => {
|
|
692
|
+
const doc = $document.get(t)
|
|
693
|
+
|
|
694
|
+
// Clear previous timeout
|
|
695
|
+
if (saveTimeout) {
|
|
696
|
+
clearTimeout(saveTimeout)
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// Mark as unsaved
|
|
700
|
+
$saveStatus.set('unsaved')
|
|
701
|
+
|
|
702
|
+
// Debounce save
|
|
703
|
+
saveTimeout = setTimeout(() => {
|
|
704
|
+
saveDocument(doc)
|
|
705
|
+
}, SAVE_DELAY)
|
|
706
|
+
})
|
|
707
|
+
|
|
708
|
+
// Manual save trigger
|
|
709
|
+
effect((t) => {
|
|
710
|
+
$manualSave.watch(t)
|
|
711
|
+
const doc = $document.pick()
|
|
712
|
+
|
|
713
|
+
// Clear debounce timeout
|
|
714
|
+
if (saveTimeout) {
|
|
715
|
+
clearTimeout(saveTimeout)
|
|
716
|
+
saveTimeout = null
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
saveDocument(doc)
|
|
720
|
+
})
|
|
721
|
+
|
|
722
|
+
// Save function
|
|
723
|
+
async function saveDocument(doc: Document) {
|
|
724
|
+
$saveStatus.set('saving')
|
|
725
|
+
$lastError.set(null)
|
|
726
|
+
|
|
727
|
+
try {
|
|
728
|
+
const response = await fetch(`/api/documents/${doc.id}`, {
|
|
729
|
+
method: 'PUT',
|
|
730
|
+
headers: { 'Content-Type': 'application/json' },
|
|
731
|
+
body: JSON.stringify(doc)
|
|
732
|
+
})
|
|
733
|
+
|
|
734
|
+
if (!response.ok) {
|
|
735
|
+
throw new Error('Save failed')
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
$saveStatus.set('saved')
|
|
739
|
+
} catch (error) {
|
|
740
|
+
$saveStatus.set('error')
|
|
741
|
+
$lastError.set(error.message)
|
|
742
|
+
|
|
743
|
+
// Retry after 5 seconds
|
|
744
|
+
setTimeout(() => {
|
|
745
|
+
if ($saveStatus.pick() === 'error') {
|
|
746
|
+
saveDocument($document.pick())
|
|
747
|
+
}
|
|
748
|
+
}, 5000)
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// UI Updates
|
|
753
|
+
effect((t) => {
|
|
754
|
+
const status = $saveStatus.get(t)
|
|
755
|
+
const error = $lastError.get(t)
|
|
756
|
+
|
|
757
|
+
updateSaveIndicator(status, error)
|
|
758
|
+
})
|
|
759
|
+
|
|
760
|
+
// Document operations
|
|
761
|
+
function updateTitle(title: string) {
|
|
762
|
+
$document.set(doc => ({
|
|
763
|
+
...doc,
|
|
764
|
+
title,
|
|
765
|
+
lastModified: Date.now()
|
|
766
|
+
}))
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function updateContent(content: string) {
|
|
770
|
+
$document.set(doc => ({
|
|
771
|
+
...doc,
|
|
772
|
+
content,
|
|
773
|
+
lastModified: Date.now()
|
|
774
|
+
}))
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
function triggerManualSave() {
|
|
778
|
+
$manualSave.trigger()
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// Warn before leaving with unsaved changes
|
|
782
|
+
window.addEventListener('beforeunload', (e) => {
|
|
783
|
+
if ($saveStatus.pick() === 'unsaved' || $saveStatus.pick() === 'saving') {
|
|
784
|
+
e.preventDefault()
|
|
785
|
+
e.returnValue = ''
|
|
786
|
+
}
|
|
787
|
+
})
|
|
788
|
+
```
|
|
789
|
+
|
|
790
|
+
### Auto-save Flow
|
|
791
|
+
|
|
792
|
+
```mermaid
|
|
793
|
+
sequenceDiagram
|
|
794
|
+
participant User
|
|
795
|
+
participant Document
|
|
796
|
+
participant Effect
|
|
797
|
+
participant API
|
|
798
|
+
|
|
799
|
+
User->>Document: Edit content
|
|
800
|
+
Document->>Effect: Change detected
|
|
801
|
+
Effect->>Effect: Start 2s timer
|
|
802
|
+
Note over Effect: Status: unsaved
|
|
803
|
+
|
|
804
|
+
User->>Document: Edit more
|
|
805
|
+
Effect->>Effect: Reset timer
|
|
806
|
+
|
|
807
|
+
Note over Effect: 2s pass...
|
|
808
|
+
Effect->>API: Save document
|
|
809
|
+
Note over Effect: Status: saving
|
|
810
|
+
API->>Effect: Success
|
|
811
|
+
Note over Effect: Status: saved
|
|
812
|
+
|
|
813
|
+
User->>Effect: Manual save
|
|
814
|
+
Effect->>Effect: Cancel timer
|
|
815
|
+
Effect->>API: Save immediately
|
|
816
|
+
```
|
|
817
|
+
|
|
818
|
+
---
|
|
819
|
+
|
|
820
|
+
## Example 6: Event Bus with Signals
|
|
821
|
+
|
|
822
|
+
A global event bus using signals to coordinate communication between independent components.
|
|
823
|
+
|
|
824
|
+
```typescript
|
|
825
|
+
import { signal, effect, state } from '@ersbeth/picoflow'
|
|
826
|
+
|
|
827
|
+
// Event signals
|
|
828
|
+
const $userLoggedIn = signal()
|
|
829
|
+
const $userLoggedOut = signal()
|
|
830
|
+
const $dataRefreshRequested = signal()
|
|
831
|
+
const $notificationRequested = signal()
|
|
832
|
+
const $themeChanged = signal()
|
|
833
|
+
|
|
834
|
+
// Shared state
|
|
835
|
+
const $currentUser = state<User | null>(null)
|
|
836
|
+
const $theme = state<'light' | 'dark'>('light')
|
|
837
|
+
const $notifications = state<string[]>([])
|
|
838
|
+
|
|
839
|
+
// Component 1: Authentication Manager
|
|
840
|
+
effect((t) => {
|
|
841
|
+
$userLoggedIn.watch(t)
|
|
842
|
+
const user = $currentUser.pick()
|
|
843
|
+
|
|
844
|
+
if (user) {
|
|
845
|
+
console.log(`User ${user.name} logged in`)
|
|
846
|
+
|
|
847
|
+
// Initialize user-specific features
|
|
848
|
+
loadUserPreferences(user.id)
|
|
849
|
+
startActivityTracking(user.id)
|
|
850
|
+
|
|
851
|
+
// Request data refresh
|
|
852
|
+
$dataRefreshRequested.trigger()
|
|
853
|
+
}
|
|
854
|
+
})
|
|
855
|
+
|
|
856
|
+
effect((t) => {
|
|
857
|
+
$userLoggedOut.watch(t)
|
|
858
|
+
|
|
859
|
+
console.log('User logged out')
|
|
860
|
+
|
|
861
|
+
// Cleanup user-specific features
|
|
862
|
+
clearUserPreferences()
|
|
863
|
+
stopActivityTracking()
|
|
864
|
+
|
|
865
|
+
// Reset state
|
|
866
|
+
$currentUser.set(null)
|
|
867
|
+
})
|
|
868
|
+
|
|
869
|
+
// Component 2: Data Manager
|
|
870
|
+
const $userData = state<UserData | null>(null)
|
|
871
|
+
const $dashboardData = state<DashboardData | null>(null)
|
|
872
|
+
|
|
873
|
+
effect((t) => {
|
|
874
|
+
$dataRefreshRequested.watch(t)
|
|
875
|
+
const user = $currentUser.pick()
|
|
876
|
+
|
|
877
|
+
if (!user) return
|
|
878
|
+
|
|
879
|
+
console.log('Refreshing all data...')
|
|
880
|
+
|
|
881
|
+
// Fetch multiple data sources
|
|
882
|
+
Promise.all([
|
|
883
|
+
fetchUserData(user.id),
|
|
884
|
+
fetchDashboardData(user.id)
|
|
885
|
+
]).then(([userData, dashboardData]) => {
|
|
886
|
+
$userData.set(userData)
|
|
887
|
+
$dashboardData.set(dashboardData)
|
|
888
|
+
$notificationRequested.trigger()
|
|
889
|
+
})
|
|
890
|
+
})
|
|
891
|
+
|
|
892
|
+
// Component 3: Notification Manager
|
|
893
|
+
const $lastNotification = state<string>('')
|
|
894
|
+
|
|
895
|
+
effect((t) => {
|
|
896
|
+
$notificationRequested.watch(t)
|
|
897
|
+
const notification = $lastNotification.pick()
|
|
898
|
+
|
|
899
|
+
if (notification) {
|
|
900
|
+
showToast(notification)
|
|
901
|
+
}
|
|
902
|
+
})
|
|
903
|
+
|
|
904
|
+
effect((t) => {
|
|
905
|
+
$userLoggedIn.watch(t)
|
|
906
|
+
const user = $currentUser.pick()
|
|
907
|
+
|
|
908
|
+
if (user) {
|
|
909
|
+
$lastNotification.set(`Welcome back, ${user.name}!`)
|
|
910
|
+
$notificationRequested.trigger()
|
|
911
|
+
}
|
|
912
|
+
})
|
|
913
|
+
|
|
914
|
+
// Component 4: Theme Manager
|
|
915
|
+
effect((t) => {
|
|
916
|
+
$themeChanged.watch(t)
|
|
917
|
+
const theme = $theme.pick()
|
|
918
|
+
|
|
919
|
+
document.body.classList.remove('light', 'dark')
|
|
920
|
+
document.body.classList.add(theme)
|
|
921
|
+
|
|
922
|
+
localStorage.setItem('theme', theme)
|
|
923
|
+
|
|
924
|
+
$lastNotification.set(`Theme changed to ${theme}`)
|
|
925
|
+
$notificationRequested.trigger()
|
|
926
|
+
})
|
|
927
|
+
|
|
928
|
+
// Component 5: Analytics Tracker
|
|
929
|
+
effect((t) => {
|
|
930
|
+
$userLoggedIn.watch(t)
|
|
931
|
+
const user = $currentUser.pick()
|
|
932
|
+
|
|
933
|
+
if (user) {
|
|
934
|
+
analytics.track('user_logged_in', {
|
|
935
|
+
userId: user.id,
|
|
936
|
+
timestamp: Date.now()
|
|
937
|
+
})
|
|
938
|
+
}
|
|
939
|
+
})
|
|
940
|
+
|
|
941
|
+
effect((t) => {
|
|
942
|
+
$dataRefreshRequested.watch(t)
|
|
943
|
+
|
|
944
|
+
analytics.track('data_refresh_requested', {
|
|
945
|
+
timestamp: Date.now()
|
|
946
|
+
})
|
|
947
|
+
})
|
|
948
|
+
|
|
949
|
+
// Public API
|
|
950
|
+
export const auth = {
|
|
951
|
+
login: (user: User) => {
|
|
952
|
+
$currentUser.set(user)
|
|
953
|
+
$userLoggedIn.trigger()
|
|
954
|
+
},
|
|
955
|
+
|
|
956
|
+
logout: () => {
|
|
957
|
+
$userLoggedOut.trigger()
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
export const data = {
|
|
962
|
+
refresh: () => {
|
|
963
|
+
$dataRefreshRequested.trigger()
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
export const theme = {
|
|
968
|
+
set: (newTheme: 'light' | 'dark') => {
|
|
969
|
+
$theme.set(newTheme)
|
|
970
|
+
$themeChanged.trigger()
|
|
971
|
+
},
|
|
972
|
+
|
|
973
|
+
toggle: () => {
|
|
974
|
+
const current = $theme.pick()
|
|
975
|
+
const newTheme = current === 'light' ? 'dark' : 'light'
|
|
976
|
+
$theme.set(newTheme)
|
|
977
|
+
$themeChanged.trigger()
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
export const notifications = {
|
|
982
|
+
show: (message: string) => {
|
|
983
|
+
$lastNotification.set(message)
|
|
984
|
+
$notificationRequested.trigger()
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
```
|
|
988
|
+
|
|
989
|
+
### Event Bus Flow
|
|
990
|
+
|
|
991
|
+
```mermaid
|
|
992
|
+
sequenceDiagram
|
|
993
|
+
participant User
|
|
994
|
+
participant Auth as Auth Manager
|
|
995
|
+
participant Data as Data Manager
|
|
996
|
+
participant Notif as Notification Manager
|
|
997
|
+
participant Analytics
|
|
998
|
+
|
|
999
|
+
User->>Auth: login(user)
|
|
1000
|
+
activate Auth
|
|
1001
|
+
Auth->>Auth: $currentUser.set(user)
|
|
1002
|
+
Auth->>Auth: $userLoggedIn.trigger()
|
|
1003
|
+
|
|
1004
|
+
Note over Auth,Analytics: Signal broadcasts to all watchers
|
|
1005
|
+
|
|
1006
|
+
Auth->>Data: Watch $userLoggedIn
|
|
1007
|
+
activate Data
|
|
1008
|
+
Note over Data: Load user preferences<br/>Start tracking
|
|
1009
|
+
Data->>Data: $dataRefreshRequested.trigger()
|
|
1010
|
+
deactivate Data
|
|
1011
|
+
|
|
1012
|
+
Auth->>Notif: Watch $userLoggedIn
|
|
1013
|
+
activate Notif
|
|
1014
|
+
Note over Notif: Show welcome message
|
|
1015
|
+
Notif->>Notif: $notificationRequested.trigger()
|
|
1016
|
+
deactivate Notif
|
|
1017
|
+
|
|
1018
|
+
Auth->>Analytics: Watch $userLoggedIn
|
|
1019
|
+
activate Analytics
|
|
1020
|
+
Note over Analytics: Track login event
|
|
1021
|
+
deactivate Analytics
|
|
1022
|
+
|
|
1023
|
+
deactivate Auth
|
|
1024
|
+
|
|
1025
|
+
Data->>Data: Watch $dataRefreshRequested
|
|
1026
|
+
activate Data
|
|
1027
|
+
Note over Data: Fetch user data
|
|
1028
|
+
Data->>Notif: $notificationRequested.trigger()
|
|
1029
|
+
deactivate Data
|
|
1030
|
+
```
|
|
1031
|
+
|
|
1032
|
+
**Key Benefits:**
|
|
1033
|
+
|
|
1034
|
+
- **Decoupled**: Components don't know about each other
|
|
1035
|
+
- **Scalable**: Easy to add new listeners to any signal
|
|
1036
|
+
- **Testable**: Each component can be tested independently
|
|
1037
|
+
- **Maintainable**: Clear separation of concerns
|
|
1038
|
+
|
|
1039
|
+
---
|
|
1040
|
+
|
|
1041
|
+
## Example 7: Manual Refresh with Signals
|
|
1042
|
+
|
|
1043
|
+
Use a signal to trigger data refresh on demand:
|
|
1044
|
+
|
|
1045
|
+
```typescript
|
|
1046
|
+
import { signal, effect, state, resource } from '@ersbeth/picoflow'
|
|
1047
|
+
|
|
1048
|
+
const $manualRefresh = signal()
|
|
1049
|
+
const $userId = state(1)
|
|
1050
|
+
|
|
1051
|
+
// Resource that refetches when signal triggers
|
|
1052
|
+
const $userData = resource(
|
|
1053
|
+
(t) => {
|
|
1054
|
+
$manualRefresh.watch(t) // Watch for manual refresh
|
|
1055
|
+
return $userId.get(t)
|
|
1056
|
+
},
|
|
1057
|
+
(id) => fetchUser(id)
|
|
1058
|
+
)
|
|
1059
|
+
|
|
1060
|
+
// Display the data
|
|
1061
|
+
effect((t) => {
|
|
1062
|
+
const data = $userData.get(t)
|
|
1063
|
+
if (data.state === 'ready') {
|
|
1064
|
+
renderUserProfile(data.value)
|
|
1065
|
+
}
|
|
1066
|
+
})
|
|
1067
|
+
|
|
1068
|
+
// Trigger refresh from a button
|
|
1069
|
+
function onRefreshClick() {
|
|
1070
|
+
$manualRefresh.trigger()
|
|
1071
|
+
}
|
|
1072
|
+
```
|
|
1073
|
+
|
|
1074
|
+
### Manual Refresh Flow
|
|
1075
|
+
|
|
1076
|
+
```mermaid
|
|
1077
|
+
sequenceDiagram
|
|
1078
|
+
participant User
|
|
1079
|
+
participant Button
|
|
1080
|
+
participant $manualRefresh as $manualRefresh (Signal)
|
|
1081
|
+
participant Resource as $userData (Resource)
|
|
1082
|
+
participant API
|
|
1083
|
+
participant Effect
|
|
1084
|
+
|
|
1085
|
+
User->>Button: Click refresh
|
|
1086
|
+
Button->>$manualRefresh: trigger()
|
|
1087
|
+
|
|
1088
|
+
activate $manualRefresh
|
|
1089
|
+
Note over $manualRefresh: Notify all watchers
|
|
1090
|
+
$manualRefresh->>Resource: Re-execute source function
|
|
1091
|
+
deactivate $manualRefresh
|
|
1092
|
+
|
|
1093
|
+
activate Resource
|
|
1094
|
+
Resource->>API: fetchUser(1)
|
|
1095
|
+
Note over Resource: State: pending
|
|
1096
|
+
|
|
1097
|
+
API-->>Resource: User data
|
|
1098
|
+
Note over Resource: State: ready
|
|
1099
|
+
Resource->>Effect: Notify
|
|
1100
|
+
deactivate Resource
|
|
1101
|
+
|
|
1102
|
+
activate Effect
|
|
1103
|
+
Effect->>Effect: renderUserProfile(data)
|
|
1104
|
+
deactivate Effect
|
|
1105
|
+
```
|
|
1106
|
+
|
|
1107
|
+
**Use Case:** Perfect for implementing refresh buttons in dashboards, data tables, or any UI that needs manual data updates.
|
|
1108
|
+
|
|
1109
|
+
---
|
|
1110
|
+
|
|
1111
|
+
## Example 8: Coordinating Multiple Operations with Signals
|
|
1112
|
+
|
|
1113
|
+
Use signals to synchronize independent reactive flows:
|
|
1114
|
+
|
|
1115
|
+
```typescript
|
|
1116
|
+
import { signal, effect, state } from '@ersbeth/picoflow'
|
|
1117
|
+
|
|
1118
|
+
const $syncPoint = signal()
|
|
1119
|
+
|
|
1120
|
+
const $dataA = state<Data | null>(null)
|
|
1121
|
+
const $dataB = state<Data | null>(null)
|
|
1122
|
+
|
|
1123
|
+
// First operation: fetch A
|
|
1124
|
+
effect((t) => {
|
|
1125
|
+
$syncPoint.watch(t)
|
|
1126
|
+
fetchDataA().then(data => {
|
|
1127
|
+
$dataA.set(data)
|
|
1128
|
+
})
|
|
1129
|
+
})
|
|
1130
|
+
|
|
1131
|
+
// Second operation: fetch B
|
|
1132
|
+
effect((t) => {
|
|
1133
|
+
$syncPoint.watch(t)
|
|
1134
|
+
fetchDataB().then(data => {
|
|
1135
|
+
$dataB.set(data)
|
|
1136
|
+
})
|
|
1137
|
+
})
|
|
1138
|
+
|
|
1139
|
+
// Combine results when both are ready
|
|
1140
|
+
effect((t) => {
|
|
1141
|
+
const a = $dataA.get(t)
|
|
1142
|
+
const b = $dataB.get(t)
|
|
1143
|
+
|
|
1144
|
+
if (a && b) {
|
|
1145
|
+
renderCombinedView(a, b)
|
|
1146
|
+
}
|
|
1147
|
+
})
|
|
1148
|
+
|
|
1149
|
+
// Trigger both fetches at once
|
|
1150
|
+
function refreshAll() {
|
|
1151
|
+
$syncPoint.trigger()
|
|
1152
|
+
}
|
|
1153
|
+
```
|
|
1154
|
+
|
|
1155
|
+
### Synchronization Flow
|
|
1156
|
+
|
|
1157
|
+
```mermaid
|
|
1158
|
+
sequenceDiagram
|
|
1159
|
+
participant User
|
|
1160
|
+
participant Signal as $syncPoint
|
|
1161
|
+
participant EffectA as Effect A
|
|
1162
|
+
participant EffectB as Effect B
|
|
1163
|
+
participant CombineEffect as Combine Effect
|
|
1164
|
+
|
|
1165
|
+
User->>Signal: refreshAll()
|
|
1166
|
+
|
|
1167
|
+
activate Signal
|
|
1168
|
+
Note over Signal: Notify all watchers
|
|
1169
|
+
|
|
1170
|
+
par Parallel Fetches
|
|
1171
|
+
Signal->>EffectA: Execute
|
|
1172
|
+
activate EffectA
|
|
1173
|
+
EffectA->>EffectA: fetchDataA()
|
|
1174
|
+
Note over EffectA: Async operation...
|
|
1175
|
+
|
|
1176
|
+
Signal->>EffectB: Execute
|
|
1177
|
+
activate EffectB
|
|
1178
|
+
EffectB->>EffectB: fetchDataB()
|
|
1179
|
+
Note over EffectB: Async operation...
|
|
1180
|
+
end
|
|
1181
|
+
deactivate Signal
|
|
1182
|
+
|
|
1183
|
+
EffectA->>EffectA: $dataA.set(data)
|
|
1184
|
+
EffectA->>CombineEffect: Notify
|
|
1185
|
+
deactivate EffectA
|
|
1186
|
+
|
|
1187
|
+
EffectB->>EffectB: $dataB.set(data)
|
|
1188
|
+
EffectB->>CombineEffect: Notify
|
|
1189
|
+
deactivate EffectB
|
|
1190
|
+
|
|
1191
|
+
activate CombineEffect
|
|
1192
|
+
Note over CombineEffect: Both ready!
|
|
1193
|
+
CombineEffect->>CombineEffect: renderCombinedView(a, b)
|
|
1194
|
+
deactivate CombineEffect
|
|
1195
|
+
```
|
|
1196
|
+
|
|
1197
|
+
**Use Case:** Ideal for coordinating multiple data fetches, synchronizing app initialization, or triggering multiple related operations from a single action.
|
|
1198
|
+
|
|
1199
|
+
---
|
|
1200
|
+
|
|
1201
|
+
## Example 9: Save Confirmation with Signals
|
|
1202
|
+
|
|
1203
|
+
Use signals to notify when async operations complete:
|
|
1204
|
+
|
|
1205
|
+
```typescript
|
|
1206
|
+
import { signal, effect, state } from '@ersbeth/picoflow'
|
|
1207
|
+
|
|
1208
|
+
const $saveComplete = signal()
|
|
1209
|
+
const $saveError = signal()
|
|
1210
|
+
const $document = state({ title: '', content: '' })
|
|
1211
|
+
|
|
1212
|
+
// Show success message when save completes
|
|
1213
|
+
effect((t) => {
|
|
1214
|
+
$saveComplete.watch(t)
|
|
1215
|
+
showNotification('Document saved!', 'success')
|
|
1216
|
+
})
|
|
1217
|
+
|
|
1218
|
+
// Show error message on failure
|
|
1219
|
+
effect((t) => {
|
|
1220
|
+
$saveError.watch(t)
|
|
1221
|
+
showNotification('Save failed', 'error')
|
|
1222
|
+
})
|
|
1223
|
+
|
|
1224
|
+
// Save function
|
|
1225
|
+
async function saveDocument() {
|
|
1226
|
+
try {
|
|
1227
|
+
const doc = $document.pick()
|
|
1228
|
+
await api.saveDocument(doc)
|
|
1229
|
+
$saveComplete.trigger()
|
|
1230
|
+
} catch (error) {
|
|
1231
|
+
$saveError.trigger()
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
```
|
|
1235
|
+
|
|
1236
|
+
### Save Confirmation Flow
|
|
1237
|
+
|
|
1238
|
+
```mermaid
|
|
1239
|
+
sequenceDiagram
|
|
1240
|
+
participant User
|
|
1241
|
+
participant SaveFn as saveDocument()
|
|
1242
|
+
participant API
|
|
1243
|
+
participant $saveComplete as $saveComplete Signal
|
|
1244
|
+
participant $saveError as $saveError Signal
|
|
1245
|
+
participant SuccessEffect
|
|
1246
|
+
participant ErrorEffect
|
|
1247
|
+
|
|
1248
|
+
User->>SaveFn: Call saveDocument()
|
|
1249
|
+
activate SaveFn
|
|
1250
|
+
|
|
1251
|
+
SaveFn->>API: POST document
|
|
1252
|
+
|
|
1253
|
+
alt Save Success
|
|
1254
|
+
API-->>SaveFn: 200 OK
|
|
1255
|
+
SaveFn->>$saveComplete: trigger()
|
|
1256
|
+
activate $saveComplete
|
|
1257
|
+
$saveComplete->>SuccessEffect: Execute
|
|
1258
|
+
activate SuccessEffect
|
|
1259
|
+
Note over SuccessEffect: Show success notification
|
|
1260
|
+
deactivate SuccessEffect
|
|
1261
|
+
deactivate $saveComplete
|
|
1262
|
+
else Save Failure
|
|
1263
|
+
API-->>SaveFn: 500 Error
|
|
1264
|
+
SaveFn->>$saveError: trigger()
|
|
1265
|
+
activate $saveError
|
|
1266
|
+
$saveError->>ErrorEffect: Execute
|
|
1267
|
+
activate ErrorEffect
|
|
1268
|
+
Note over ErrorEffect: Show error notification
|
|
1269
|
+
deactivate ErrorEffect
|
|
1270
|
+
deactivate $saveError
|
|
1271
|
+
end
|
|
1272
|
+
|
|
1273
|
+
deactivate SaveFn
|
|
1274
|
+
```
|
|
1275
|
+
|
|
1276
|
+
**Use Case:** Great for handling async operation outcomes, showing notifications, or triggering cleanup after operations complete.
|
|
1277
|
+
|
|
1278
|
+
---
|
|
1279
|
+
|
|
1280
|
+
## Example 10: Mathematical Calculations with Derivations
|
|
1281
|
+
|
|
1282
|
+
Use derivations to compute statistics from reactive data:
|
|
1283
|
+
|
|
1284
|
+
```typescript
|
|
1285
|
+
import { state, derivation, effect } from '@ersbeth/picoflow'
|
|
1286
|
+
|
|
1287
|
+
const $numbers = state([1, 2, 3, 4, 5])
|
|
1288
|
+
|
|
1289
|
+
const $sum = derivation((t) => {
|
|
1290
|
+
return $numbers.get(t).reduce((a, b) => a + b, 0)
|
|
1291
|
+
})
|
|
1292
|
+
|
|
1293
|
+
const $average = derivation((t) => {
|
|
1294
|
+
const sum = $sum.get(t)
|
|
1295
|
+
const count = $numbers.pick().length
|
|
1296
|
+
return sum / count
|
|
1297
|
+
})
|
|
1298
|
+
|
|
1299
|
+
const $max = derivation((t) => {
|
|
1300
|
+
return Math.max(...$numbers.get(t))
|
|
1301
|
+
})
|
|
1302
|
+
|
|
1303
|
+
const $min = derivation((t) => {
|
|
1304
|
+
return Math.min(...$numbers.get(t))
|
|
1305
|
+
})
|
|
1306
|
+
|
|
1307
|
+
// Display statistics
|
|
1308
|
+
effect((t) => {
|
|
1309
|
+
console.log('Sum:', $sum.get(t))
|
|
1310
|
+
console.log('Average:', $average.get(t))
|
|
1311
|
+
console.log('Max:', $max.get(t))
|
|
1312
|
+
console.log('Min:', $min.get(t))
|
|
1313
|
+
})
|
|
1314
|
+
|
|
1315
|
+
// Update numbers
|
|
1316
|
+
$numbers.set([10, 20, 30, 40, 50])
|
|
1317
|
+
// All statistics update automatically
|
|
1318
|
+
```
|
|
1319
|
+
|
|
1320
|
+
**Use Case:** Real-time statistics dashboards, data analysis tools, financial calculations.
|
|
1321
|
+
|
|
1322
|
+
---
|
|
1323
|
+
|
|
1324
|
+
## Example 11: Filtered Lists with Derivations
|
|
1325
|
+
|
|
1326
|
+
Implement dynamic filtering with chained derivations:
|
|
1327
|
+
|
|
1328
|
+
```typescript
|
|
1329
|
+
interface Todo {
|
|
1330
|
+
id: number
|
|
1331
|
+
text: string
|
|
1332
|
+
completed: boolean
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
const $todos = state<Todo[]>([
|
|
1336
|
+
{ id: 1, text: 'Learn PicoFlow', completed: true },
|
|
1337
|
+
{ id: 2, text: 'Build app', completed: false },
|
|
1338
|
+
{ id: 3, text: 'Deploy', completed: false }
|
|
1339
|
+
])
|
|
1340
|
+
|
|
1341
|
+
const $filter = state<'all' | 'active' | 'completed'>('all')
|
|
1342
|
+
|
|
1343
|
+
const $filteredTodos = derivation((t) => {
|
|
1344
|
+
const todos = $todos.get(t)
|
|
1345
|
+
const filter = $filter.get(t)
|
|
1346
|
+
|
|
1347
|
+
switch (filter) {
|
|
1348
|
+
case 'active':
|
|
1349
|
+
return todos.filter(todo => !todo.completed)
|
|
1350
|
+
case 'completed':
|
|
1351
|
+
return todos.filter(todo => todo.completed)
|
|
1352
|
+
default:
|
|
1353
|
+
return todos
|
|
1354
|
+
}
|
|
1355
|
+
})
|
|
1356
|
+
|
|
1357
|
+
const $activeCount = derivation((t) => {
|
|
1358
|
+
return $todos.get(t).filter(todo => !todo.completed).length
|
|
1359
|
+
})
|
|
1360
|
+
|
|
1361
|
+
const $completedCount = derivation((t) => {
|
|
1362
|
+
return $todos.get(t).filter(todo => todo.completed).length
|
|
1363
|
+
})
|
|
1364
|
+
|
|
1365
|
+
// Display filtered todos
|
|
1366
|
+
effect((t) => {
|
|
1367
|
+
const filtered = $filteredTodos.get(t)
|
|
1368
|
+
const active = $activeCount.get(t)
|
|
1369
|
+
const completed = $completedCount.get(t)
|
|
1370
|
+
|
|
1371
|
+
console.log(`Showing ${filtered.length} todos`)
|
|
1372
|
+
console.log(`${active} active, ${completed} completed`)
|
|
1373
|
+
renderTodoList(filtered)
|
|
1374
|
+
})
|
|
1375
|
+
```
|
|
1376
|
+
|
|
1377
|
+
**Use Case:** Todo lists, task managers, filtered data grids with multiple view modes.
|
|
1378
|
+
|
|
1379
|
+
---
|
|
1380
|
+
|
|
1381
|
+
## Example 12: Formatted Values with Derivations
|
|
1382
|
+
|
|
1383
|
+
Use derivations for internationalization and formatting:
|
|
1384
|
+
|
|
1385
|
+
```typescript
|
|
1386
|
+
const $amount = state(1234.56)
|
|
1387
|
+
const $currency = state('USD')
|
|
1388
|
+
|
|
1389
|
+
const $formatted = derivation((t) => {
|
|
1390
|
+
const amount = $amount.get(t)
|
|
1391
|
+
const currency = $currency.get(t)
|
|
1392
|
+
|
|
1393
|
+
return new Intl.NumberFormat('en-US', {
|
|
1394
|
+
style: 'currency',
|
|
1395
|
+
currency: currency
|
|
1396
|
+
}).format(amount)
|
|
1397
|
+
})
|
|
1398
|
+
|
|
1399
|
+
effect((t) => {
|
|
1400
|
+
console.log($formatted.get(t))
|
|
1401
|
+
})
|
|
1402
|
+
|
|
1403
|
+
$amount.set(9999.99) // "$9,999.99"
|
|
1404
|
+
$currency.set('EUR') // "€9,999.99"
|
|
1405
|
+
$currency.set('JPY') // "¥9,999"
|
|
1406
|
+
```
|
|
1407
|
+
|
|
1408
|
+
**Use Case:** Multi-currency applications, internationalized interfaces, dynamic formatting based on user locale.
|
|
1409
|
+
|
|
1410
|
+
---
|
|
1411
|
+
|
|
1412
|
+
## Example 13: Derived UI State
|
|
1413
|
+
|
|
1414
|
+
Create a unified UI state from multiple sources:
|
|
1415
|
+
|
|
1416
|
+
```typescript
|
|
1417
|
+
const $user = state<User | null>(null)
|
|
1418
|
+
const $loading = state(false)
|
|
1419
|
+
const $error = state<string | null>(null)
|
|
1420
|
+
|
|
1421
|
+
const $uiState = derivation((t) => {
|
|
1422
|
+
const loading = $loading.get(t)
|
|
1423
|
+
const error = $error.get(t)
|
|
1424
|
+
const user = $user.get(t)
|
|
1425
|
+
|
|
1426
|
+
if (loading) return { type: 'loading' as const }
|
|
1427
|
+
if (error) return { type: 'error' as const, message: error }
|
|
1428
|
+
if (user) return { type: 'success' as const, user }
|
|
1429
|
+
return { type: 'idle' as const }
|
|
1430
|
+
})
|
|
1431
|
+
|
|
1432
|
+
effect((t) => {
|
|
1433
|
+
const state = $uiState.get(t)
|
|
1434
|
+
|
|
1435
|
+
switch (state.type) {
|
|
1436
|
+
case 'loading':
|
|
1437
|
+
showSpinner()
|
|
1438
|
+
break
|
|
1439
|
+
case 'error':
|
|
1440
|
+
showError(state.message)
|
|
1441
|
+
break
|
|
1442
|
+
case 'success':
|
|
1443
|
+
showUser(state.user)
|
|
1444
|
+
break
|
|
1445
|
+
case 'idle':
|
|
1446
|
+
showLogin()
|
|
1447
|
+
break
|
|
1448
|
+
}
|
|
1449
|
+
})
|
|
1450
|
+
|
|
1451
|
+
// Simulate async operation
|
|
1452
|
+
async function loadUser() {
|
|
1453
|
+
$loading.set(true)
|
|
1454
|
+
$error.set(null)
|
|
1455
|
+
|
|
1456
|
+
try {
|
|
1457
|
+
const user = await fetchUser()
|
|
1458
|
+
$user.set(user)
|
|
1459
|
+
} catch (err) {
|
|
1460
|
+
$error.set(err.message)
|
|
1461
|
+
} finally {
|
|
1462
|
+
$loading.set(false)
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
```
|
|
1466
|
+
|
|
1467
|
+
**Use Case:** State machines for async operations, loading screens, error boundaries.
|
|
1468
|
+
|
|
1469
|
+
---
|
|
1470
|
+
|
|
1471
|
+
## Example 14: Shopping Cart with Derived Totals
|
|
1472
|
+
|
|
1473
|
+
Calculate cart totals reactively with multiple derivations:
|
|
1474
|
+
|
|
1475
|
+
```typescript
|
|
1476
|
+
interface CartItem {
|
|
1477
|
+
id: string
|
|
1478
|
+
name: string
|
|
1479
|
+
price: number
|
|
1480
|
+
quantity: number
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
const $cartItems = state<CartItem[]>([])
|
|
1484
|
+
|
|
1485
|
+
const $subtotal = derivation((t) => {
|
|
1486
|
+
return $cartItems.get(t).reduce((sum, item) => {
|
|
1487
|
+
return sum + (item.price * item.quantity)
|
|
1488
|
+
}, 0)
|
|
1489
|
+
})
|
|
1490
|
+
|
|
1491
|
+
const $tax = derivation((t) => {
|
|
1492
|
+
return $subtotal.get(t) * 0.08 // 8% tax
|
|
1493
|
+
})
|
|
1494
|
+
|
|
1495
|
+
const $shipping = derivation((t) => {
|
|
1496
|
+
const subtotal = $subtotal.get(t)
|
|
1497
|
+
return subtotal > 50 ? 0 : 5.99 // Free shipping over $50
|
|
1498
|
+
})
|
|
1499
|
+
|
|
1500
|
+
const $total = derivation((t) => {
|
|
1501
|
+
return $subtotal.get(t) + $tax.get(t) + $shipping.get(t)
|
|
1502
|
+
})
|
|
1503
|
+
|
|
1504
|
+
// Display in UI
|
|
1505
|
+
effect((t) => {
|
|
1506
|
+
const subtotal = $subtotal.get(t)
|
|
1507
|
+
const tax = $tax.get(t)
|
|
1508
|
+
const shipping = $shipping.get(t)
|
|
1509
|
+
const total = $total.get(t)
|
|
1510
|
+
|
|
1511
|
+
document.getElementById('subtotal').textContent = `$${subtotal.toFixed(2)}`
|
|
1512
|
+
document.getElementById('tax').textContent = `$${tax.toFixed(2)}`
|
|
1513
|
+
document.getElementById('shipping').textContent = `$${shipping.toFixed(2)}`
|
|
1514
|
+
document.getElementById('total').textContent = `$${total.toFixed(2)}`
|
|
1515
|
+
})
|
|
1516
|
+
|
|
1517
|
+
// Add items to cart
|
|
1518
|
+
function addItem(item: CartItem) {
|
|
1519
|
+
$cartItems.set(items => [...items, item])
|
|
1520
|
+
}
|
|
1521
|
+
```
|
|
1522
|
+
|
|
1523
|
+
**Use Case:** E-commerce carts, order summaries, price calculators with multiple components.
|
|
1524
|
+
|
|
1525
|
+
---
|
|
1526
|
+
|
|
1527
|
+
## Example 15: Product Search and Filter
|
|
1528
|
+
|
|
1529
|
+
Multi-criteria filtering with derivations:
|
|
1530
|
+
|
|
1531
|
+
```typescript
|
|
1532
|
+
interface Product {
|
|
1533
|
+
id: number
|
|
1534
|
+
name: string
|
|
1535
|
+
category: string
|
|
1536
|
+
price: number
|
|
1537
|
+
inStock: boolean
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
const $products = state<Product[]>([
|
|
1541
|
+
{ id: 1, name: 'Laptop', category: 'Electronics', price: 999, inStock: true },
|
|
1542
|
+
{ id: 2, name: 'Mouse', category: 'Electronics', price: 25, inStock: true },
|
|
1543
|
+
{ id: 3, name: 'Desk', category: 'Furniture', price: 300, inStock: false },
|
|
1544
|
+
// ... more products
|
|
1545
|
+
])
|
|
1546
|
+
|
|
1547
|
+
const $searchQuery = state('')
|
|
1548
|
+
const $selectedCategory = state<string | null>(null)
|
|
1549
|
+
const $showOutOfStock = state(false)
|
|
1550
|
+
|
|
1551
|
+
const $filteredProducts = derivation((t) => {
|
|
1552
|
+
let products = $products.get(t)
|
|
1553
|
+
const query = $searchQuery.get(t).toLowerCase()
|
|
1554
|
+
const category = $selectedCategory.get(t)
|
|
1555
|
+
const showOutOfStock = $showOutOfStock.get(t)
|
|
1556
|
+
|
|
1557
|
+
// Apply filters
|
|
1558
|
+
if (query) {
|
|
1559
|
+
products = products.filter(p =>
|
|
1560
|
+
p.name.toLowerCase().includes(query)
|
|
1561
|
+
)
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
if (category) {
|
|
1565
|
+
products = products.filter(p => p.category === category)
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
if (!showOutOfStock) {
|
|
1569
|
+
products = products.filter(p => p.inStock)
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
return products
|
|
1573
|
+
})
|
|
1574
|
+
|
|
1575
|
+
const $resultCount = derivation((t) => {
|
|
1576
|
+
return $filteredProducts.get(t).length
|
|
1577
|
+
})
|
|
1578
|
+
|
|
1579
|
+
// Display results
|
|
1580
|
+
effect((t) => {
|
|
1581
|
+
const products = $filteredProducts.get(t)
|
|
1582
|
+
const count = $resultCount.get(t)
|
|
1583
|
+
|
|
1584
|
+
console.log(`Found ${count} products`)
|
|
1585
|
+
renderProducts(products)
|
|
1586
|
+
})
|
|
1587
|
+
|
|
1588
|
+
// Update filters
|
|
1589
|
+
function updateSearch(query: string) {
|
|
1590
|
+
$searchQuery.set(query)
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
function selectCategory(category: string | null) {
|
|
1594
|
+
$selectedCategory.set(category)
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
function toggleOutOfStock() {
|
|
1598
|
+
$showOutOfStock.set(value => !value)
|
|
1599
|
+
}
|
|
1600
|
+
```
|
|
1601
|
+
|
|
1602
|
+
**Use Case:** Product catalogs, data tables with filters, search interfaces with multiple criteria.
|
|
1603
|
+
|
|
1604
|
+
---
|
|
1605
|
+
|
|
1606
|
+
## Example 16: Form Validation with Derivations
|
|
1607
|
+
|
|
1608
|
+
Real-time form validation using derivations:
|
|
1609
|
+
|
|
1610
|
+
```typescript
|
|
1611
|
+
const $email = state('')
|
|
1612
|
+
const $password = state('')
|
|
1613
|
+
const $confirmPassword = state('')
|
|
1614
|
+
|
|
1615
|
+
const $emailError = derivation((t) => {
|
|
1616
|
+
const email = $email.get(t)
|
|
1617
|
+
if (!email) return null
|
|
1618
|
+
if (!email.includes('@')) return 'Email must contain @'
|
|
1619
|
+
if (email.length < 5) return 'Email too short'
|
|
1620
|
+
return null
|
|
1621
|
+
})
|
|
1622
|
+
|
|
1623
|
+
const $passwordError = derivation((t) => {
|
|
1624
|
+
const password = $password.get(t)
|
|
1625
|
+
if (!password) return null
|
|
1626
|
+
if (password.length < 8) return 'Password must be at least 8 characters'
|
|
1627
|
+
return null
|
|
1628
|
+
})
|
|
1629
|
+
|
|
1630
|
+
const $confirmError = derivation((t) => {
|
|
1631
|
+
const password = $password.get(t)
|
|
1632
|
+
const confirm = $confirmPassword.get(t)
|
|
1633
|
+
if (!confirm) return null
|
|
1634
|
+
if (password !== confirm) return 'Passwords must match'
|
|
1635
|
+
return null
|
|
1636
|
+
})
|
|
1637
|
+
|
|
1638
|
+
const $isValid = derivation((t) => {
|
|
1639
|
+
return !$emailError.get(t) &&
|
|
1640
|
+
!$passwordError.get(t) &&
|
|
1641
|
+
!$confirmError.get(t)
|
|
1642
|
+
})
|
|
1643
|
+
|
|
1644
|
+
// Update submit button
|
|
1645
|
+
effect((t) => {
|
|
1646
|
+
const valid = $isValid.get(t)
|
|
1647
|
+
const submitButton = document.getElementById('submit') as HTMLButtonElement
|
|
1648
|
+
submitButton.disabled = !valid
|
|
1649
|
+
})
|
|
1650
|
+
|
|
1651
|
+
// Show email error
|
|
1652
|
+
effect((t) => {
|
|
1653
|
+
const error = $emailError.get(t)
|
|
1654
|
+
const errorEl = document.getElementById('email-error')
|
|
1655
|
+
errorEl.textContent = error || ''
|
|
1656
|
+
errorEl.style.display = error ? 'block' : 'none'
|
|
1657
|
+
})
|
|
1658
|
+
|
|
1659
|
+
// Show password error
|
|
1660
|
+
effect((t) => {
|
|
1661
|
+
const error = $passwordError.get(t)
|
|
1662
|
+
const errorEl = document.getElementById('password-error')
|
|
1663
|
+
errorEl.textContent = error || ''
|
|
1664
|
+
errorEl.style.display = error ? 'block' : 'none'
|
|
1665
|
+
})
|
|
1666
|
+
|
|
1667
|
+
// Show confirm error
|
|
1668
|
+
effect((t) => {
|
|
1669
|
+
const error = $confirmError.get(t)
|
|
1670
|
+
const errorEl = document.getElementById('confirm-error')
|
|
1671
|
+
errorEl.textContent = error || ''
|
|
1672
|
+
errorEl.style.display = error ? 'block' : 'none'
|
|
1673
|
+
})
|
|
1674
|
+
|
|
1675
|
+
// Form submission
|
|
1676
|
+
async function handleSubmit() {
|
|
1677
|
+
if (!$isValid.pick()) return
|
|
1678
|
+
|
|
1679
|
+
const email = $email.pick()
|
|
1680
|
+
const password = $password.pick()
|
|
1681
|
+
|
|
1682
|
+
await registerUser(email, password)
|
|
1683
|
+
}
|
|
1684
|
+
```
|
|
1685
|
+
|
|
1686
|
+
**Use Case:** Registration forms, settings panels, any form requiring real-time validation feedback.
|
|
1687
|
+
|
|
1688
|
+
---
|
|
1689
|
+
|
|
1690
|
+
## Example 17: Console Logging with Effects
|
|
1691
|
+
|
|
1692
|
+
Simple reactive console logging:
|
|
1693
|
+
|
|
1694
|
+
```typescript
|
|
1695
|
+
import { state, effect } from '@ersbeth/picoflow'
|
|
1696
|
+
|
|
1697
|
+
const $temperature = state(20)
|
|
1698
|
+
|
|
1699
|
+
effect((t) => {
|
|
1700
|
+
const temp = $temperature.get(t)
|
|
1701
|
+
console.log(`Temperature: ${temp}°C`)
|
|
1702
|
+
})
|
|
1703
|
+
|
|
1704
|
+
$temperature.set(25) // Logs: "Temperature: 25°C"
|
|
1705
|
+
```
|
|
1706
|
+
|
|
1707
|
+
**Use Case:** Development debugging, tracking state changes, monitoring application behavior.
|
|
1708
|
+
|
|
1709
|
+
---
|
|
1710
|
+
|
|
1711
|
+
## Example 18: Document Title Updates
|
|
1712
|
+
|
|
1713
|
+
Update the browser document title reactively:
|
|
1714
|
+
|
|
1715
|
+
```typescript
|
|
1716
|
+
import { state, derivation, effect } from '@ersbeth/picoflow'
|
|
1717
|
+
|
|
1718
|
+
const $pageName = state('Home')
|
|
1719
|
+
const $unreadCount = state(0)
|
|
1720
|
+
|
|
1721
|
+
const $title = derivation((t) => {
|
|
1722
|
+
const page = $pageName.get(t)
|
|
1723
|
+
const unread = $unreadCount.get(t)
|
|
1724
|
+
|
|
1725
|
+
return unread > 0
|
|
1726
|
+
? `(${unread}) ${page}`
|
|
1727
|
+
: page
|
|
1728
|
+
})
|
|
1729
|
+
|
|
1730
|
+
effect((t) => {
|
|
1731
|
+
document.title = $title.get(t)
|
|
1732
|
+
})
|
|
1733
|
+
|
|
1734
|
+
$pageName.set('Messages') // Title: "Messages"
|
|
1735
|
+
$unreadCount.set(5) // Title: "(5) Messages"
|
|
1736
|
+
```
|
|
1737
|
+
|
|
1738
|
+
**Use Case:** Dynamic page titles, notification badges in browser tabs, user feedback.
|
|
1739
|
+
|
|
1740
|
+
---
|
|
1741
|
+
|
|
1742
|
+
## Example 19: Form Validation Feedback
|
|
1743
|
+
|
|
1744
|
+
Real-time form validation with visual feedback:
|
|
1745
|
+
|
|
1746
|
+
```typescript
|
|
1747
|
+
import { state, derivation, effect } from '@ersbeth/picoflow'
|
|
1748
|
+
|
|
1749
|
+
const $email = state('')
|
|
1750
|
+
const $password = state('')
|
|
1751
|
+
|
|
1752
|
+
const $errors = derivation((t) => {
|
|
1753
|
+
const email = $email.get(t)
|
|
1754
|
+
const password = $password.get(t)
|
|
1755
|
+
|
|
1756
|
+
const errors = []
|
|
1757
|
+
if (email && !email.includes('@')) {
|
|
1758
|
+
errors.push('Invalid email')
|
|
1759
|
+
}
|
|
1760
|
+
if (password && password.length < 8) {
|
|
1761
|
+
errors.push('Password too short')
|
|
1762
|
+
}
|
|
1763
|
+
return errors
|
|
1764
|
+
})
|
|
1765
|
+
|
|
1766
|
+
// Update error display
|
|
1767
|
+
effect((t) => {
|
|
1768
|
+
const errors = $errors.get(t)
|
|
1769
|
+
const errorDiv = document.getElementById('errors')
|
|
1770
|
+
errorDiv.innerHTML = errors.map(e => `<div>${e}</div>`).join('')
|
|
1771
|
+
})
|
|
1772
|
+
```
|
|
1773
|
+
|
|
1774
|
+
**Use Case:** Real-time form validation, user input feedback, accessibility improvements.
|
|
1775
|
+
|
|
1776
|
+
---
|
|
1777
|
+
|
|
1778
|
+
## Example 20: Auto-save with Debouncing
|
|
1779
|
+
|
|
1780
|
+
Automatically save document changes with debouncing:
|
|
1781
|
+
|
|
1782
|
+
```typescript
|
|
1783
|
+
import { state, signal, effect } from '@ersbeth/picoflow'
|
|
1784
|
+
|
|
1785
|
+
const $document = state({ title: '', content: '' })
|
|
1786
|
+
const $saveStatus = state<'saved' | 'saving' | 'unsaved'>('saved')
|
|
1787
|
+
|
|
1788
|
+
// Debounced save effect (simplified)
|
|
1789
|
+
let saveTimeout: number | null = null
|
|
1790
|
+
|
|
1791
|
+
effect((t) => {
|
|
1792
|
+
const doc = $document.get(t)
|
|
1793
|
+
|
|
1794
|
+
// Clear previous timeout
|
|
1795
|
+
if (saveTimeout) clearTimeout(saveTimeout)
|
|
1796
|
+
|
|
1797
|
+
$saveStatus.set('unsaved')
|
|
1798
|
+
|
|
1799
|
+
// Debounce save
|
|
1800
|
+
saveTimeout = setTimeout(() => {
|
|
1801
|
+
$saveStatus.set('saving')
|
|
1802
|
+
saveToServer(doc).then(() => {
|
|
1803
|
+
$saveStatus.set('saved')
|
|
1804
|
+
})
|
|
1805
|
+
}, 1000)
|
|
1806
|
+
})
|
|
1807
|
+
|
|
1808
|
+
// Show save status
|
|
1809
|
+
effect((t) => {
|
|
1810
|
+
const status = $saveStatus.get(t)
|
|
1811
|
+
document.getElementById('status').textContent = status
|
|
1812
|
+
})
|
|
1813
|
+
```
|
|
1814
|
+
|
|
1815
|
+
**Use Case:** Text editors, draft saving, preventing data loss, user experience improvements.
|
|
1816
|
+
|
|
1817
|
+
---
|
|
1818
|
+
|
|
1819
|
+
## Example 21: User Profile Display
|
|
1820
|
+
|
|
1821
|
+
Complete user profile management with multiple effects:
|
|
1822
|
+
|
|
1823
|
+
```typescript
|
|
1824
|
+
import { state, derivation, effect } from '@ersbeth/picoflow'
|
|
1825
|
+
|
|
1826
|
+
// State
|
|
1827
|
+
const $user = state({
|
|
1828
|
+
firstName: 'Alice',
|
|
1829
|
+
lastName: 'Smith',
|
|
1830
|
+
email: 'alice@example.com'
|
|
1831
|
+
})
|
|
1832
|
+
|
|
1833
|
+
const $editMode = state(false)
|
|
1834
|
+
|
|
1835
|
+
// Derived values
|
|
1836
|
+
const $fullName = derivation((t) => {
|
|
1837
|
+
const user = $user.get(t)
|
|
1838
|
+
return `${user.firstName} ${user.lastName}`
|
|
1839
|
+
})
|
|
1840
|
+
|
|
1841
|
+
// Effects
|
|
1842
|
+
effect((t) => {
|
|
1843
|
+
const name = $fullName.get(t)
|
|
1844
|
+
document.getElementById('display-name').textContent = name
|
|
1845
|
+
})
|
|
1846
|
+
|
|
1847
|
+
effect((t) => {
|
|
1848
|
+
const user = $user.get(t)
|
|
1849
|
+
document.getElementById('email').textContent = user.email
|
|
1850
|
+
})
|
|
1851
|
+
|
|
1852
|
+
effect((t) => {
|
|
1853
|
+
const editMode = $editMode.get(t)
|
|
1854
|
+
document.body.classList.toggle('edit-mode', editMode)
|
|
1855
|
+
})
|
|
1856
|
+
|
|
1857
|
+
effect((t) => {
|
|
1858
|
+
const user = $user.get(t)
|
|
1859
|
+
localStorage.setItem('user', JSON.stringify(user))
|
|
1860
|
+
})
|
|
1861
|
+
|
|
1862
|
+
// Update functions
|
|
1863
|
+
function updateFirstName(name: string) {
|
|
1864
|
+
$user.set(user => ({ ...user, firstName: name }))
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
function toggleEditMode() {
|
|
1868
|
+
$editMode.set(mode => !mode)
|
|
1869
|
+
}
|
|
1870
|
+
```
|
|
1871
|
+
|
|
1872
|
+
**Use Case:** User profiles, settings panels, account management interfaces.
|
|
1873
|
+
|
|
1874
|
+
---
|
|
1875
|
+
|
|
1876
|
+
## Example 22: User List Management with Reactive Maps
|
|
1877
|
+
|
|
1878
|
+
A user management system using fine-grained map tracking to efficiently handle additions, updates, and deletions.
|
|
1879
|
+
|
|
1880
|
+
```typescript
|
|
1881
|
+
import { map, effect, derivation } from '@ersbeth/picoflow'
|
|
1882
|
+
|
|
1883
|
+
interface User {
|
|
1884
|
+
id: number
|
|
1885
|
+
name: string
|
|
1886
|
+
status: 'online' | 'offline'
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
const $users = map<number, User>()
|
|
1890
|
+
|
|
1891
|
+
// Add user to UI when added
|
|
1892
|
+
effect((t) => {
|
|
1893
|
+
const lastAdded = $users.$lastAdded.get(t)
|
|
1894
|
+
if (lastAdded) {
|
|
1895
|
+
addUserToDOM(lastAdded.value)
|
|
1896
|
+
}
|
|
1897
|
+
})
|
|
1898
|
+
|
|
1899
|
+
// Update user in UI when updated
|
|
1900
|
+
effect((t) => {
|
|
1901
|
+
const lastUpdated = $users.$lastUpdated.get(t)
|
|
1902
|
+
if (lastUpdated) {
|
|
1903
|
+
updateUserInDOM(lastUpdated.value)
|
|
1904
|
+
}
|
|
1905
|
+
})
|
|
1906
|
+
|
|
1907
|
+
// Remove user from UI when deleted
|
|
1908
|
+
effect((t) => {
|
|
1909
|
+
const lastDeleted = $users.$lastDeleted.get(t)
|
|
1910
|
+
if (lastDeleted) {
|
|
1911
|
+
removeUserFromDOM(lastDeleted.key)
|
|
1912
|
+
}
|
|
1913
|
+
})
|
|
1914
|
+
|
|
1915
|
+
// Update count
|
|
1916
|
+
const $userCount = derivation((t) => {
|
|
1917
|
+
const users = $users.get(t)
|
|
1918
|
+
return users.size
|
|
1919
|
+
})
|
|
1920
|
+
|
|
1921
|
+
// API calls
|
|
1922
|
+
function addUser(user: User) {
|
|
1923
|
+
$users.add(user.id, user)
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
function updateUser(user: User) {
|
|
1927
|
+
$users.update(user.id, user)
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
function removeUser(userId: number) {
|
|
1931
|
+
$users.delete(userId)
|
|
1932
|
+
}
|
|
1933
|
+
```
|
|
1934
|
+
|
|
1935
|
+
**Use Case:** User management interfaces, contact lists, real-time collaboration tools where individual user changes need to be tracked separately.
|
|
1936
|
+
|
|
1937
|
+
---
|
|
1938
|
+
|
|
1939
|
+
## Example 23: Shopping Cart with Reactive Maps
|
|
1940
|
+
|
|
1941
|
+
A shopping cart implementation using fine-grained map tracking for efficient UI updates.
|
|
1942
|
+
|
|
1943
|
+
```typescript
|
|
1944
|
+
import { map, derivation, effect } from '@ersbeth/picoflow'
|
|
1945
|
+
|
|
1946
|
+
interface CartItem {
|
|
1947
|
+
id: string
|
|
1948
|
+
name: string
|
|
1949
|
+
price: number
|
|
1950
|
+
quantity: number
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
const $cart = map<string, CartItem>()
|
|
1954
|
+
|
|
1955
|
+
// Update UI only for added items
|
|
1956
|
+
effect((t) => {
|
|
1957
|
+
const lastAdded = $cart.$lastAdded.get(t)
|
|
1958
|
+
if (lastAdded) {
|
|
1959
|
+
addCartItemToUI(lastAdded.value)
|
|
1960
|
+
}
|
|
1961
|
+
})
|
|
1962
|
+
|
|
1963
|
+
// Update UI only for updated items
|
|
1964
|
+
effect((t) => {
|
|
1965
|
+
const lastUpdated = $cart.$lastUpdated.get(t)
|
|
1966
|
+
if (lastUpdated) {
|
|
1967
|
+
updateCartItemInUI(lastUpdated.value)
|
|
1968
|
+
}
|
|
1969
|
+
})
|
|
1970
|
+
|
|
1971
|
+
// Remove item from UI
|
|
1972
|
+
effect((t) => {
|
|
1973
|
+
const lastDeleted = $cart.$lastDeleted.get(t)
|
|
1974
|
+
if (lastDeleted) {
|
|
1975
|
+
removeCartItemFromUI(lastDeleted.key)
|
|
1976
|
+
}
|
|
1977
|
+
})
|
|
1978
|
+
|
|
1979
|
+
// Computed totals
|
|
1980
|
+
const $subtotal = derivation((t) => {
|
|
1981
|
+
const cart = $cart.get(t)
|
|
1982
|
+
let sum = 0
|
|
1983
|
+
for (const item of cart.values()) {
|
|
1984
|
+
sum += item.price * item.quantity
|
|
1985
|
+
}
|
|
1986
|
+
return sum
|
|
1987
|
+
})
|
|
1988
|
+
|
|
1989
|
+
const $itemCount = derivation((t) => {
|
|
1990
|
+
const cart = $cart.get(t)
|
|
1991
|
+
let count = 0
|
|
1992
|
+
for (const item of cart.values()) {
|
|
1993
|
+
count += item.quantity
|
|
1994
|
+
}
|
|
1995
|
+
return count
|
|
1996
|
+
})
|
|
1997
|
+
|
|
1998
|
+
// Cart operations
|
|
1999
|
+
function addToCart(item: CartItem) {
|
|
2000
|
+
const existing = $cart.pick().get(item.id)
|
|
2001
|
+
if (existing) {
|
|
2002
|
+
$cart.update(item.id, {
|
|
2003
|
+
...existing,
|
|
2004
|
+
quantity: existing.quantity + 1
|
|
2005
|
+
})
|
|
2006
|
+
} else {
|
|
2007
|
+
$cart.add(item.id, item)
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
function removeFromCart(itemId: string) {
|
|
2012
|
+
$cart.delete(itemId)
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
function updateQuantity(itemId: string, quantity: number) {
|
|
2016
|
+
const item = $cart.pick().get(itemId)
|
|
2017
|
+
if (item) {
|
|
2018
|
+
if (quantity <= 0) {
|
|
2019
|
+
$cart.delete(itemId)
|
|
2020
|
+
} else {
|
|
2021
|
+
$cart.update(itemId, { ...item, quantity })
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
```
|
|
2026
|
+
|
|
2027
|
+
**Use Case:** E-commerce shopping carts, order management, inventory tracking with granular updates.
|
|
2028
|
+
|
|
2029
|
+
---
|
|
2030
|
+
|
|
2031
|
+
## Example 24: Entity Cache with LRU
|
|
2032
|
+
|
|
2033
|
+
An LRU (Least Recently Used) cache implementation using reactive maps and arrays.
|
|
2034
|
+
|
|
2035
|
+
```typescript
|
|
2036
|
+
import { map, array, effect } from '@ersbeth/picoflow'
|
|
2037
|
+
|
|
2038
|
+
const MAX_CACHE_SIZE = 100
|
|
2039
|
+
|
|
2040
|
+
const $entityCache = map<string, any>()
|
|
2041
|
+
const $accessOrder = array<string>([])
|
|
2042
|
+
|
|
2043
|
+
// Track cache additions
|
|
2044
|
+
effect((t) => {
|
|
2045
|
+
const lastAdded = $entityCache.$lastAdded.get(t)
|
|
2046
|
+
if (!lastAdded) return
|
|
2047
|
+
|
|
2048
|
+
// Update access order
|
|
2049
|
+
const order = $accessOrder.pick()
|
|
2050
|
+
const existingIndex = order.indexOf(lastAdded.key)
|
|
2051
|
+
|
|
2052
|
+
if (existingIndex !== -1) {
|
|
2053
|
+
$accessOrder.splice(existingIndex, 1)
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
$accessOrder.push(lastAdded.key)
|
|
2057
|
+
|
|
2058
|
+
// Evict oldest if over limit
|
|
2059
|
+
if ($accessOrder.pick().length > MAX_CACHE_SIZE) {
|
|
2060
|
+
const oldest = $accessOrder.shift()
|
|
2061
|
+
if (oldest) {
|
|
2062
|
+
$entityCache.delete(oldest)
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
})
|
|
2066
|
+
|
|
2067
|
+
// Track cache updates (move to end of access order)
|
|
2068
|
+
effect((t) => {
|
|
2069
|
+
const lastUpdated = $entityCache.$lastUpdated.get(t)
|
|
2070
|
+
if (!lastUpdated) return
|
|
2071
|
+
|
|
2072
|
+
const order = $accessOrder.pick()
|
|
2073
|
+
const index = order.indexOf(lastUpdated.key)
|
|
2074
|
+
if (index !== -1) {
|
|
2075
|
+
$accessOrder.splice(index, 1)
|
|
2076
|
+
$accessOrder.push(lastUpdated.key)
|
|
2077
|
+
}
|
|
2078
|
+
})
|
|
2079
|
+
|
|
2080
|
+
function getEntity(id: string) {
|
|
2081
|
+
const cached = $entityCache.pick().get(id)
|
|
2082
|
+
if (cached) {
|
|
2083
|
+
// Update access order
|
|
2084
|
+
const order = $accessOrder.pick()
|
|
2085
|
+
const index = order.indexOf(id)
|
|
2086
|
+
if (index !== -1) {
|
|
2087
|
+
$accessOrder.splice(index, 1)
|
|
2088
|
+
$accessOrder.push(id)
|
|
2089
|
+
}
|
|
2090
|
+
return cached
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
// Fetch and cache
|
|
2094
|
+
return fetchEntity(id).then(entity => {
|
|
2095
|
+
const existing = $entityCache.pick().has(id)
|
|
2096
|
+
if (existing) {
|
|
2097
|
+
$entityCache.update(id, entity)
|
|
2098
|
+
} else {
|
|
2099
|
+
$entityCache.add(id, entity)
|
|
2100
|
+
}
|
|
2101
|
+
return entity
|
|
2102
|
+
})
|
|
2103
|
+
}
|
|
2104
|
+
```
|
|
2105
|
+
|
|
2106
|
+
**Use Case:** API response caching, resource management, memory-efficient data stores with automatic eviction.
|
|
2107
|
+
|
|
2108
|
+
---
|
|
2109
|
+
|
|
2110
|
+
## Example 25: Todo List with Granular Array Updates
|
|
2111
|
+
|
|
2112
|
+
A todo list implementation using reactive arrays with fine-grained tracking for animations and efficient updates.
|
|
2113
|
+
|
|
2114
|
+
```typescript
|
|
2115
|
+
import { array, derivation, effect } from '@ersbeth/picoflow'
|
|
2116
|
+
|
|
2117
|
+
interface Todo {
|
|
2118
|
+
id: number
|
|
2119
|
+
text: string
|
|
2120
|
+
completed: boolean
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
const $todos = array<Todo>([])
|
|
2124
|
+
|
|
2125
|
+
// Animate additions
|
|
2126
|
+
effect((t) => {
|
|
2127
|
+
const action = $todos.$lastAction.get(t)
|
|
2128
|
+
if (action && action.type === 'push') {
|
|
2129
|
+
animateNewTodo(action.item)
|
|
2130
|
+
}
|
|
2131
|
+
})
|
|
2132
|
+
|
|
2133
|
+
// Animate removals
|
|
2134
|
+
effect((t) => {
|
|
2135
|
+
const action = $todos.$lastAction.get(t)
|
|
2136
|
+
if (action && action.type === 'splice') {
|
|
2137
|
+
animateRemovedTodos(action.start, action.deleteCount)
|
|
2138
|
+
}
|
|
2139
|
+
})
|
|
2140
|
+
|
|
2141
|
+
// Computed counts
|
|
2142
|
+
const $totalCount = derivation((t) => {
|
|
2143
|
+
return $todos.get(t).length
|
|
2144
|
+
})
|
|
2145
|
+
|
|
2146
|
+
const $completedCount = derivation((t) => {
|
|
2147
|
+
return $todos.get(t).filter(t => t.completed).length
|
|
2148
|
+
})
|
|
2149
|
+
|
|
2150
|
+
const $activeCount = derivation((t) => {
|
|
2151
|
+
return $todos.get(t).filter(t => !t.completed).length
|
|
2152
|
+
})
|
|
2153
|
+
|
|
2154
|
+
// Todo operations
|
|
2155
|
+
function addTodo(text: string) {
|
|
2156
|
+
$todos.push({
|
|
2157
|
+
id: Date.now(),
|
|
2158
|
+
text,
|
|
2159
|
+
completed: false
|
|
2160
|
+
})
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
function removeTodo(id: number) {
|
|
2164
|
+
const todos = $todos.pick()
|
|
2165
|
+
const index = todos.findIndex(t => t.id === id)
|
|
2166
|
+
if (index !== -1) {
|
|
2167
|
+
$todos.splice(index, 1)
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
function toggleTodo(id: number) {
|
|
2172
|
+
const todos = $todos.pick()
|
|
2173
|
+
const index = todos.findIndex(t => t.id === id)
|
|
2174
|
+
if (index !== -1) {
|
|
2175
|
+
const todo = todos[index]
|
|
2176
|
+
$todos.splice(index, 1, {
|
|
2177
|
+
...todo,
|
|
2178
|
+
completed: !todo.completed
|
|
2179
|
+
})
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
```
|
|
2183
|
+
|
|
2184
|
+
**Use Case:** Todo applications, task management systems, lists with animations and granular UI updates.
|
|
2185
|
+
|
|
2186
|
+
---
|
|
2187
|
+
|
|
2188
|
+
## Example 26: Collaborative Task Board with Arrays
|
|
2189
|
+
|
|
2190
|
+
A collaborative task board using multiple reactive arrays for different status columns with fine-grained tracking.
|
|
2191
|
+
|
|
2192
|
+
```typescript
|
|
2193
|
+
import { array, derivation, effect } from '@ersbeth/picoflow'
|
|
2194
|
+
|
|
2195
|
+
interface Task {
|
|
2196
|
+
id: string
|
|
2197
|
+
title: string
|
|
2198
|
+
assignee: string
|
|
2199
|
+
status: 'todo' | 'doing' | 'done'
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
// Tasks by status column
|
|
2203
|
+
const $todoTasks = array<Task>([])
|
|
2204
|
+
const $doingTasks = array<Task>([])
|
|
2205
|
+
const $doneTasks = array<Task>([])
|
|
2206
|
+
|
|
2207
|
+
// Track task additions for animations
|
|
2208
|
+
effect((t) => {
|
|
2209
|
+
const action = $todoTasks.$lastAction.get(t)
|
|
2210
|
+
if (action && action.type === 'push') {
|
|
2211
|
+
animateTaskAddition('todo', action.item)
|
|
2212
|
+
}
|
|
2213
|
+
})
|
|
2214
|
+
|
|
2215
|
+
effect((t) => {
|
|
2216
|
+
const action = $doingTasks.$lastAction.get(t)
|
|
2217
|
+
if (action && action.type === 'push') {
|
|
2218
|
+
animateTaskAddition('doing', action.item)
|
|
2219
|
+
}
|
|
2220
|
+
})
|
|
2221
|
+
|
|
2222
|
+
effect((t) => {
|
|
2223
|
+
const action = $doneTasks.$lastAction.get(t)
|
|
2224
|
+
if (action && action.type === 'push') {
|
|
2225
|
+
animateTaskAddition('done', action.item)
|
|
2226
|
+
}
|
|
2227
|
+
})
|
|
2228
|
+
|
|
2229
|
+
// Track removals for cleanup
|
|
2230
|
+
effect((t) => {
|
|
2231
|
+
const action = $todoTasks.$lastAction.get(t)
|
|
2232
|
+
if (action && action.type === 'splice') {
|
|
2233
|
+
animateTaskRemoval('todo', action.start)
|
|
2234
|
+
}
|
|
2235
|
+
})
|
|
2236
|
+
|
|
2237
|
+
// Computed statistics
|
|
2238
|
+
const $totalTasks = derivation((t) => {
|
|
2239
|
+
return $todoTasks.get(t).length +
|
|
2240
|
+
$doingTasks.get(t).length +
|
|
2241
|
+
$doneTasks.get(t).length
|
|
2242
|
+
})
|
|
2243
|
+
|
|
2244
|
+
const $completionRate = derivation((t) => {
|
|
2245
|
+
const total = $totalTasks.get(t)
|
|
2246
|
+
const done = $doneTasks.get(t).length
|
|
2247
|
+
return total > 0 ? (done / total) * 100 : 0
|
|
2248
|
+
})
|
|
2249
|
+
|
|
2250
|
+
// Operations
|
|
2251
|
+
function addTask(task: Task) {
|
|
2252
|
+
switch (task.status) {
|
|
2253
|
+
case 'todo':
|
|
2254
|
+
$todoTasks.push(task)
|
|
2255
|
+
break
|
|
2256
|
+
case 'doing':
|
|
2257
|
+
$doingTasks.push(task)
|
|
2258
|
+
break
|
|
2259
|
+
case 'done':
|
|
2260
|
+
$doneTasks.push(task)
|
|
2261
|
+
break
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2265
|
+
function moveTask(taskId: string, from: string, to: string) {
|
|
2266
|
+
const sourceArray = getArrayForStatus(from)
|
|
2267
|
+
const destArray = getArrayForStatus(to)
|
|
2268
|
+
|
|
2269
|
+
const tasks = sourceArray.pick()
|
|
2270
|
+
const index = tasks.findIndex(t => t.id === taskId)
|
|
2271
|
+
|
|
2272
|
+
if (index !== -1) {
|
|
2273
|
+
const task = tasks[index]
|
|
2274
|
+
sourceArray.splice(index, 1)
|
|
2275
|
+
destArray.push({ ...task, status: to as Task['status'] })
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
function getArrayForStatus(status: string) {
|
|
2280
|
+
switch (status) {
|
|
2281
|
+
case 'todo': return $todoTasks
|
|
2282
|
+
case 'doing': return $doingTasks
|
|
2283
|
+
case 'done': return $doneTasks
|
|
2284
|
+
default: throw new Error('Invalid status')
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
// Display stats
|
|
2289
|
+
effect((t) => {
|
|
2290
|
+
const total = $totalTasks.get(t)
|
|
2291
|
+
const rate = $completionRate.get(t)
|
|
2292
|
+
|
|
2293
|
+
updateStatsDisplay(total, rate)
|
|
2294
|
+
})
|
|
2295
|
+
```
|
|
2296
|
+
|
|
2297
|
+
**Use Case:** Kanban boards, project management tools, collaborative task tracking with real-time updates and animations.
|
|
2298
|
+
|
|
2299
|
+
---
|
|
2300
|
+
|
|
2301
|
+
## Next Steps
|
|
2302
|
+
|
|
2303
|
+
These examples demonstrate how PicoFlow primitives work together in real applications. For more information:
|
|
2304
|
+
|
|
2305
|
+
- [State](/guide/primitives/state) - Managing reactive values
|
|
2306
|
+
- [Signals](/guide/primitives/signal) - Event notifications
|
|
2307
|
+
- [Effects](/guide/primitives/effects) - Handling side effects
|
|
2308
|
+
- [Derivations](/guide/primitives/derivations) - Computing derived values
|
|
2309
|
+
- [Resources](/guide/primitives/resources) - Fetching data
|
|
2310
|
+
- [Streams](/guide/primitives/streams) - Handling events
|
|
2311
|
+
- [Reactive Maps](/guide/primitives/map) - Fine-grained map tracking
|
|
2312
|
+
- [Reactive Arrays](/guide/primitives/array) - Fine-grained array tracking
|
|
2313
|
+
- [API Reference](/api/) - Complete API documentation
|