@view-models/core 5.0.0 → 6.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 +35 -29
- package/dist/index.d.ts +0 -27
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/ViewModel.ts +2 -32
package/README.md
CHANGED
|
@@ -191,52 +191,58 @@ class TodosViewModel extends ViewModel<TodosState> {
|
|
|
191
191
|
}
|
|
192
192
|
```
|
|
193
193
|
|
|
194
|
-
### Derived State
|
|
194
|
+
### Derived State
|
|
195
195
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
196
|
+
When you have state values that are derived from other state, create a dedicated
|
|
197
|
+
update method that computes these values. This approach is more explicit and
|
|
198
|
+
efficient than intercepting every state update:
|
|
199
199
|
|
|
200
200
|
```typescript
|
|
201
|
-
type
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
201
|
+
type TodoState = {
|
|
202
|
+
todos: ReadonlyArray<{ id: number; text: string; completed: boolean }>;
|
|
203
|
+
completedCount: number;
|
|
204
|
+
pendingCount: number;
|
|
205
205
|
};
|
|
206
206
|
|
|
207
|
-
class
|
|
207
|
+
class TodoViewModel extends ViewModel<TodoState> {
|
|
208
|
+
private nextId = 1;
|
|
209
|
+
|
|
208
210
|
constructor() {
|
|
209
|
-
super({
|
|
211
|
+
super({ todos: [], completedCount: 0, pendingCount: 0 });
|
|
210
212
|
}
|
|
211
213
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
...
|
|
215
|
-
|
|
216
|
-
|
|
214
|
+
add(text: string) {
|
|
215
|
+
const todos = [
|
|
216
|
+
...super.state.todos,
|
|
217
|
+
{ id: this.nextId++, text, completed: false },
|
|
218
|
+
];
|
|
219
|
+
this.updateTodos(todos);
|
|
217
220
|
}
|
|
218
221
|
|
|
219
|
-
|
|
220
|
-
super.
|
|
222
|
+
toggle(id: number) {
|
|
223
|
+
const todos = super.state.todos.map((todo) =>
|
|
224
|
+
todo.id === id ? { ...todo, completed: !todo.completed } : todo,
|
|
225
|
+
);
|
|
226
|
+
this.updateTodos(todos);
|
|
221
227
|
}
|
|
222
228
|
|
|
223
|
-
|
|
224
|
-
super.update({
|
|
229
|
+
private updateTodos(todos: TodoState["todos"]) {
|
|
230
|
+
super.update({
|
|
231
|
+
todos,
|
|
232
|
+
completedCount: todos.filter((t) => t.completed).length,
|
|
233
|
+
pendingCount: todos.filter((t) => !t.completed).length,
|
|
234
|
+
});
|
|
225
235
|
}
|
|
226
236
|
}
|
|
227
237
|
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
238
|
+
const todoModel = new TodoViewModel();
|
|
239
|
+
todoModel.add("Buy milk");
|
|
240
|
+
todoModel.add("Walk the dog");
|
|
241
|
+
todoModel.toggle(1);
|
|
242
|
+
console.log(todoModel.state.completedCount); // 1
|
|
243
|
+
console.log(todoModel.state.pendingCount); // 1
|
|
232
244
|
```
|
|
233
245
|
|
|
234
|
-
This pattern is useful for:
|
|
235
|
-
|
|
236
|
-
- Computing derived values that depend on multiple state fields
|
|
237
|
-
- Enforcing invariants (e.g., ensuring values stay within bounds)
|
|
238
|
-
- Normalizing state (e.g., trimming strings, sorting arrays)
|
|
239
|
-
|
|
240
246
|
## License
|
|
241
247
|
|
|
242
248
|
MIT License
|
package/dist/index.d.ts
CHANGED
|
@@ -75,33 +75,6 @@ declare abstract class ViewModel<S extends object> {
|
|
|
75
75
|
* ```
|
|
76
76
|
*/
|
|
77
77
|
protected update(update: Partial<S>): void;
|
|
78
|
-
/**
|
|
79
|
-
* Hook to transform state before it is committed and subscribers are notified.
|
|
80
|
-
*
|
|
81
|
-
* Override this method in your subclass to intercept and modify state updates.
|
|
82
|
-
* This is useful for computing derived values, enforcing invariants, or
|
|
83
|
-
* normalizing state before it becomes the new state.
|
|
84
|
-
*
|
|
85
|
-
* By default, this method returns the input unchanged.
|
|
86
|
-
*
|
|
87
|
-
* @param updatedState - The new state that will be committed
|
|
88
|
-
* @returns The state to commit (can be modified or the same object)
|
|
89
|
-
*
|
|
90
|
-
* @example
|
|
91
|
-
* ```typescript
|
|
92
|
-
* type FormState = { firstName: string; lastName: string; fullName: string };
|
|
93
|
-
*
|
|
94
|
-
* class FormViewModel extends ViewModel<FormState> {
|
|
95
|
-
* protected prepareState(updatedState: FormState): FormState {
|
|
96
|
-
* return {
|
|
97
|
-
* ...updatedState,
|
|
98
|
-
* fullName: `${updatedState.firstName} ${updatedState.lastName}`,
|
|
99
|
-
* };
|
|
100
|
-
* }
|
|
101
|
-
* }
|
|
102
|
-
* ```
|
|
103
|
-
*/
|
|
104
|
-
protected prepareState(updatedState: S): S;
|
|
105
78
|
/**
|
|
106
79
|
* Get the current state.
|
|
107
80
|
*
|
package/dist/index.js
CHANGED
|
@@ -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=
|
|
1
|
+
class t{subscribe(t){return this.t.add(t),()=>{this.t.delete(t)}}constructor(t){this.t=new Set,this.i=t}update(t){this.i={...this.i,...t},this.t.forEach(t=>t())}get state(){return this.i}}export{t as ViewModel};
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../src/ViewModel.ts"],"sourcesContent":["/**\n * Function that gets called when the state changes.\n */\nexport type ViewModelListener = () => void;\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(() => {\n * console.log('Count:', counter.state.count);\n * });\n * counter.increment(); // Logs: Count: 1\n * ```\n */\nexport abstract class ViewModel<S extends object> {\n private _listeners = new Set<ViewModelListener>();\n private _state: S;\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(() => {\n * console.log('State changed:', viewModel.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 state.\n *\n * @param initialState - The initial state of the view model\n */\n constructor(initialState: S) {\n this._state =
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/ViewModel.ts"],"sourcesContent":["/**\n * Function that gets called when the state changes.\n */\nexport type ViewModelListener = () => void;\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(() => {\n * console.log('Count:', counter.state.count);\n * });\n * counter.increment(); // Logs: Count: 1\n * ```\n */\nexport abstract class ViewModel<S extends object> {\n private _listeners = new Set<ViewModelListener>();\n private _state: S;\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(() => {\n * console.log('State changed:', viewModel.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 state.\n *\n * @param initialState - The initial state of the view model\n */\n constructor(initialState: S) {\n this._state = initialState;\n }\n\n /**\n * Update the 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 state to create the new state.\n *\n * @param update - Partial state to merge with the current state\n *\n * @example\n * ```typescript\n * super.update({\n * count: super.state.count + 1\n * });\n * ```\n */\n protected update(update: Partial<S>) {\n this._state = { ...this._state, ...update };\n this._listeners.forEach((l) => l());\n }\n\n /**\n * Get the current state.\n *\n * @returns The current state\n */\n get state(): S {\n return this._state;\n }\n}\n"],"names":["ViewModel","subscribe","listener","this","_listeners","add","delete","constructor","initialState","Set","_state","update","forEach","l","state"],"mappings":"MAkCsBA,EAsBpB,SAAAC,CAAUC,GAER,OADAC,KAAKC,EAAWC,IAAIH,GACb,KACLC,KAAKC,EAAWE,OAAOJ,GAE3B,CAOA,WAAAK,CAAYC,GAjCJL,KAAAC,EAAa,IAAIK,IAkCvBN,KAAKO,EAASF,CAChB,CAiBU,MAAAG,CAAOA,GACfR,KAAKO,EAAS,IAAKP,KAAKO,KAAWC,GACnCR,KAAKC,EAAWQ,QAASC,GAAMA,IACjC,CAOA,SAAIC,GACF,OAAOX,KAAKO,CACd"}
|
package/package.json
CHANGED
package/src/ViewModel.ts
CHANGED
|
@@ -67,7 +67,7 @@ export abstract class ViewModel<S extends object> {
|
|
|
67
67
|
* @param initialState - The initial state of the view model
|
|
68
68
|
*/
|
|
69
69
|
constructor(initialState: S) {
|
|
70
|
-
this._state =
|
|
70
|
+
this._state = initialState;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
/**
|
|
@@ -86,40 +86,10 @@ export abstract class ViewModel<S extends object> {
|
|
|
86
86
|
* ```
|
|
87
87
|
*/
|
|
88
88
|
protected update(update: Partial<S>) {
|
|
89
|
-
this._state =
|
|
89
|
+
this._state = { ...this._state, ...update };
|
|
90
90
|
this._listeners.forEach((l) => l());
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
/**
|
|
94
|
-
* Hook to transform state before it is committed and subscribers are notified.
|
|
95
|
-
*
|
|
96
|
-
* Override this method in your subclass to intercept and modify state updates.
|
|
97
|
-
* This is useful for computing derived values, enforcing invariants, or
|
|
98
|
-
* normalizing state before it becomes the new state.
|
|
99
|
-
*
|
|
100
|
-
* By default, this method returns the input unchanged.
|
|
101
|
-
*
|
|
102
|
-
* @param updatedState - The new state that will be committed
|
|
103
|
-
* @returns The state to commit (can be modified or the same object)
|
|
104
|
-
*
|
|
105
|
-
* @example
|
|
106
|
-
* ```typescript
|
|
107
|
-
* type FormState = { firstName: string; lastName: string; fullName: string };
|
|
108
|
-
*
|
|
109
|
-
* class FormViewModel extends ViewModel<FormState> {
|
|
110
|
-
* protected prepareState(updatedState: FormState): FormState {
|
|
111
|
-
* return {
|
|
112
|
-
* ...updatedState,
|
|
113
|
-
* fullName: `${updatedState.firstName} ${updatedState.lastName}`,
|
|
114
|
-
* };
|
|
115
|
-
* }
|
|
116
|
-
* }
|
|
117
|
-
* ```
|
|
118
|
-
*/
|
|
119
|
-
protected prepareState(updatedState: S): S {
|
|
120
|
-
return updatedState;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
93
|
/**
|
|
124
94
|
* Get the current state.
|
|
125
95
|
*
|