@view-models/core 4.0.0 → 5.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 +55 -6
- package/dist/index.d.ts +29 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/ViewModel.ts +34 -4
package/README.md
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
[](https://github.com/sunesimonsen/view-models-core/actions/workflows/ci.yml)
|
|
4
4
|
[](https://unpkg.com/@view-models/core@latest/dist/index.js)
|
|
5
5
|
|
|
6
|
-
A lightweight, framework-agnostic library for building reactive view models with
|
|
6
|
+
A lightweight, framework-agnostic library for building reactive view models with
|
|
7
|
+
TypeScript. Separate your business logic from your UI framework with a simple,
|
|
8
|
+
testable pattern.
|
|
7
9
|
|
|
8
10
|

|
|
9
11
|
|
|
@@ -38,17 +40,17 @@ class CounterViewModel extends ViewModel<CounterState> {
|
|
|
38
40
|
super({ count: 0 });
|
|
39
41
|
}
|
|
40
42
|
|
|
41
|
-
increment() {
|
|
43
|
+
increment = () => {
|
|
42
44
|
super.update({
|
|
43
45
|
count: super.state.count + 1,
|
|
44
46
|
});
|
|
45
|
-
}
|
|
47
|
+
};
|
|
46
48
|
|
|
47
|
-
decrement() {
|
|
49
|
+
decrement = () => {
|
|
48
50
|
super.update({
|
|
49
51
|
count: super.state.count - 1,
|
|
50
52
|
});
|
|
51
|
-
}
|
|
53
|
+
};
|
|
52
54
|
}
|
|
53
55
|
```
|
|
54
56
|
|
|
@@ -84,7 +86,8 @@ describe("CounterViewModel", () => {
|
|
|
84
86
|
|
|
85
87
|
## Framework Integration
|
|
86
88
|
|
|
87
|
-
The view models are designed to work with framework-specific adapters. Upcoming
|
|
89
|
+
The view models are designed to work with framework-specific adapters. Upcoming
|
|
90
|
+
adapters include:
|
|
88
91
|
|
|
89
92
|
- [@view-models/react](https://github.com/sunesimonsen/view-models-react) - React hooks integration
|
|
90
93
|
- [@view-models/preact](https://github.com/sunesimonsen/view-models-preact) - Preact hooks integration
|
|
@@ -188,6 +191,52 @@ class TodosViewModel extends ViewModel<TodosState> {
|
|
|
188
191
|
}
|
|
189
192
|
```
|
|
190
193
|
|
|
194
|
+
### Derived State with prepareState
|
|
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:
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
type FormState = {
|
|
202
|
+
firstName: string;
|
|
203
|
+
lastName: string;
|
|
204
|
+
fullName: string;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
class FormViewModel extends ViewModel<FormState> {
|
|
208
|
+
constructor() {
|
|
209
|
+
super({ firstName: "", lastName: "", fullName: "" });
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
protected prepareState(updatedState: FormState): FormState {
|
|
213
|
+
return {
|
|
214
|
+
...updatedState,
|
|
215
|
+
fullName: `${updatedState.firstName} ${updatedState.lastName}`.trim(),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
setFirstName(firstName: string) {
|
|
220
|
+
super.update({ firstName });
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
setLastName(lastName: string) {
|
|
224
|
+
super.update({ lastName });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const form = new FormViewModel();
|
|
229
|
+
form.setFirstName("John");
|
|
230
|
+
form.setLastName("Doe");
|
|
231
|
+
console.log(form.state.fullName); // "John Doe"
|
|
232
|
+
```
|
|
233
|
+
|
|
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
|
+
|
|
191
240
|
## License
|
|
192
241
|
|
|
193
242
|
MIT License
|
package/dist/index.d.ts
CHANGED
|
@@ -65,7 +65,7 @@ declare abstract class ViewModel<S extends object> {
|
|
|
65
65
|
* This method is protected and should only be called from within your view model subclass.
|
|
66
66
|
* The partial state is merged with the current state to create the new state.
|
|
67
67
|
*
|
|
68
|
-
* @param
|
|
68
|
+
* @param update - Partial state to merge with the current state
|
|
69
69
|
*
|
|
70
70
|
* @example
|
|
71
71
|
* ```typescript
|
|
@@ -74,7 +74,34 @@ declare abstract class ViewModel<S extends object> {
|
|
|
74
74
|
* });
|
|
75
75
|
* ```
|
|
76
76
|
*/
|
|
77
|
-
protected update(
|
|
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;
|
|
78
105
|
/**
|
|
79
106
|
* Get the current state.
|
|
80
107
|
*
|
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=t}update(t){this.i={...this.i,...t},this.t.forEach(t=>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=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};
|
|
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 = 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
|
|
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"}
|
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 = initialState;
|
|
70
|
+
this._state = this.prepareState(initialState);
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
/**
|
|
@@ -76,7 +76,7 @@ export abstract class ViewModel<S extends object> {
|
|
|
76
76
|
* This method is protected and should only be called from within your view model subclass.
|
|
77
77
|
* The partial state is merged with the current state to create the new state.
|
|
78
78
|
*
|
|
79
|
-
* @param
|
|
79
|
+
* @param update - Partial state to merge with the current state
|
|
80
80
|
*
|
|
81
81
|
* @example
|
|
82
82
|
* ```typescript
|
|
@@ -85,11 +85,41 @@ export abstract class ViewModel<S extends object> {
|
|
|
85
85
|
* });
|
|
86
86
|
* ```
|
|
87
87
|
*/
|
|
88
|
-
protected update(
|
|
89
|
-
this._state = { ...this._state, ...
|
|
88
|
+
protected update(update: Partial<S>) {
|
|
89
|
+
this._state = this.prepareState({ ...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
|
+
|
|
93
123
|
/**
|
|
94
124
|
* Get the current state.
|
|
95
125
|
*
|