@view-models/core 2.0.0 → 2.1.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
@@ -31,10 +31,6 @@ type CounterState = {
31
31
  };
32
32
 
33
33
  class CounterViewModel extends ViewModel<CounterState> {
34
- constructor() {
35
- super({ count: 0 });
36
- }
37
-
38
34
  increment() {
39
35
  this.update(({ count }) => ({
40
36
  count: count + 1,
@@ -46,10 +42,6 @@ class CounterViewModel extends ViewModel<CounterState> {
46
42
  count: count - 1,
47
43
  }));
48
44
  }
49
-
50
- reset() {
51
- this.update(() => ({ count: 0 }));
52
- }
53
45
  }
54
46
  ```
55
47
 
@@ -73,7 +65,7 @@ describe("CounterViewModel", () => {
73
65
  const counter = new CounterViewModel();
74
66
  const updates = [];
75
67
 
76
- counter.subscribe((state) => updates.push(state));
68
+ counter.subscribe(() => updates.push(counter.state));
77
69
 
78
70
  counter.increment();
79
71
  counter.increment();
@@ -83,6 +75,65 @@ describe("CounterViewModel", () => {
83
75
  });
84
76
  ```
85
77
 
78
+ ## View Models with Derived State
79
+
80
+ When you need to compute derived values from your state (like counts, filtered lists, or formatted data), use `ViewModelWithDerivedState`:
81
+
82
+ ```typescript
83
+ import { ViewModelWithDerivedState } from "@view-models/core";
84
+
85
+ type TodoState = {
86
+ items: Array<{ id: string; text: string; done: boolean }>;
87
+ };
88
+
89
+ type TodoDerivedState = TodoState & {
90
+ totalCount: number;
91
+ completedCount: number;
92
+ remainingCount: number;
93
+ };
94
+
95
+ class TodoViewModel extends ViewModelWithDerivedState<
96
+ TodoState,
97
+ TodoDerivedState
98
+ > {
99
+ constructor() {
100
+ super({ items: [] });
101
+ }
102
+
103
+ computeDerivedState({ items }: TodoState): TodoDerivedState {
104
+ return {
105
+ items,
106
+ totalCount: items.length,
107
+ completedCount: items.filter((item) => item.done).length,
108
+ remainingCount: items.filter((item) => !item.done).length,
109
+ };
110
+ }
111
+
112
+ addTodo(text: string) {
113
+ this.update(({ items }) => ({
114
+ items: [...items, { id: crypto.randomUUID(), text, done: false }],
115
+ }));
116
+ }
117
+
118
+ toggleTodo(id: string) {
119
+ this.update(({ items }) => ({
120
+ items: items.map((item) =>
121
+ item.id === id ? { ...item, done: !item.done } : item
122
+ ),
123
+ }));
124
+ }
125
+ }
126
+
127
+ const todos = new TodoViewModel();
128
+ console.log(todos.state.totalCount); // 0
129
+
130
+ todos.addTodo("Learn ViewModels");
131
+ console.log(todos.state.totalCount); // 1
132
+ console.log(todos.state.remainingCount); // 1
133
+ ```
134
+
135
+ The derived state is automatically recomputed whenever the internal state changes, ensuring your computed properties are always up-to-date.
136
+
86
137
  ## Framework Integration
87
138
 
88
139
  The view models are designed to work with framework-specific adapters. Upcoming adapters include:
@@ -123,22 +174,17 @@ Initialize the view model with an initial state.
123
174
 
124
175
  #### Methods
125
176
 
126
- ##### `subscribe(listener: ViewModelListener<T>): void`
177
+ ##### `subscribe(listener: ViewModelListener): () => void`
127
178
 
128
- Subscribe to state changes. The listener will be called with the new state whenever `update()` is called.
179
+ Subscribe to state changes. The listener will be called whenever `update()` is called. Returns a function to unsubscribe.
129
180
 
130
181
  ```typescript
131
- const unsubscribe = viewModel.subscribe((state) => {
132
- console.log("State changed:", state);
182
+ const unsubscribe = viewModel.subscribe(() => {
183
+ console.log("State changed:", viewModel.state);
133
184
  });
134
- ```
135
185
 
136
- ##### `unsubscribe(listener: ViewModelListener<T>): void`
137
-
138
- Unsubscribe a previously subscribed listener.
139
-
140
- ```typescript
141
- viewModel.unsubscribe(listener);
186
+ // Later, to unsubscribe:
187
+ unsubscribe();
142
188
  ```
143
189
 
144
190
  ##### `update(updater: Updater<T>): void` (protected)
@@ -159,6 +205,63 @@ Access the current state.
159
205
  const currentState = viewModel.state;
160
206
  ```
161
207
 
208
+ ### `ViewModelWithDerivedState<S, D>`
209
+
210
+ Abstract base class for creating view models with derived state. Use this when you need computed properties that are automatically recalculated when the internal state changes.
211
+
212
+ #### Constructor
213
+
214
+ ```typescript
215
+ constructor(initialState: S)
216
+ ```
217
+
218
+ Initialize the view model with an initial internal state. The derived state is automatically computed during construction.
219
+
220
+ #### Methods
221
+
222
+ ##### `subscribe(listener: ViewModelListener): () => void`
223
+
224
+ Subscribe to state changes. The listener will be called whenever the derived state changes. Returns a function to unsubscribe.
225
+
226
+ ```typescript
227
+ const unsubscribe = viewModel.subscribe(() => {
228
+ console.log("State changed:", viewModel.state);
229
+ });
230
+
231
+ // Later, to unsubscribe:
232
+ unsubscribe();
233
+ ```
234
+
235
+ ##### `update(updater: Updater<S>): void` (protected)
236
+
237
+ Update the internal state, automatically recompute derived state, and notify all subscribers. This method is protected and should only be called from within your view model subclass.
238
+
239
+ ```typescript
240
+ protected update(updater: (currentState: S) => S): void
241
+ ```
242
+
243
+ ##### `computeDerivedState(state: S): D` (abstract)
244
+
245
+ Abstract method that must be implemented to compute the derived state from the internal state. This is called automatically after each update and during initialization.
246
+
247
+ ```typescript
248
+ computeDerivedState({ items }: InternalState): DerivedState {
249
+ return {
250
+ items,
251
+ count: items.length,
252
+ hasItems: items.length > 0,
253
+ };
254
+ }
255
+ ```
256
+
257
+ ##### `state: D` (getter)
258
+
259
+ Access the current derived state.
260
+
261
+ ```typescript
262
+ const currentState = viewModel.state;
263
+ ```
264
+
162
265
  ## Patterns and Best Practices
163
266
 
164
267
  ### Keep State Immutable
@@ -1,19 +1,4 @@
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
- * @param state - The new state
15
- */
16
- export type ViewModelListener = () => void;
1
+ import { ViewModelWithDerivedState } from "./ViewModelWithDerivedState";
17
2
  /**
18
3
  * Abstract base class for creating reactive view models.
19
4
  *
@@ -43,57 +28,13 @@ export type ViewModelListener = () => void;
43
28
  * counter.increment(); // Logs: Count: 1
44
29
  * ```
45
30
  */
46
- export declare abstract class ViewModel<S> {
47
- private _listeners;
48
- /**
49
- * Subscribe to state changes.
50
- *
51
- * The listener will be called immediately after any state update.
52
- *
53
- * @param listener - Function to call when state changes
54
- * @returns Function to unsubscribe the listener
55
- *
56
- * @example
57
- * ```typescript
58
- * const unsubscribe = viewModel.subscribe((state) => {
59
- * console.log('State changed:', state);
60
- * });
61
- *
62
- * // Later, when you want to stop listening:
63
- * unsubscribe();
64
- * ```
65
- */
66
- subscribe(listener: ViewModelListener): () => void;
67
- private _state;
31
+ export declare abstract class ViewModel<S> extends ViewModelWithDerivedState<S, S> {
68
32
  /**
69
33
  * Create a new ViewModel with the given initial state.
70
34
  *
71
35
  * @param initialState - The initial state of the view model
72
36
  */
73
37
  constructor(initialState: S);
74
- /**
75
- * Update the state and notify all subscribers.
76
- *
77
- * This method is protected and should only be called from within your view model subclass.
78
- * The updater function receives the current state and should return the new state.
79
- * Always return a new state object to ensure immutability.
80
- *
81
- * @param updater - Function that receives current state and returns new state
82
- *
83
- * @example
84
- * ```typescript
85
- * this.update((currentState) => ({
86
- * ...currentState,
87
- * count: currentState.count + 1
88
- * }));
89
- * ```
90
- */
91
- protected update(updater: Updater<S>): void;
92
- /**
93
- * Get the current state.
94
- *
95
- * @returns The current state
96
- */
97
- get state(): S;
38
+ computeDerivedState(state: S): S;
98
39
  }
99
40
  //# sourceMappingURL=ViewModel.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ViewModel.d.ts","sourceRoot":"","sources":["../src/ViewModel.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,MAAM,OAAO,CAAC,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC,KAAK,CAAC,CAAC;AAEhD;;;;;GAKG;AACH,MAAM,MAAM,iBAAiB,GAAG,MAAM,IAAI,CAAC;AAE3C;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,8BAAsB,SAAS,CAAC,CAAC;IAC/B,OAAO,CAAC,UAAU,CAAqC;IAEvD;;;;;;;;;;;;;;;;;OAiBG;IACH,SAAS,CAAC,QAAQ,EAAE,iBAAiB,GAAG,MAAM,IAAI;IAOlD,OAAO,CAAC,MAAM,CAAI;IAElB;;;;OAIG;gBACS,YAAY,EAAE,CAAC;IAI3B;;;;;;;;;;;;;;;;OAgBG;IACH,SAAS,CAAC,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;IAQpC;;;;OAIG;IACH,IAAI,KAAK,IAAI,CAAC,CAEb;CACF"}
1
+ {"version":3,"file":"ViewModel.d.ts","sourceRoot":"","sources":["../src/ViewModel.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,yBAAyB,EAAE,MAAM,6BAA6B,CAAC;AAExE;;;;;;;;;;;;;;;;;;;;;;;;;;;;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 CHANGED
@@ -1,3 +1,4 @@
1
+ import { ViewModelWithDerivedState } from "./ViewModelWithDerivedState";
1
2
  /**
2
3
  * Abstract base class for creating reactive view models.
3
4
  *
@@ -27,69 +28,16 @@
27
28
  * counter.increment(); // Logs: Count: 1
28
29
  * ```
29
30
  */
30
- export class ViewModel {
31
- /**
32
- * Subscribe to state changes.
33
- *
34
- * The listener will be called immediately after any state update.
35
- *
36
- * @param listener - Function to call when state changes
37
- * @returns Function to unsubscribe the listener
38
- *
39
- * @example
40
- * ```typescript
41
- * const unsubscribe = viewModel.subscribe((state) => {
42
- * console.log('State changed:', state);
43
- * });
44
- *
45
- * // Later, when you want to stop listening:
46
- * unsubscribe();
47
- * ```
48
- */
49
- subscribe(listener) {
50
- this._listeners.add(listener);
51
- return () => {
52
- this._listeners.delete(listener);
53
- };
54
- }
31
+ export class ViewModel extends ViewModelWithDerivedState {
55
32
  /**
56
33
  * Create a new ViewModel with the given initial state.
57
34
  *
58
35
  * @param initialState - The initial state of the view model
59
36
  */
60
37
  constructor(initialState) {
61
- this._listeners = new Set();
62
- this._state = initialState;
63
- }
64
- /**
65
- * Update the state and notify all subscribers.
66
- *
67
- * This method is protected and should only be called from within your view model subclass.
68
- * The updater function receives the current state and should return the new state.
69
- * Always return a new state object to ensure immutability.
70
- *
71
- * @param updater - Function that receives current state and returns new state
72
- *
73
- * @example
74
- * ```typescript
75
- * this.update((currentState) => ({
76
- * ...currentState,
77
- * count: currentState.count + 1
78
- * }));
79
- * ```
80
- */
81
- update(updater) {
82
- this._state = updater(this._state);
83
- for (const listener of this._listeners) {
84
- listener();
85
- }
38
+ super(initialState);
86
39
  }
87
- /**
88
- * Get the current state.
89
- *
90
- * @returns The current state
91
- */
92
- get state() {
93
- return this._state;
40
+ computeDerivedState(state) {
41
+ return state;
94
42
  }
95
43
  }
@@ -0,0 +1,152 @@
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
+ * @param state - The new state
15
+ */
16
+ export type ViewModelListener = () => void;
17
+ /**
18
+ * Abstract base class for creating reactive view models with derived state.
19
+ *
20
+ * This class extends the basic ViewModel pattern by allowing you to compute
21
+ * derived state from internal state. The internal state is managed privately,
22
+ * while the derived state (which can include computed properties) is exposed
23
+ * to subscribers.
24
+ *
25
+ * @template S - The internal state type (managed internally)
26
+ * @template D - The derived state type (exposed to subscribers)
27
+ *
28
+ * @example
29
+ * ```typescript
30
+ * type TodoInternalState = {
31
+ * items: Array<{ id: string; text: string; done: boolean }>;
32
+ * };
33
+ *
34
+ * type TodoDerivedState = TodoInternalState & {
35
+ * totalCount: number;
36
+ * completedCount: number;
37
+ * remainingCount: number;
38
+ * };
39
+ *
40
+ * class TodoViewModel extends ViewModelWithDerivedState<TodoInternalState, TodoDerivedState> {
41
+ * constructor() {
42
+ * super({ items: [] });
43
+ * }
44
+ *
45
+ * computeDerivedState({ items }: TodoInternalState): TodoDerivedState {
46
+ * return {
47
+ * items,
48
+ * totalCount: items.length,
49
+ * completedCount: items.filter(item => item.done).length,
50
+ * remainingCount: items.filter(item => !item.done).length,
51
+ * };
52
+ * }
53
+ *
54
+ * addTodo(text: string) {
55
+ * this.update(({ items }) => ({
56
+ * items: [...items, { id: crypto.randomUUID(), text, done: false }],
57
+ * }));
58
+ * }
59
+ * }
60
+ *
61
+ * const todos = new TodoViewModel();
62
+ * todos.subscribe(() => {
63
+ * console.log('Completed:', todos.state.completedCount);
64
+ * });
65
+ * todos.addTodo('Learn ViewModels'); // Logs: Completed: 0
66
+ * ```
67
+ */
68
+ export declare abstract class ViewModelWithDerivedState<S, D> {
69
+ private _listeners;
70
+ private _internalState;
71
+ private _state;
72
+ /**
73
+ * Subscribe to state changes.
74
+ *
75
+ * The listener will be called immediately after any state update.
76
+ *
77
+ * @param listener - Function to call when state changes
78
+ * @returns Function to unsubscribe the listener
79
+ *
80
+ * @example
81
+ * ```typescript
82
+ * const unsubscribe = viewModel.subscribe((state) => {
83
+ * console.log('State changed:', state);
84
+ * });
85
+ *
86
+ * // Later, when you want to stop listening:
87
+ * unsubscribe();
88
+ * ```
89
+ */
90
+ subscribe(listener: ViewModelListener): () => void;
91
+ /**
92
+ * Create a new ViewModel with the given initial internal state.
93
+ *
94
+ * The constructor initializes the internal state and immediately computes
95
+ * the derived state by calling `computeDerivedState`.
96
+ *
97
+ * @param initialState - The initial internal state of the view model
98
+ */
99
+ constructor(initialState: S);
100
+ /**
101
+ * Update the internal state, recompute derived state, and notify all subscribers.
102
+ *
103
+ * This method is protected and should only be called from within your view model subclass.
104
+ * The updater function receives the current internal state and should return the new internal state.
105
+ * After updating, the derived state is automatically recomputed via `computeDerivedState`.
106
+ * Always return a new state object to ensure immutability.
107
+ *
108
+ * @param updater - Function that receives current internal state and returns new internal state
109
+ *
110
+ * @example
111
+ * ```typescript
112
+ * this.update((currentState) => ({
113
+ * ...currentState,
114
+ * count: currentState.count + 1
115
+ * }));
116
+ * ```
117
+ */
118
+ protected update(updater: Updater<S>): void;
119
+ /**
120
+ * Compute the derived state from the internal state.
121
+ *
122
+ * This abstract method must be implemented by subclasses to transform
123
+ * the internal state into the derived state that will be exposed to
124
+ * subscribers. This method is called automatically after each state
125
+ * update and during initialization.
126
+ *
127
+ * @param state - The current internal state
128
+ * @returns The derived state with any computed properties
129
+ *
130
+ * @example
131
+ * ```typescript
132
+ * computeDerivedState({ count }: CounterState): CounterDerivedState {
133
+ * return {
134
+ * count,
135
+ * isEven: count % 2 === 0,
136
+ * isPositive: count > 0,
137
+ * };
138
+ * }
139
+ * ```
140
+ */
141
+ abstract computeDerivedState(state: S): D;
142
+ /**
143
+ * Get the current derived state.
144
+ *
145
+ * This returns the derived state computed by `computeDerivedState`,
146
+ * not the internal state.
147
+ *
148
+ * @returns The current derived state
149
+ */
150
+ get state(): D;
151
+ }
152
+ //# sourceMappingURL=ViewModelWithDerivedState.d.ts.map
@@ -0,0 +1 @@
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;;;;;GAKG;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"}
@@ -0,0 +1,126 @@
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 TodoInternalState = {
15
+ * items: Array<{ id: string; text: string; done: boolean }>;
16
+ * };
17
+ *
18
+ * type TodoDerivedState = TodoInternalState & {
19
+ * totalCount: number;
20
+ * completedCount: number;
21
+ * remainingCount: number;
22
+ * };
23
+ *
24
+ * class TodoViewModel extends ViewModelWithDerivedState<TodoInternalState, TodoDerivedState> {
25
+ * constructor() {
26
+ * super({ items: [] });
27
+ * }
28
+ *
29
+ * computeDerivedState({ items }: TodoInternalState): 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 CHANGED
@@ -1,2 +1,3 @@
1
1
  export * from "./ViewModel.js";
2
+ export * from "./ViewModelWithDerivedState.js";
2
3
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gCAAgC,CAAC"}
package/dist/index.js CHANGED
@@ -1 +1,2 @@
1
1
  export * from "./ViewModel.js";
2
+ export * from "./ViewModelWithDerivedState.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@view-models/core",
3
- "version": "2.0.0",
3
+ "version": "2.1.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
@@ -1,20 +1,4 @@
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
- /**
12
- * Function that gets called when the state changes.
13
- *
14
- * @template T - The state type
15
- * @param state - The new state
16
- */
17
- export type ViewModelListener = () => void;
1
+ import { ViewModelWithDerivedState } from "./ViewModelWithDerivedState";
18
2
 
19
3
  /**
20
4
  * Abstract base class for creating reactive view models.
@@ -45,76 +29,17 @@ export type ViewModelListener = () => void;
45
29
  * counter.increment(); // Logs: Count: 1
46
30
  * ```
47
31
  */
48
- export abstract class ViewModel<S> {
49
- private _listeners: Set<ViewModelListener> = new Set();
50
-
51
- /**
52
- * Subscribe to state changes.
53
- *
54
- * The listener will be called immediately after any state update.
55
- *
56
- * @param listener - Function to call when state changes
57
- * @returns Function to unsubscribe the listener
58
- *
59
- * @example
60
- * ```typescript
61
- * const unsubscribe = viewModel.subscribe((state) => {
62
- * console.log('State changed:', state);
63
- * });
64
- *
65
- * // Later, when you want to stop listening:
66
- * unsubscribe();
67
- * ```
68
- */
69
- subscribe(listener: ViewModelListener): () => void {
70
- this._listeners.add(listener);
71
- return () => {
72
- this._listeners.delete(listener);
73
- };
74
- }
75
-
76
- private _state: S;
77
-
32
+ export abstract class ViewModel<S> extends ViewModelWithDerivedState<S, S> {
78
33
  /**
79
34
  * Create a new ViewModel with the given initial state.
80
35
  *
81
36
  * @param initialState - The initial state of the view model
82
37
  */
83
38
  constructor(initialState: S) {
84
- this._state = initialState;
85
- }
86
-
87
- /**
88
- * Update the state and notify all subscribers.
89
- *
90
- * This method is protected and should only be called from within your view model subclass.
91
- * The updater function receives the current state and should return the new state.
92
- * Always return a new state object to ensure immutability.
93
- *
94
- * @param updater - Function that receives current state and returns new state
95
- *
96
- * @example
97
- * ```typescript
98
- * this.update((currentState) => ({
99
- * ...currentState,
100
- * count: currentState.count + 1
101
- * }));
102
- * ```
103
- */
104
- protected update(updater: Updater<S>) {
105
- this._state = updater(this._state);
106
-
107
- for (const listener of this._listeners) {
108
- listener();
109
- }
39
+ super(initialState);
110
40
  }
111
41
 
112
- /**
113
- * Get the current state.
114
- *
115
- * @returns The current state
116
- */
117
- get state(): S {
118
- return this._state;
42
+ computeDerivedState(state: S): S {
43
+ return state;
119
44
  }
120
45
  }
@@ -0,0 +1,175 @@
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
+ /**
12
+ * Function that gets called when the state changes.
13
+ *
14
+ * @template T - The state type
15
+ * @param state - The new state
16
+ */
17
+ export type ViewModelListener = () => void;
18
+
19
+ /**
20
+ * Abstract base class for creating reactive view models with derived state.
21
+ *
22
+ * This class extends the basic ViewModel pattern by allowing you to compute
23
+ * derived state from internal state. The internal state is managed privately,
24
+ * while the derived state (which can include computed properties) is exposed
25
+ * to subscribers.
26
+ *
27
+ * @template S - The internal state type (managed internally)
28
+ * @template D - The derived state type (exposed to subscribers)
29
+ *
30
+ * @example
31
+ * ```typescript
32
+ * type TodoInternalState = {
33
+ * items: Array<{ id: string; text: string; done: boolean }>;
34
+ * };
35
+ *
36
+ * type TodoDerivedState = TodoInternalState & {
37
+ * totalCount: number;
38
+ * completedCount: number;
39
+ * remainingCount: number;
40
+ * };
41
+ *
42
+ * class TodoViewModel extends ViewModelWithDerivedState<TodoInternalState, TodoDerivedState> {
43
+ * constructor() {
44
+ * super({ items: [] });
45
+ * }
46
+ *
47
+ * computeDerivedState({ items }: TodoInternalState): TodoDerivedState {
48
+ * return {
49
+ * items,
50
+ * totalCount: items.length,
51
+ * completedCount: items.filter(item => item.done).length,
52
+ * remainingCount: items.filter(item => !item.done).length,
53
+ * };
54
+ * }
55
+ *
56
+ * addTodo(text: string) {
57
+ * this.update(({ items }) => ({
58
+ * items: [...items, { id: crypto.randomUUID(), text, done: false }],
59
+ * }));
60
+ * }
61
+ * }
62
+ *
63
+ * const todos = new TodoViewModel();
64
+ * todos.subscribe(() => {
65
+ * console.log('Completed:', todos.state.completedCount);
66
+ * });
67
+ * todos.addTodo('Learn ViewModels'); // Logs: Completed: 0
68
+ * ```
69
+ */
70
+ export abstract class ViewModelWithDerivedState<S, D> {
71
+ private _listeners: Set<ViewModelListener> = new Set();
72
+ private _internalState: S;
73
+ private _state: D;
74
+
75
+ /**
76
+ * Subscribe to state changes.
77
+ *
78
+ * The listener will be called immediately after any state update.
79
+ *
80
+ * @param listener - Function to call when state changes
81
+ * @returns Function to unsubscribe the listener
82
+ *
83
+ * @example
84
+ * ```typescript
85
+ * const unsubscribe = viewModel.subscribe((state) => {
86
+ * console.log('State changed:', state);
87
+ * });
88
+ *
89
+ * // Later, when you want to stop listening:
90
+ * unsubscribe();
91
+ * ```
92
+ */
93
+ subscribe(listener: ViewModelListener): () => void {
94
+ this._listeners.add(listener);
95
+ return () => {
96
+ this._listeners.delete(listener);
97
+ };
98
+ }
99
+
100
+ /**
101
+ * Create a new ViewModel with the given initial internal state.
102
+ *
103
+ * The constructor initializes the internal state and immediately computes
104
+ * the derived state by calling `computeDerivedState`.
105
+ *
106
+ * @param initialState - The initial internal state of the view model
107
+ */
108
+ constructor(initialState: S) {
109
+ this._internalState = initialState;
110
+ this._state = this.computeDerivedState(this._internalState);
111
+ }
112
+
113
+ /**
114
+ * Update the internal state, recompute derived state, and notify all subscribers.
115
+ *
116
+ * This method is protected and should only be called from within your view model subclass.
117
+ * The updater function receives the current internal state and should return the new internal state.
118
+ * After updating, the derived state is automatically recomputed via `computeDerivedState`.
119
+ * Always return a new state object to ensure immutability.
120
+ *
121
+ * @param updater - Function that receives current internal state and returns new internal state
122
+ *
123
+ * @example
124
+ * ```typescript
125
+ * this.update((currentState) => ({
126
+ * ...currentState,
127
+ * count: currentState.count + 1
128
+ * }));
129
+ * ```
130
+ */
131
+ protected update(updater: Updater<S>) {
132
+ this._internalState = updater(this._internalState);
133
+ this._state = this.computeDerivedState(this._internalState);
134
+
135
+ for (const listener of this._listeners) {
136
+ listener();
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Compute the derived state from the internal state.
142
+ *
143
+ * This abstract method must be implemented by subclasses to transform
144
+ * the internal state into the derived state that will be exposed to
145
+ * subscribers. This method is called automatically after each state
146
+ * update and during initialization.
147
+ *
148
+ * @param state - The current internal state
149
+ * @returns The derived state with any computed properties
150
+ *
151
+ * @example
152
+ * ```typescript
153
+ * computeDerivedState({ count }: CounterState): CounterDerivedState {
154
+ * return {
155
+ * count,
156
+ * isEven: count % 2 === 0,
157
+ * isPositive: count > 0,
158
+ * };
159
+ * }
160
+ * ```
161
+ */
162
+ abstract computeDerivedState(state: S): D;
163
+
164
+ /**
165
+ * Get the current derived state.
166
+ *
167
+ * This returns the derived state computed by `computeDerivedState`,
168
+ * not the internal state.
169
+ *
170
+ * @returns The current derived state
171
+ */
172
+ get state(): D {
173
+ return this._state;
174
+ }
175
+ }
package/src/index.ts CHANGED
@@ -1 +1,2 @@
1
1
  export * from "./ViewModel.js";
2
+ export * from "./ViewModelWithDerivedState.js";