@view-models/core 2.1.1 → 3.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/README.md +41 -33
- package/dist/index.d.ts +181 -3
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -0
- package/package.json +8 -2
- package/src/ViewModel.ts +6 -4
- package/src/{ViewModelWithDerivedState.ts → ViewModelWithComputedState.ts} +19 -31
- package/src/index.ts +1 -1
- package/dist/ViewModel.d.ts +0 -40
- package/dist/ViewModel.d.ts.map +0 -1
- package/dist/ViewModel.js +0 -43
- package/dist/ViewModelWithDerivedState.d.ts +0 -151
- package/dist/ViewModelWithDerivedState.d.ts.map +0 -1
- package/dist/ViewModelWithDerivedState.js +0 -126
- package/dist/index.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# @view-models/core
|
|
2
2
|
|
|
3
|
+
[](https://github.com/sunesimonsen/view-models-core/actions/workflows/ci.yml)
|
|
4
|
+
[](https://unpkg.com/@view-models/core@2.2.0/dist/index.js)
|
|
5
|
+
|
|
3
6
|
A lightweight, framework-agnostic library for building reactive view models with TypeScript. Separate your business logic from your UI framework with a simple, testable pattern.
|
|
4
7
|
|
|
5
8
|

|
|
@@ -31,16 +34,20 @@ type CounterState = {
|
|
|
31
34
|
};
|
|
32
35
|
|
|
33
36
|
class CounterViewModel extends ViewModel<CounterState> {
|
|
37
|
+
constructor() {
|
|
38
|
+
super({ count: 0 });
|
|
39
|
+
}
|
|
40
|
+
|
|
34
41
|
increment() {
|
|
35
|
-
|
|
36
|
-
count: count + 1,
|
|
37
|
-
})
|
|
42
|
+
super.update({
|
|
43
|
+
count: super.state.count + 1,
|
|
44
|
+
});
|
|
38
45
|
}
|
|
39
46
|
|
|
40
47
|
decrement() {
|
|
41
|
-
|
|
42
|
-
count: count - 1,
|
|
43
|
-
})
|
|
48
|
+
super.update({
|
|
49
|
+
count: super.state.count - 1,
|
|
50
|
+
});
|
|
44
51
|
}
|
|
45
52
|
}
|
|
46
53
|
```
|
|
@@ -77,10 +84,10 @@ describe("CounterViewModel", () => {
|
|
|
77
84
|
|
|
78
85
|
## View Models with Derived State
|
|
79
86
|
|
|
80
|
-
When you need to compute derived values from your state (like counts, filtered lists, or formatted data), use `
|
|
87
|
+
When you need to compute derived values from your state (like counts, filtered lists, or formatted data), use `ViewModelWithComputedState`:
|
|
81
88
|
|
|
82
89
|
```typescript
|
|
83
|
-
import {
|
|
90
|
+
import { ViewModelWithComputedState } from "@view-models/core";
|
|
84
91
|
|
|
85
92
|
type TodoState = {
|
|
86
93
|
items: Array<{ id: string; text: string; done: boolean }>;
|
|
@@ -92,7 +99,7 @@ type TodoDerivedState = TodoState & {
|
|
|
92
99
|
remainingCount: number;
|
|
93
100
|
};
|
|
94
101
|
|
|
95
|
-
class TodoViewModel extends
|
|
102
|
+
class TodoViewModel extends ViewModelWithComputedState<
|
|
96
103
|
TodoState,
|
|
97
104
|
TodoDerivedState
|
|
98
105
|
> {
|
|
@@ -100,7 +107,7 @@ class TodoViewModel extends ViewModelWithDerivedState<
|
|
|
100
107
|
super({ items: [] });
|
|
101
108
|
}
|
|
102
109
|
|
|
103
|
-
|
|
110
|
+
computedState({ items }: TodoState): TodoDerivedState {
|
|
104
111
|
return {
|
|
105
112
|
items,
|
|
106
113
|
totalCount: items.length,
|
|
@@ -110,17 +117,20 @@ class TodoViewModel extends ViewModelWithDerivedState<
|
|
|
110
117
|
}
|
|
111
118
|
|
|
112
119
|
addTodo(text: string) {
|
|
113
|
-
|
|
114
|
-
items: [
|
|
115
|
-
|
|
120
|
+
super.update({
|
|
121
|
+
items: [
|
|
122
|
+
...super.state.items,
|
|
123
|
+
{ id: crypto.randomUUID(), text, done: false },
|
|
124
|
+
],
|
|
125
|
+
});
|
|
116
126
|
}
|
|
117
127
|
|
|
118
128
|
toggleTodo(id: string) {
|
|
119
|
-
|
|
120
|
-
items: items.map((item) =>
|
|
129
|
+
super.update({
|
|
130
|
+
items: super.state.items.map((item) =>
|
|
121
131
|
item.id === id ? { ...item, done: !item.done } : item,
|
|
122
132
|
),
|
|
123
|
-
})
|
|
133
|
+
});
|
|
124
134
|
}
|
|
125
135
|
}
|
|
126
136
|
|
|
@@ -170,16 +180,14 @@ Always return new state objects from your updater functions:
|
|
|
170
180
|
|
|
171
181
|
```typescript
|
|
172
182
|
// Good
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}));
|
|
183
|
+
super.update({
|
|
184
|
+
count: super.state.count + 1,
|
|
185
|
+
});
|
|
177
186
|
|
|
178
187
|
// Bad - mutates existing state
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
});
|
|
188
|
+
const state = super.state;
|
|
189
|
+
state.count++;
|
|
190
|
+
super.update(state);
|
|
183
191
|
```
|
|
184
192
|
|
|
185
193
|
### Use Readonly Types
|
|
@@ -212,29 +220,29 @@ actions:
|
|
|
212
220
|
|
|
213
221
|
```typescript
|
|
214
222
|
class TodosViewModel extends ViewModel<TodosState> {
|
|
215
|
-
private api: API
|
|
223
|
+
private api: API;
|
|
216
224
|
|
|
217
225
|
constructor(state: TodosState, api: API) {
|
|
218
|
-
super(state)
|
|
219
|
-
this.api = api
|
|
226
|
+
super(state);
|
|
227
|
+
this.api = api;
|
|
220
228
|
}
|
|
221
229
|
|
|
222
230
|
async loadTodos() {
|
|
223
|
-
|
|
231
|
+
super.update({ loading: true, failed: false });
|
|
224
232
|
try {
|
|
225
233
|
const todos = await this.api.fetchTodos();
|
|
226
|
-
|
|
234
|
+
super.update({ todos, loading: false });
|
|
227
235
|
} catch {
|
|
228
|
-
|
|
236
|
+
super.update({ loading: false, failed: true });
|
|
229
237
|
}
|
|
230
238
|
}
|
|
231
239
|
|
|
232
240
|
async addTodo(text: string) {
|
|
233
241
|
try {
|
|
234
242
|
const todo = await this.api.createTodo(text);
|
|
235
|
-
|
|
236
|
-
todos: [...todos, todo],
|
|
237
|
-
})
|
|
243
|
+
super.update({
|
|
244
|
+
todos: [...super.state.todos, todo],
|
|
245
|
+
});
|
|
238
246
|
} catch {
|
|
239
247
|
// TODO show error
|
|
240
248
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,181 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Function that gets called when the state changes.
|
|
3
|
+
*
|
|
4
|
+
* @template T - The state type
|
|
5
|
+
*/
|
|
6
|
+
type ViewModelListener = () => void;
|
|
7
|
+
/**
|
|
8
|
+
* Abstract base class for creating reactive view models with derived state.
|
|
9
|
+
*
|
|
10
|
+
* This class extends the basic ViewModel pattern by allowing you to compute
|
|
11
|
+
* derived state from internal state. The internal state is managed privately,
|
|
12
|
+
* while the derived state (which can include computed properties) is exposed
|
|
13
|
+
* to subscribers.
|
|
14
|
+
*
|
|
15
|
+
* @template S - The internal state type (managed internally)
|
|
16
|
+
* @template D - The derived state type (exposed to subscribers)
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* type TodoState = {
|
|
21
|
+
* items: Array<{ id: string; text: string; done: boolean }>;
|
|
22
|
+
* };
|
|
23
|
+
*
|
|
24
|
+
* type TodoDerivedState = TodoState & {
|
|
25
|
+
* totalCount: number;
|
|
26
|
+
* completedCount: number;
|
|
27
|
+
* remainingCount: number;
|
|
28
|
+
* };
|
|
29
|
+
*
|
|
30
|
+
* class TodoViewModel extends ViewModelWithComputedState<TodoState, TodoDerivedState> {
|
|
31
|
+
* constructor() {
|
|
32
|
+
* super({ items: [] });
|
|
33
|
+
* }
|
|
34
|
+
*
|
|
35
|
+
* computedState({ items }: TodoState): TodoDerivedState {
|
|
36
|
+
* return {
|
|
37
|
+
* items,
|
|
38
|
+
* totalCount: items.length,
|
|
39
|
+
* completedCount: items.filter(item => item.done).length,
|
|
40
|
+
* remainingCount: items.filter(item => !item.done).length,
|
|
41
|
+
* };
|
|
42
|
+
* }
|
|
43
|
+
*
|
|
44
|
+
* addTodo(text: string) {
|
|
45
|
+
* super.update({
|
|
46
|
+
* items: [...super.state.items, { id: crypto.randomUUID(), text, done: false }],
|
|
47
|
+
* });
|
|
48
|
+
* }
|
|
49
|
+
* }
|
|
50
|
+
*
|
|
51
|
+
* const todos = new TodoViewModel();
|
|
52
|
+
* todos.subscribe(() => {
|
|
53
|
+
* console.log('Completed:', todos.state.completedCount);
|
|
54
|
+
* });
|
|
55
|
+
* todos.addTodo('Learn ViewModels'); // Logs: Completed: 0
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
declare abstract class ViewModelWithComputedState<S extends object, D> {
|
|
59
|
+
private _listeners;
|
|
60
|
+
private _internalState;
|
|
61
|
+
private _state;
|
|
62
|
+
/**
|
|
63
|
+
* Subscribe to state changes.
|
|
64
|
+
*
|
|
65
|
+
* The listener will be called immediately after any state update.
|
|
66
|
+
*
|
|
67
|
+
* @param listener - Function to call when state changes
|
|
68
|
+
* @returns Function to unsubscribe the listener
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```typescript
|
|
72
|
+
* const unsubscribe = viewModel.subscribe((state) => {
|
|
73
|
+
* console.log('State changed:', state);
|
|
74
|
+
* });
|
|
75
|
+
*
|
|
76
|
+
* // Later, when you want to stop listening:
|
|
77
|
+
* unsubscribe();
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
subscribe(listener: ViewModelListener): () => void;
|
|
81
|
+
/**
|
|
82
|
+
* Create a new ViewModel with the given initial internal state.
|
|
83
|
+
*
|
|
84
|
+
* The constructor initializes the internal state and immediately computes
|
|
85
|
+
* the derived state by calling `computedState`.
|
|
86
|
+
*
|
|
87
|
+
* @param initialState - The initial internal state of the view model
|
|
88
|
+
*/
|
|
89
|
+
constructor(initialState: S);
|
|
90
|
+
/**
|
|
91
|
+
* Update the internal state, recompute derived state, and notify all subscribers.
|
|
92
|
+
*
|
|
93
|
+
* This method is protected and should only be called from within your view model subclass.
|
|
94
|
+
* The partial state is merged with the current internal state to create the new internal state.
|
|
95
|
+
* After updating, the derived state is automatically recomputed via `computeDerivedState`.
|
|
96
|
+
*
|
|
97
|
+
* @param partial - Partial state to merge with the current internal state
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```typescript
|
|
101
|
+
* super.update({
|
|
102
|
+
* count: super.state.count + 1
|
|
103
|
+
* });
|
|
104
|
+
* ```
|
|
105
|
+
*/
|
|
106
|
+
protected update(partial: Partial<S>): void;
|
|
107
|
+
/**
|
|
108
|
+
* Compute the derived state from the internal state.
|
|
109
|
+
*
|
|
110
|
+
* This abstract method must be implemented by subclasses to transform
|
|
111
|
+
* the internal state into the derived state that will be exposed to
|
|
112
|
+
* subscribers. This method is called automatically after each state
|
|
113
|
+
* update and during initialization.
|
|
114
|
+
*
|
|
115
|
+
* @param state - The current internal state
|
|
116
|
+
* @returns The derived state with any computed properties
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```typescript
|
|
120
|
+
* computedState({ count }: CounterState): CounterDerivedState {
|
|
121
|
+
* return {
|
|
122
|
+
* count,
|
|
123
|
+
* isEven: count % 2 === 0,
|
|
124
|
+
* isPositive: count > 0,
|
|
125
|
+
* };
|
|
126
|
+
* }
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
129
|
+
abstract computedState(state: S): D;
|
|
130
|
+
/**
|
|
131
|
+
* Get the current derived state.
|
|
132
|
+
*
|
|
133
|
+
* This returns the derived state computed by `computedState`,
|
|
134
|
+
* not the internal state.
|
|
135
|
+
*
|
|
136
|
+
* @returns The current derived state
|
|
137
|
+
*/
|
|
138
|
+
get state(): D;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Abstract base class for creating reactive view models.
|
|
143
|
+
*
|
|
144
|
+
* A ViewModel manages state and notifies subscribers when the state changes.
|
|
145
|
+
* Extend this class to create your own view models with custom business logic.
|
|
146
|
+
*
|
|
147
|
+
* @template S - The state type
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* ```typescript
|
|
151
|
+
* type CounterState = { count: number };
|
|
152
|
+
*
|
|
153
|
+
* class CounterViewModel extends ViewModel<CounterState> {
|
|
154
|
+
* constructor() {
|
|
155
|
+
* super({ count: 0 });
|
|
156
|
+
* }
|
|
157
|
+
*
|
|
158
|
+
* increment() {
|
|
159
|
+
* super.update({ count: super.state.count + 1 });
|
|
160
|
+
* }
|
|
161
|
+
* }
|
|
162
|
+
*
|
|
163
|
+
* const counter = new CounterViewModel();
|
|
164
|
+
* const unsubscribe = counter.subscribe((state) => {
|
|
165
|
+
* console.log('Count:', state.count);
|
|
166
|
+
* });
|
|
167
|
+
* counter.increment(); // Logs: Count: 1
|
|
168
|
+
* ```
|
|
169
|
+
*/
|
|
170
|
+
declare abstract class ViewModel<S extends object> extends ViewModelWithComputedState<S, S> {
|
|
171
|
+
/**
|
|
172
|
+
* Create a new ViewModel with the given initial state.
|
|
173
|
+
*
|
|
174
|
+
* @param initialState - The initial state of the view model
|
|
175
|
+
*/
|
|
176
|
+
constructor(initialState: S);
|
|
177
|
+
computedState(state: S): S;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export { ViewModel, ViewModelWithComputedState };
|
|
181
|
+
export type { ViewModelListener };
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
class t{subscribe(t){return this.t.add(t),()=>{this.t.delete(t)}}constructor(t){this.t=new Set,this.i=t,this.h=this.computedState(this.i)}update(t){this.i={...this.i,...t},this.h=this.computedState(this.i);for(const t of this.t)t()}get state(){return this.h}}class s extends t{constructor(t){super(t)}computedState(t){return t}}export{s as ViewModel,t as ViewModelWithComputedState};
|
|
2
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/ViewModelWithComputedState.ts","../src/ViewModel.ts"],"sourcesContent":["/**\n * Function that gets called when the state changes.\n *\n * @template T - The state type\n */\nexport type ViewModelListener = () => void;\n\n/**\n * Abstract base class for creating reactive view models with derived state.\n *\n * This class extends the basic ViewModel pattern by allowing you to compute\n * derived state from internal state. The internal state is managed privately,\n * while the derived state (which can include computed properties) is exposed\n * to subscribers.\n *\n * @template S - The internal state type (managed internally)\n * @template D - The derived state type (exposed to subscribers)\n *\n * @example\n * ```typescript\n * type TodoState = {\n * items: Array<{ id: string; text: string; done: boolean }>;\n * };\n *\n * type TodoDerivedState = TodoState & {\n * totalCount: number;\n * completedCount: number;\n * remainingCount: number;\n * };\n *\n * class TodoViewModel extends ViewModelWithComputedState<TodoState, TodoDerivedState> {\n * constructor() {\n * super({ items: [] });\n * }\n *\n * computedState({ items }: TodoState): TodoDerivedState {\n * return {\n * items,\n * totalCount: items.length,\n * completedCount: items.filter(item => item.done).length,\n * remainingCount: items.filter(item => !item.done).length,\n * };\n * }\n *\n * addTodo(text: string) {\n * super.update({\n * items: [...super.state.items, { id: crypto.randomUUID(), text, done: false }],\n * });\n * }\n * }\n *\n * const todos = new TodoViewModel();\n * todos.subscribe(() => {\n * console.log('Completed:', todos.state.completedCount);\n * });\n * todos.addTodo('Learn ViewModels'); // Logs: Completed: 0\n * ```\n */\nexport abstract class ViewModelWithComputedState<S extends object, D> {\n private _listeners: Set<ViewModelListener> = new Set();\n private _internalState: S;\n private _state: D;\n\n /**\n * Subscribe to state changes.\n *\n * The listener will be called immediately after any state update.\n *\n * @param listener - Function to call when state changes\n * @returns Function to unsubscribe the listener\n *\n * @example\n * ```typescript\n * const unsubscribe = viewModel.subscribe((state) => {\n * console.log('State changed:', state);\n * });\n *\n * // Later, when you want to stop listening:\n * unsubscribe();\n * ```\n */\n subscribe(listener: ViewModelListener): () => void {\n this._listeners.add(listener);\n return () => {\n this._listeners.delete(listener);\n };\n }\n\n /**\n * Create a new ViewModel with the given initial internal state.\n *\n * The constructor initializes the internal state and immediately computes\n * the derived state by calling `computedState`.\n *\n * @param initialState - The initial internal state of the view model\n */\n constructor(initialState: S) {\n this._internalState = initialState;\n this._state = this.computedState(this._internalState);\n }\n\n /**\n * Update the internal state, recompute derived state, and notify all subscribers.\n *\n * This method is protected and should only be called from within your view model subclass.\n * The partial state is merged with the current internal state to create the new internal state.\n * After updating, the derived state is automatically recomputed via `computeDerivedState`.\n *\n * @param partial - Partial state to merge with the current internal state\n *\n * @example\n * ```typescript\n * super.update({\n * count: super.state.count + 1\n * });\n * ```\n */\n protected update(partial: Partial<S>) {\n this._internalState = { ...this._internalState, ...partial };\n this._state = this.computedState(this._internalState);\n\n for (const listener of this._listeners) {\n listener();\n }\n }\n\n /**\n * Compute the derived state from the internal state.\n *\n * This abstract method must be implemented by subclasses to transform\n * the internal state into the derived state that will be exposed to\n * subscribers. This method is called automatically after each state\n * update and during initialization.\n *\n * @param state - The current internal state\n * @returns The derived state with any computed properties\n *\n * @example\n * ```typescript\n * computedState({ count }: CounterState): CounterDerivedState {\n * return {\n * count,\n * isEven: count % 2 === 0,\n * isPositive: count > 0,\n * };\n * }\n * ```\n */\n abstract computedState(state: S): D;\n\n /**\n * Get the current derived state.\n *\n * This returns the derived state computed by `computedState`,\n * not the internal state.\n *\n * @returns The current derived state\n */\n get state(): D {\n return this._state;\n }\n}\n","import { ViewModelWithComputedState } from \"./ViewModelWithComputedState.js\";\n\n/**\n * Abstract base class for creating reactive view models.\n *\n * A ViewModel manages state and notifies subscribers when the state changes.\n * Extend this class to create your own view models with custom business logic.\n *\n * @template S - The state type\n *\n * @example\n * ```typescript\n * type CounterState = { count: number };\n *\n * class CounterViewModel extends ViewModel<CounterState> {\n * constructor() {\n * super({ count: 0 });\n * }\n *\n * increment() {\n * super.update({ count: super.state.count + 1 });\n * }\n * }\n *\n * const counter = new CounterViewModel();\n * const unsubscribe = counter.subscribe((state) => {\n * console.log('Count:', state.count);\n * });\n * counter.increment(); // Logs: Count: 1\n * ```\n */\nexport abstract class ViewModel<\n S extends object,\n> extends ViewModelWithComputedState<S, S> {\n /**\n * Create a new ViewModel with the given initial state.\n *\n * @param initialState - The initial state of the view model\n */\n constructor(initialState: S) {\n super(initialState);\n }\n\n computedState(state: S): S {\n return state;\n }\n}\n"],"names":["ViewModelWithComputedState","subscribe","listener","this","_listeners","add","delete","constructor","initialState","Set","_internalState","_state","computedState","update","partial","state","ViewModel","super"],"mappings":"MA0DsBA,EAuBpB,SAAAC,CAAUC,GAER,OADAC,KAAKC,EAAWC,IAAIH,GACb,KACLC,KAAKC,EAAWE,OAAOJ,GAE3B,CAUA,WAAAK,CAAYC,GArCJL,KAAAC,EAAqC,IAAIK,IAsC/CN,KAAKO,EAAiBF,EACtBL,KAAKQ,EAASR,KAAKS,cAAcT,KAAKO,EACxC,CAkBU,MAAAG,CAAOC,GACfX,KAAKO,EAAiB,IAAKP,KAAKO,KAAmBI,GACnDX,KAAKQ,EAASR,KAAKS,cAAcT,KAAKO,GAEtC,IAAK,MAAMR,KAAYC,KAAKC,EAC1BF,GAEJ,CAkCA,SAAIa,GACF,OAAOZ,KAAKQ,CACd,ECjII,MAAgBK,UAEZhB,EAMR,WAAAO,CAAYC,GACVS,MAAMT,EACR,CAEA,aAAAI,CAAcG,GACZ,OAAOA,CACT"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@view-models/core",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "A lightweight, framework-agnostic library for building reactive view models with TypeScript",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"view",
|
|
@@ -36,7 +36,8 @@
|
|
|
36
36
|
"node": ">=18.0.0"
|
|
37
37
|
},
|
|
38
38
|
"scripts": {
|
|
39
|
-
"
|
|
39
|
+
"clean": "rm -rf dist",
|
|
40
|
+
"build": "npm run clean && rollup -c",
|
|
40
41
|
"typecheck": "tsc --noEmit",
|
|
41
42
|
"test": "vitest run",
|
|
42
43
|
"test:watch": "vitest",
|
|
@@ -47,8 +48,13 @@
|
|
|
47
48
|
"prepublishOnly": "npm run build"
|
|
48
49
|
},
|
|
49
50
|
"devDependencies": {
|
|
51
|
+
"@rollup/plugin-terser": "^0.4.4",
|
|
52
|
+
"@rollup/plugin-typescript": "^12.3.0",
|
|
50
53
|
"@vitest/coverage-v8": "^4.0.16",
|
|
51
54
|
"prettier": "^3.7.4",
|
|
55
|
+
"rollup": "^4.57.0",
|
|
56
|
+
"rollup-plugin-dts": "^6.3.0",
|
|
57
|
+
"tslib": "^2.8.1",
|
|
52
58
|
"typedoc": "^0.28.16",
|
|
53
59
|
"typedoc-plugin-markdown": "^4.9.0",
|
|
54
60
|
"typescript": "^5.7.3",
|
package/src/ViewModel.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ViewModelWithComputedState } from "./ViewModelWithComputedState.js";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Abstract base class for creating reactive view models.
|
|
@@ -18,7 +18,7 @@ import { ViewModelWithDerivedState } from "./ViewModelWithDerivedState.js";
|
|
|
18
18
|
* }
|
|
19
19
|
*
|
|
20
20
|
* increment() {
|
|
21
|
-
*
|
|
21
|
+
* super.update({ count: super.state.count + 1 });
|
|
22
22
|
* }
|
|
23
23
|
* }
|
|
24
24
|
*
|
|
@@ -29,7 +29,9 @@ import { ViewModelWithDerivedState } from "./ViewModelWithDerivedState.js";
|
|
|
29
29
|
* counter.increment(); // Logs: Count: 1
|
|
30
30
|
* ```
|
|
31
31
|
*/
|
|
32
|
-
export abstract class ViewModel<
|
|
32
|
+
export abstract class ViewModel<
|
|
33
|
+
S extends object,
|
|
34
|
+
> extends ViewModelWithComputedState<S, S> {
|
|
33
35
|
/**
|
|
34
36
|
* Create a new ViewModel with the given initial state.
|
|
35
37
|
*
|
|
@@ -39,7 +41,7 @@ export abstract class ViewModel<S> extends ViewModelWithDerivedState<S, S> {
|
|
|
39
41
|
super(initialState);
|
|
40
42
|
}
|
|
41
43
|
|
|
42
|
-
|
|
44
|
+
computedState(state: S): S {
|
|
43
45
|
return state;
|
|
44
46
|
}
|
|
45
47
|
}
|
|
@@ -1,13 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Function that receives the current state and returns the new state.
|
|
3
|
-
* The updater function should be pure and return a new state object.
|
|
4
|
-
*
|
|
5
|
-
* @template T - The state type
|
|
6
|
-
* @param currentState - The current state
|
|
7
|
-
* @returns The new state
|
|
8
|
-
*/
|
|
9
|
-
export type Updater<T> = (currentState: T) => T;
|
|
10
|
-
|
|
11
1
|
/**
|
|
12
2
|
* Function that gets called when the state changes.
|
|
13
3
|
*
|
|
@@ -38,12 +28,12 @@ export type ViewModelListener = () => void;
|
|
|
38
28
|
* remainingCount: number;
|
|
39
29
|
* };
|
|
40
30
|
*
|
|
41
|
-
* class TodoViewModel extends
|
|
31
|
+
* class TodoViewModel extends ViewModelWithComputedState<TodoState, TodoDerivedState> {
|
|
42
32
|
* constructor() {
|
|
43
33
|
* super({ items: [] });
|
|
44
34
|
* }
|
|
45
35
|
*
|
|
46
|
-
*
|
|
36
|
+
* computedState({ items }: TodoState): TodoDerivedState {
|
|
47
37
|
* return {
|
|
48
38
|
* items,
|
|
49
39
|
* totalCount: items.length,
|
|
@@ -53,9 +43,9 @@ export type ViewModelListener = () => void;
|
|
|
53
43
|
* }
|
|
54
44
|
*
|
|
55
45
|
* addTodo(text: string) {
|
|
56
|
-
*
|
|
57
|
-
* items: [...items, { id: crypto.randomUUID(), text, done: false }],
|
|
58
|
-
* })
|
|
46
|
+
* super.update({
|
|
47
|
+
* items: [...super.state.items, { id: crypto.randomUUID(), text, done: false }],
|
|
48
|
+
* });
|
|
59
49
|
* }
|
|
60
50
|
* }
|
|
61
51
|
*
|
|
@@ -66,7 +56,7 @@ export type ViewModelListener = () => void;
|
|
|
66
56
|
* todos.addTodo('Learn ViewModels'); // Logs: Completed: 0
|
|
67
57
|
* ```
|
|
68
58
|
*/
|
|
69
|
-
export abstract class
|
|
59
|
+
export abstract class ViewModelWithComputedState<S extends object, D> {
|
|
70
60
|
private _listeners: Set<ViewModelListener> = new Set();
|
|
71
61
|
private _internalState: S;
|
|
72
62
|
private _state: D;
|
|
@@ -100,36 +90,34 @@ export abstract class ViewModelWithDerivedState<S, D> {
|
|
|
100
90
|
* Create a new ViewModel with the given initial internal state.
|
|
101
91
|
*
|
|
102
92
|
* The constructor initializes the internal state and immediately computes
|
|
103
|
-
* the derived state by calling `
|
|
93
|
+
* the derived state by calling `computedState`.
|
|
104
94
|
*
|
|
105
95
|
* @param initialState - The initial internal state of the view model
|
|
106
96
|
*/
|
|
107
97
|
constructor(initialState: S) {
|
|
108
98
|
this._internalState = initialState;
|
|
109
|
-
this._state = this.
|
|
99
|
+
this._state = this.computedState(this._internalState);
|
|
110
100
|
}
|
|
111
101
|
|
|
112
102
|
/**
|
|
113
103
|
* Update the internal state, recompute derived state, and notify all subscribers.
|
|
114
104
|
*
|
|
115
105
|
* This method is protected and should only be called from within your view model subclass.
|
|
116
|
-
* The
|
|
106
|
+
* The partial state is merged with the current internal state to create the new internal state.
|
|
117
107
|
* After updating, the derived state is automatically recomputed via `computeDerivedState`.
|
|
118
|
-
* Always return a new state object to ensure immutability.
|
|
119
108
|
*
|
|
120
|
-
* @param
|
|
109
|
+
* @param partial - Partial state to merge with the current internal state
|
|
121
110
|
*
|
|
122
111
|
* @example
|
|
123
112
|
* ```typescript
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
* }));
|
|
113
|
+
* super.update({
|
|
114
|
+
* count: super.state.count + 1
|
|
115
|
+
* });
|
|
128
116
|
* ```
|
|
129
117
|
*/
|
|
130
|
-
protected update(
|
|
131
|
-
this._internalState =
|
|
132
|
-
this._state = this.
|
|
118
|
+
protected update(partial: Partial<S>) {
|
|
119
|
+
this._internalState = { ...this._internalState, ...partial };
|
|
120
|
+
this._state = this.computedState(this._internalState);
|
|
133
121
|
|
|
134
122
|
for (const listener of this._listeners) {
|
|
135
123
|
listener();
|
|
@@ -149,7 +137,7 @@ export abstract class ViewModelWithDerivedState<S, D> {
|
|
|
149
137
|
*
|
|
150
138
|
* @example
|
|
151
139
|
* ```typescript
|
|
152
|
-
*
|
|
140
|
+
* computedState({ count }: CounterState): CounterDerivedState {
|
|
153
141
|
* return {
|
|
154
142
|
* count,
|
|
155
143
|
* isEven: count % 2 === 0,
|
|
@@ -158,12 +146,12 @@ export abstract class ViewModelWithDerivedState<S, D> {
|
|
|
158
146
|
* }
|
|
159
147
|
* ```
|
|
160
148
|
*/
|
|
161
|
-
abstract
|
|
149
|
+
abstract computedState(state: S): D;
|
|
162
150
|
|
|
163
151
|
/**
|
|
164
152
|
* Get the current derived state.
|
|
165
153
|
*
|
|
166
|
-
* This returns the derived state computed by `
|
|
154
|
+
* This returns the derived state computed by `computedState`,
|
|
167
155
|
* not the internal state.
|
|
168
156
|
*
|
|
169
157
|
* @returns The current derived state
|
package/src/index.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export * from "./ViewModel.js";
|
|
2
|
-
export * from "./
|
|
2
|
+
export * from "./ViewModelWithComputedState.js";
|
package/dist/ViewModel.d.ts
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { ViewModelWithDerivedState } from "./ViewModelWithDerivedState.js";
|
|
2
|
-
/**
|
|
3
|
-
* Abstract base class for creating reactive view models.
|
|
4
|
-
*
|
|
5
|
-
* A ViewModel manages state and notifies subscribers when the state changes.
|
|
6
|
-
* Extend this class to create your own view models with custom business logic.
|
|
7
|
-
*
|
|
8
|
-
* @template S - The state type
|
|
9
|
-
*
|
|
10
|
-
* @example
|
|
11
|
-
* ```typescript
|
|
12
|
-
* type CounterState = { count: number };
|
|
13
|
-
*
|
|
14
|
-
* class CounterViewModel extends ViewModel<CounterState> {
|
|
15
|
-
* constructor() {
|
|
16
|
-
* super({ count: 0 });
|
|
17
|
-
* }
|
|
18
|
-
*
|
|
19
|
-
* increment() {
|
|
20
|
-
* this.update(({ count }) => ({ count: count + 1 }));
|
|
21
|
-
* }
|
|
22
|
-
* }
|
|
23
|
-
*
|
|
24
|
-
* const counter = new CounterViewModel();
|
|
25
|
-
* const unsubscribe = counter.subscribe((state) => {
|
|
26
|
-
* console.log('Count:', state.count);
|
|
27
|
-
* });
|
|
28
|
-
* counter.increment(); // Logs: Count: 1
|
|
29
|
-
* ```
|
|
30
|
-
*/
|
|
31
|
-
export declare abstract class ViewModel<S> extends ViewModelWithDerivedState<S, S> {
|
|
32
|
-
/**
|
|
33
|
-
* Create a new ViewModel with the given initial state.
|
|
34
|
-
*
|
|
35
|
-
* @param initialState - The initial state of the view model
|
|
36
|
-
*/
|
|
37
|
-
constructor(initialState: S);
|
|
38
|
-
computeDerivedState(state: S): S;
|
|
39
|
-
}
|
|
40
|
-
//# sourceMappingURL=ViewModel.d.ts.map
|
package/dist/ViewModel.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ViewModel.d.ts","sourceRoot":"","sources":["../src/ViewModel.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,yBAAyB,EAAE,MAAM,gCAAgC,CAAC;AAE3E;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,8BAAsB,SAAS,CAAC,CAAC,CAAE,SAAQ,yBAAyB,CAAC,CAAC,EAAE,CAAC,CAAC;IACxE;;;;OAIG;gBACS,YAAY,EAAE,CAAC;IAI3B,mBAAmB,CAAC,KAAK,EAAE,CAAC,GAAG,CAAC;CAGjC"}
|
package/dist/ViewModel.js
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { ViewModelWithDerivedState } from "./ViewModelWithDerivedState.js";
|
|
2
|
-
/**
|
|
3
|
-
* Abstract base class for creating reactive view models.
|
|
4
|
-
*
|
|
5
|
-
* A ViewModel manages state and notifies subscribers when the state changes.
|
|
6
|
-
* Extend this class to create your own view models with custom business logic.
|
|
7
|
-
*
|
|
8
|
-
* @template S - The state type
|
|
9
|
-
*
|
|
10
|
-
* @example
|
|
11
|
-
* ```typescript
|
|
12
|
-
* type CounterState = { count: number };
|
|
13
|
-
*
|
|
14
|
-
* class CounterViewModel extends ViewModel<CounterState> {
|
|
15
|
-
* constructor() {
|
|
16
|
-
* super({ count: 0 });
|
|
17
|
-
* }
|
|
18
|
-
*
|
|
19
|
-
* increment() {
|
|
20
|
-
* this.update(({ count }) => ({ count: count + 1 }));
|
|
21
|
-
* }
|
|
22
|
-
* }
|
|
23
|
-
*
|
|
24
|
-
* const counter = new CounterViewModel();
|
|
25
|
-
* const unsubscribe = counter.subscribe((state) => {
|
|
26
|
-
* console.log('Count:', state.count);
|
|
27
|
-
* });
|
|
28
|
-
* counter.increment(); // Logs: Count: 1
|
|
29
|
-
* ```
|
|
30
|
-
*/
|
|
31
|
-
export class ViewModel extends ViewModelWithDerivedState {
|
|
32
|
-
/**
|
|
33
|
-
* Create a new ViewModel with the given initial state.
|
|
34
|
-
*
|
|
35
|
-
* @param initialState - The initial state of the view model
|
|
36
|
-
*/
|
|
37
|
-
constructor(initialState) {
|
|
38
|
-
super(initialState);
|
|
39
|
-
}
|
|
40
|
-
computeDerivedState(state) {
|
|
41
|
-
return state;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Function that receives the current state and returns the new state.
|
|
3
|
-
* The updater function should be pure and return a new state object.
|
|
4
|
-
*
|
|
5
|
-
* @template T - The state type
|
|
6
|
-
* @param currentState - The current state
|
|
7
|
-
* @returns The new state
|
|
8
|
-
*/
|
|
9
|
-
export type Updater<T> = (currentState: T) => T;
|
|
10
|
-
/**
|
|
11
|
-
* Function that gets called when the state changes.
|
|
12
|
-
*
|
|
13
|
-
* @template T - The state type
|
|
14
|
-
*/
|
|
15
|
-
export type ViewModelListener = () => void;
|
|
16
|
-
/**
|
|
17
|
-
* Abstract base class for creating reactive view models with derived state.
|
|
18
|
-
*
|
|
19
|
-
* This class extends the basic ViewModel pattern by allowing you to compute
|
|
20
|
-
* derived state from internal state. The internal state is managed privately,
|
|
21
|
-
* while the derived state (which can include computed properties) is exposed
|
|
22
|
-
* to subscribers.
|
|
23
|
-
*
|
|
24
|
-
* @template S - The internal state type (managed internally)
|
|
25
|
-
* @template D - The derived state type (exposed to subscribers)
|
|
26
|
-
*
|
|
27
|
-
* @example
|
|
28
|
-
* ```typescript
|
|
29
|
-
* type TodoState = {
|
|
30
|
-
* items: Array<{ id: string; text: string; done: boolean }>;
|
|
31
|
-
* };
|
|
32
|
-
*
|
|
33
|
-
* type TodoDerivedState = TodoState & {
|
|
34
|
-
* totalCount: number;
|
|
35
|
-
* completedCount: number;
|
|
36
|
-
* remainingCount: number;
|
|
37
|
-
* };
|
|
38
|
-
*
|
|
39
|
-
* class TodoViewModel extends ViewModelWithDerivedState<TodoState, TodoDerivedState> {
|
|
40
|
-
* constructor() {
|
|
41
|
-
* super({ items: [] });
|
|
42
|
-
* }
|
|
43
|
-
*
|
|
44
|
-
* computeDerivedState({ items }: TodoState): TodoDerivedState {
|
|
45
|
-
* return {
|
|
46
|
-
* items,
|
|
47
|
-
* totalCount: items.length,
|
|
48
|
-
* completedCount: items.filter(item => item.done).length,
|
|
49
|
-
* remainingCount: items.filter(item => !item.done).length,
|
|
50
|
-
* };
|
|
51
|
-
* }
|
|
52
|
-
*
|
|
53
|
-
* addTodo(text: string) {
|
|
54
|
-
* this.update(({ items }) => ({
|
|
55
|
-
* items: [...items, { id: crypto.randomUUID(), text, done: false }],
|
|
56
|
-
* }));
|
|
57
|
-
* }
|
|
58
|
-
* }
|
|
59
|
-
*
|
|
60
|
-
* const todos = new TodoViewModel();
|
|
61
|
-
* todos.subscribe(() => {
|
|
62
|
-
* console.log('Completed:', todos.state.completedCount);
|
|
63
|
-
* });
|
|
64
|
-
* todos.addTodo('Learn ViewModels'); // Logs: Completed: 0
|
|
65
|
-
* ```
|
|
66
|
-
*/
|
|
67
|
-
export declare abstract class ViewModelWithDerivedState<S, D> {
|
|
68
|
-
private _listeners;
|
|
69
|
-
private _internalState;
|
|
70
|
-
private _state;
|
|
71
|
-
/**
|
|
72
|
-
* Subscribe to state changes.
|
|
73
|
-
*
|
|
74
|
-
* The listener will be called immediately after any state update.
|
|
75
|
-
*
|
|
76
|
-
* @param listener - Function to call when state changes
|
|
77
|
-
* @returns Function to unsubscribe the listener
|
|
78
|
-
*
|
|
79
|
-
* @example
|
|
80
|
-
* ```typescript
|
|
81
|
-
* const unsubscribe = viewModel.subscribe((state) => {
|
|
82
|
-
* console.log('State changed:', state);
|
|
83
|
-
* });
|
|
84
|
-
*
|
|
85
|
-
* // Later, when you want to stop listening:
|
|
86
|
-
* unsubscribe();
|
|
87
|
-
* ```
|
|
88
|
-
*/
|
|
89
|
-
subscribe(listener: ViewModelListener): () => void;
|
|
90
|
-
/**
|
|
91
|
-
* Create a new ViewModel with the given initial internal state.
|
|
92
|
-
*
|
|
93
|
-
* The constructor initializes the internal state and immediately computes
|
|
94
|
-
* the derived state by calling `computeDerivedState`.
|
|
95
|
-
*
|
|
96
|
-
* @param initialState - The initial internal state of the view model
|
|
97
|
-
*/
|
|
98
|
-
constructor(initialState: S);
|
|
99
|
-
/**
|
|
100
|
-
* Update the internal state, recompute derived state, and notify all subscribers.
|
|
101
|
-
*
|
|
102
|
-
* This method is protected and should only be called from within your view model subclass.
|
|
103
|
-
* The updater function receives the current internal state and should return the new internal state.
|
|
104
|
-
* After updating, the derived state is automatically recomputed via `computeDerivedState`.
|
|
105
|
-
* Always return a new state object to ensure immutability.
|
|
106
|
-
*
|
|
107
|
-
* @param updater - Function that receives current internal state and returns new internal state
|
|
108
|
-
*
|
|
109
|
-
* @example
|
|
110
|
-
* ```typescript
|
|
111
|
-
* this.update((currentState) => ({
|
|
112
|
-
* ...currentState,
|
|
113
|
-
* count: currentState.count + 1
|
|
114
|
-
* }));
|
|
115
|
-
* ```
|
|
116
|
-
*/
|
|
117
|
-
protected update(updater: Updater<S>): void;
|
|
118
|
-
/**
|
|
119
|
-
* Compute the derived state from the internal state.
|
|
120
|
-
*
|
|
121
|
-
* This abstract method must be implemented by subclasses to transform
|
|
122
|
-
* the internal state into the derived state that will be exposed to
|
|
123
|
-
* subscribers. This method is called automatically after each state
|
|
124
|
-
* update and during initialization.
|
|
125
|
-
*
|
|
126
|
-
* @param state - The current internal state
|
|
127
|
-
* @returns The derived state with any computed properties
|
|
128
|
-
*
|
|
129
|
-
* @example
|
|
130
|
-
* ```typescript
|
|
131
|
-
* computeDerivedState({ count }: CounterState): CounterDerivedState {
|
|
132
|
-
* return {
|
|
133
|
-
* count,
|
|
134
|
-
* isEven: count % 2 === 0,
|
|
135
|
-
* isPositive: count > 0,
|
|
136
|
-
* };
|
|
137
|
-
* }
|
|
138
|
-
* ```
|
|
139
|
-
*/
|
|
140
|
-
abstract computeDerivedState(state: S): D;
|
|
141
|
-
/**
|
|
142
|
-
* Get the current derived state.
|
|
143
|
-
*
|
|
144
|
-
* This returns the derived state computed by `computeDerivedState`,
|
|
145
|
-
* not the internal state.
|
|
146
|
-
*
|
|
147
|
-
* @returns The current derived state
|
|
148
|
-
*/
|
|
149
|
-
get state(): D;
|
|
150
|
-
}
|
|
151
|
-
//# sourceMappingURL=ViewModelWithDerivedState.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ViewModelWithDerivedState.d.ts","sourceRoot":"","sources":["../src/ViewModelWithDerivedState.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,MAAM,OAAO,CAAC,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC,KAAK,CAAC,CAAC;AAEhD;;;;GAIG;AACH,MAAM,MAAM,iBAAiB,GAAG,MAAM,IAAI,CAAC;AAE3C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AACH,8BAAsB,yBAAyB,CAAC,CAAC,EAAE,CAAC;IAClD,OAAO,CAAC,UAAU,CAAqC;IACvD,OAAO,CAAC,cAAc,CAAI;IAC1B,OAAO,CAAC,MAAM,CAAI;IAElB;;;;;;;;;;;;;;;;;OAiBG;IACH,SAAS,CAAC,QAAQ,EAAE,iBAAiB,GAAG,MAAM,IAAI;IAOlD;;;;;;;OAOG;gBACS,YAAY,EAAE,CAAC;IAK3B;;;;;;;;;;;;;;;;;OAiBG;IACH,SAAS,CAAC,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;IASpC;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,QAAQ,CAAC,mBAAmB,CAAC,KAAK,EAAE,CAAC,GAAG,CAAC;IAEzC;;;;;;;OAOG;IACH,IAAI,KAAK,IAAI,CAAC,CAEb;CACF"}
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Abstract base class for creating reactive view models with derived state.
|
|
3
|
-
*
|
|
4
|
-
* This class extends the basic ViewModel pattern by allowing you to compute
|
|
5
|
-
* derived state from internal state. The internal state is managed privately,
|
|
6
|
-
* while the derived state (which can include computed properties) is exposed
|
|
7
|
-
* to subscribers.
|
|
8
|
-
*
|
|
9
|
-
* @template S - The internal state type (managed internally)
|
|
10
|
-
* @template D - The derived state type (exposed to subscribers)
|
|
11
|
-
*
|
|
12
|
-
* @example
|
|
13
|
-
* ```typescript
|
|
14
|
-
* type TodoState = {
|
|
15
|
-
* items: Array<{ id: string; text: string; done: boolean }>;
|
|
16
|
-
* };
|
|
17
|
-
*
|
|
18
|
-
* type TodoDerivedState = TodoState & {
|
|
19
|
-
* totalCount: number;
|
|
20
|
-
* completedCount: number;
|
|
21
|
-
* remainingCount: number;
|
|
22
|
-
* };
|
|
23
|
-
*
|
|
24
|
-
* class TodoViewModel extends ViewModelWithDerivedState<TodoState, TodoDerivedState> {
|
|
25
|
-
* constructor() {
|
|
26
|
-
* super({ items: [] });
|
|
27
|
-
* }
|
|
28
|
-
*
|
|
29
|
-
* computeDerivedState({ items }: TodoState): TodoDerivedState {
|
|
30
|
-
* return {
|
|
31
|
-
* items,
|
|
32
|
-
* totalCount: items.length,
|
|
33
|
-
* completedCount: items.filter(item => item.done).length,
|
|
34
|
-
* remainingCount: items.filter(item => !item.done).length,
|
|
35
|
-
* };
|
|
36
|
-
* }
|
|
37
|
-
*
|
|
38
|
-
* addTodo(text: string) {
|
|
39
|
-
* this.update(({ items }) => ({
|
|
40
|
-
* items: [...items, { id: crypto.randomUUID(), text, done: false }],
|
|
41
|
-
* }));
|
|
42
|
-
* }
|
|
43
|
-
* }
|
|
44
|
-
*
|
|
45
|
-
* const todos = new TodoViewModel();
|
|
46
|
-
* todos.subscribe(() => {
|
|
47
|
-
* console.log('Completed:', todos.state.completedCount);
|
|
48
|
-
* });
|
|
49
|
-
* todos.addTodo('Learn ViewModels'); // Logs: Completed: 0
|
|
50
|
-
* ```
|
|
51
|
-
*/
|
|
52
|
-
export class ViewModelWithDerivedState {
|
|
53
|
-
/**
|
|
54
|
-
* Subscribe to state changes.
|
|
55
|
-
*
|
|
56
|
-
* The listener will be called immediately after any state update.
|
|
57
|
-
*
|
|
58
|
-
* @param listener - Function to call when state changes
|
|
59
|
-
* @returns Function to unsubscribe the listener
|
|
60
|
-
*
|
|
61
|
-
* @example
|
|
62
|
-
* ```typescript
|
|
63
|
-
* const unsubscribe = viewModel.subscribe((state) => {
|
|
64
|
-
* console.log('State changed:', state);
|
|
65
|
-
* });
|
|
66
|
-
*
|
|
67
|
-
* // Later, when you want to stop listening:
|
|
68
|
-
* unsubscribe();
|
|
69
|
-
* ```
|
|
70
|
-
*/
|
|
71
|
-
subscribe(listener) {
|
|
72
|
-
this._listeners.add(listener);
|
|
73
|
-
return () => {
|
|
74
|
-
this._listeners.delete(listener);
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
/**
|
|
78
|
-
* Create a new ViewModel with the given initial internal state.
|
|
79
|
-
*
|
|
80
|
-
* The constructor initializes the internal state and immediately computes
|
|
81
|
-
* the derived state by calling `computeDerivedState`.
|
|
82
|
-
*
|
|
83
|
-
* @param initialState - The initial internal state of the view model
|
|
84
|
-
*/
|
|
85
|
-
constructor(initialState) {
|
|
86
|
-
this._listeners = new Set();
|
|
87
|
-
this._internalState = initialState;
|
|
88
|
-
this._state = this.computeDerivedState(this._internalState);
|
|
89
|
-
}
|
|
90
|
-
/**
|
|
91
|
-
* Update the internal state, recompute derived state, and notify all subscribers.
|
|
92
|
-
*
|
|
93
|
-
* This method is protected and should only be called from within your view model subclass.
|
|
94
|
-
* The updater function receives the current internal state and should return the new internal state.
|
|
95
|
-
* After updating, the derived state is automatically recomputed via `computeDerivedState`.
|
|
96
|
-
* Always return a new state object to ensure immutability.
|
|
97
|
-
*
|
|
98
|
-
* @param updater - Function that receives current internal state and returns new internal state
|
|
99
|
-
*
|
|
100
|
-
* @example
|
|
101
|
-
* ```typescript
|
|
102
|
-
* this.update((currentState) => ({
|
|
103
|
-
* ...currentState,
|
|
104
|
-
* count: currentState.count + 1
|
|
105
|
-
* }));
|
|
106
|
-
* ```
|
|
107
|
-
*/
|
|
108
|
-
update(updater) {
|
|
109
|
-
this._internalState = updater(this._internalState);
|
|
110
|
-
this._state = this.computeDerivedState(this._internalState);
|
|
111
|
-
for (const listener of this._listeners) {
|
|
112
|
-
listener();
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
/**
|
|
116
|
-
* Get the current derived state.
|
|
117
|
-
*
|
|
118
|
-
* This returns the derived state computed by `computeDerivedState`,
|
|
119
|
-
* not the internal state.
|
|
120
|
-
*
|
|
121
|
-
* @returns The current derived state
|
|
122
|
-
*/
|
|
123
|
-
get state() {
|
|
124
|
-
return this._state;
|
|
125
|
-
}
|
|
126
|
-
}
|
package/dist/index.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gCAAgC,CAAC"}
|