@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 CHANGED
@@ -191,52 +191,58 @@ class TodosViewModel extends ViewModel<TodosState> {
191
191
  }
192
192
  ```
193
193
 
194
- ### Derived State with prepareState
194
+ ### Derived State
195
195
 
196
- Override the `prepareState` method to compute derived values or enforce
197
- invariants before state is committed. This hook intercepts every state update,
198
- allowing you to transform the state before subscribers are notified:
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 FormState = {
202
- firstName: string;
203
- lastName: string;
204
- fullName: string;
201
+ type TodoState = {
202
+ todos: ReadonlyArray<{ id: number; text: string; completed: boolean }>;
203
+ completedCount: number;
204
+ pendingCount: number;
205
205
  };
206
206
 
207
- class FormViewModel extends ViewModel<FormState> {
207
+ class TodoViewModel extends ViewModel<TodoState> {
208
+ private nextId = 1;
209
+
208
210
  constructor() {
209
- super({ firstName: "", lastName: "", fullName: "" });
211
+ super({ todos: [], completedCount: 0, pendingCount: 0 });
210
212
  }
211
213
 
212
- protected prepareState(updatedState: FormState): FormState {
213
- return {
214
- ...updatedState,
215
- fullName: `${updatedState.firstName} ${updatedState.lastName}`.trim(),
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
- setFirstName(firstName: string) {
220
- super.update({ firstName });
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
- setLastName(lastName: string) {
224
- super.update({ lastName });
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 form = new FormViewModel();
229
- form.setFirstName("John");
230
- form.setLastName("Doe");
231
- console.log(form.state.fullName); // "John Doe"
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=this.prepareState(t)}update(t){this.i=this.prepareState({...this.i,...t}),this.t.forEach(t=>t())}prepareState(t){return t}get state(){return this.i}}export{t as ViewModel};
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 = this.prepareState(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.prepareState({ ...this._state, ...update });\n this._listeners.forEach((l) => l());\n }\n\n /**\n * Hook to transform state before it is committed and subscribers are notified.\n *\n * Override this method in your subclass to intercept and modify state updates.\n * This is useful for computing derived values, enforcing invariants, or\n * normalizing state before it becomes the new state.\n *\n * By default, this method returns the input unchanged.\n *\n * @param updatedState - The new state that will be committed\n * @returns The state to commit (can be modified or the same object)\n *\n * @example\n * ```typescript\n * type FormState = { firstName: string; lastName: string; fullName: string };\n *\n * class FormViewModel extends ViewModel<FormState> {\n * protected prepareState(updatedState: FormState): FormState {\n * return {\n * ...updatedState,\n * fullName: `${updatedState.firstName} ${updatedState.lastName}`,\n * };\n * }\n * }\n * ```\n */\n protected prepareState(updatedState: S): S {\n return updatedState;\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","prepareState","update","forEach","l","updatedState","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,EAASP,KAAKQ,aAAaH,EAClC,CAiBU,MAAAI,CAAOA,GACfT,KAAKO,EAASP,KAAKQ,aAAa,IAAKR,KAAKO,KAAWE,IACrDT,KAAKC,EAAWS,QAASC,GAAMA,IACjC,CA4BU,YAAAH,CAAaI,GACrB,OAAOA,CACT,CAOA,SAAIC,GACF,OAAOb,KAAKO,CACd"}
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@view-models/core",
3
- "version": "5.0.0",
3
+ "version": "6.0.0",
4
4
  "description": "A lightweight, framework-agnostic library for building reactive view models with TypeScript",
5
5
  "keywords": [
6
6
  "view",
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 = this.prepareState(initialState);
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 = this.prepareState({ ...this._state, ...update });
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
  *