@view-models/core 2.0.0 → 2.1.1
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 +61 -58
- package/dist/ViewModel.d.ts +3 -62
- package/dist/ViewModel.d.ts.map +1 -1
- package/dist/ViewModel.js +5 -57
- package/dist/ViewModelWithDerivedState.d.ts +151 -0
- package/dist/ViewModelWithDerivedState.d.ts.map +1 -0
- package/dist/ViewModelWithDerivedState.js +126 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/package.json +4 -1
- package/src/ViewModel.ts +5 -80
- package/src/ViewModelWithDerivedState.ts +174 -0
- package/src/index.ts +1 -0
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((
|
|
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:
|
|
@@ -109,55 +160,7 @@ function Counter({ model }) {
|
|
|
109
160
|
|
|
110
161
|
## API Reference
|
|
111
162
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
Abstract base class for creating view models.
|
|
115
|
-
|
|
116
|
-
#### Constructor
|
|
117
|
-
|
|
118
|
-
```typescript
|
|
119
|
-
constructor(initialState: T)
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
Initialize the view model with an initial state.
|
|
123
|
-
|
|
124
|
-
#### Methods
|
|
125
|
-
|
|
126
|
-
##### `subscribe(listener: ViewModelListener<T>): void`
|
|
127
|
-
|
|
128
|
-
Subscribe to state changes. The listener will be called with the new state whenever `update()` is called.
|
|
129
|
-
|
|
130
|
-
```typescript
|
|
131
|
-
const unsubscribe = viewModel.subscribe((state) => {
|
|
132
|
-
console.log("State changed:", state);
|
|
133
|
-
});
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
##### `unsubscribe(listener: ViewModelListener<T>): void`
|
|
137
|
-
|
|
138
|
-
Unsubscribe a previously subscribed listener.
|
|
139
|
-
|
|
140
|
-
```typescript
|
|
141
|
-
viewModel.unsubscribe(listener);
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
##### `update(updater: Updater<T>): void` (protected)
|
|
145
|
-
|
|
146
|
-
Update the state and notify all subscribers. This method is protected and should only be called from within your view model subclass.
|
|
147
|
-
|
|
148
|
-
```typescript
|
|
149
|
-
protected update(updater: (currentState: T) => T): void
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
The updater function receives the current state and should return the new state.
|
|
153
|
-
|
|
154
|
-
##### `state: T` (getter)
|
|
155
|
-
|
|
156
|
-
Access the current state.
|
|
157
|
-
|
|
158
|
-
```typescript
|
|
159
|
-
const currentState = viewModel.state;
|
|
160
|
-
```
|
|
163
|
+
For detailed API documentation, see [docs](./docs).
|
|
161
164
|
|
|
162
165
|
## Patterns and Best Practices
|
|
163
166
|
|
package/dist/ViewModel.d.ts
CHANGED
|
@@ -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.js";
|
|
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
|
package/dist/ViewModel.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ViewModel.d.ts","sourceRoot":"","sources":["../src/ViewModel.ts"],"names":[],"mappings":"AAAA
|
|
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
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ViewModelWithDerivedState } from "./ViewModelWithDerivedState.js";
|
|
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
|
-
|
|
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
|
-
|
|
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,151 @@
|
|
|
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
|
|
@@ -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;;;;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"}
|
|
@@ -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 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
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@view-models/core",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"description": "A lightweight, framework-agnostic library for building reactive view models with TypeScript",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"view",
|
|
@@ -43,11 +43,14 @@
|
|
|
43
43
|
"test:coverage": "vitest run --coverage",
|
|
44
44
|
"format": "prettier --write \"**/*.{ts,js,json,md}\"",
|
|
45
45
|
"format:check": "prettier --check \"**/*.{ts,js,json,md}\"",
|
|
46
|
+
"docs": "typedoc && npm run format",
|
|
46
47
|
"prepublishOnly": "npm run build"
|
|
47
48
|
},
|
|
48
49
|
"devDependencies": {
|
|
49
50
|
"@vitest/coverage-v8": "^4.0.16",
|
|
50
51
|
"prettier": "^3.7.4",
|
|
52
|
+
"typedoc": "^0.28.16",
|
|
53
|
+
"typedoc-plugin-markdown": "^4.9.0",
|
|
51
54
|
"typescript": "^5.7.3",
|
|
52
55
|
"vitest": "^4.0.16"
|
|
53
56
|
},
|
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.js";
|
|
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
|
-
|
|
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
|
-
|
|
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,174 @@
|
|
|
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
|
+
*/
|
|
16
|
+
export type ViewModelListener = () => void;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Abstract base class for creating reactive view models with derived state.
|
|
20
|
+
*
|
|
21
|
+
* This class extends the basic ViewModel pattern by allowing you to compute
|
|
22
|
+
* derived state from internal state. The internal state is managed privately,
|
|
23
|
+
* while the derived state (which can include computed properties) is exposed
|
|
24
|
+
* to subscribers.
|
|
25
|
+
*
|
|
26
|
+
* @template S - The internal state type (managed internally)
|
|
27
|
+
* @template D - The derived state type (exposed to subscribers)
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```typescript
|
|
31
|
+
* type TodoState = {
|
|
32
|
+
* items: Array<{ id: string; text: string; done: boolean }>;
|
|
33
|
+
* };
|
|
34
|
+
*
|
|
35
|
+
* type TodoDerivedState = TodoState & {
|
|
36
|
+
* totalCount: number;
|
|
37
|
+
* completedCount: number;
|
|
38
|
+
* remainingCount: number;
|
|
39
|
+
* };
|
|
40
|
+
*
|
|
41
|
+
* class TodoViewModel extends ViewModelWithDerivedState<TodoState, TodoDerivedState> {
|
|
42
|
+
* constructor() {
|
|
43
|
+
* super({ items: [] });
|
|
44
|
+
* }
|
|
45
|
+
*
|
|
46
|
+
* computeDerivedState({ items }: TodoState): TodoDerivedState {
|
|
47
|
+
* return {
|
|
48
|
+
* items,
|
|
49
|
+
* totalCount: items.length,
|
|
50
|
+
* completedCount: items.filter(item => item.done).length,
|
|
51
|
+
* remainingCount: items.filter(item => !item.done).length,
|
|
52
|
+
* };
|
|
53
|
+
* }
|
|
54
|
+
*
|
|
55
|
+
* addTodo(text: string) {
|
|
56
|
+
* this.update(({ items }) => ({
|
|
57
|
+
* items: [...items, { id: crypto.randomUUID(), text, done: false }],
|
|
58
|
+
* }));
|
|
59
|
+
* }
|
|
60
|
+
* }
|
|
61
|
+
*
|
|
62
|
+
* const todos = new TodoViewModel();
|
|
63
|
+
* todos.subscribe(() => {
|
|
64
|
+
* console.log('Completed:', todos.state.completedCount);
|
|
65
|
+
* });
|
|
66
|
+
* todos.addTodo('Learn ViewModels'); // Logs: Completed: 0
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
export abstract class ViewModelWithDerivedState<S, D> {
|
|
70
|
+
private _listeners: Set<ViewModelListener> = new Set();
|
|
71
|
+
private _internalState: S;
|
|
72
|
+
private _state: D;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Subscribe to state changes.
|
|
76
|
+
*
|
|
77
|
+
* The listener will be called immediately after any state update.
|
|
78
|
+
*
|
|
79
|
+
* @param listener - Function to call when state changes
|
|
80
|
+
* @returns Function to unsubscribe the listener
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```typescript
|
|
84
|
+
* const unsubscribe = viewModel.subscribe((state) => {
|
|
85
|
+
* console.log('State changed:', state);
|
|
86
|
+
* });
|
|
87
|
+
*
|
|
88
|
+
* // Later, when you want to stop listening:
|
|
89
|
+
* unsubscribe();
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
subscribe(listener: ViewModelListener): () => void {
|
|
93
|
+
this._listeners.add(listener);
|
|
94
|
+
return () => {
|
|
95
|
+
this._listeners.delete(listener);
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Create a new ViewModel with the given initial internal state.
|
|
101
|
+
*
|
|
102
|
+
* The constructor initializes the internal state and immediately computes
|
|
103
|
+
* the derived state by calling `computeDerivedState`.
|
|
104
|
+
*
|
|
105
|
+
* @param initialState - The initial internal state of the view model
|
|
106
|
+
*/
|
|
107
|
+
constructor(initialState: S) {
|
|
108
|
+
this._internalState = initialState;
|
|
109
|
+
this._state = this.computeDerivedState(this._internalState);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Update the internal state, recompute derived state, and notify all subscribers.
|
|
114
|
+
*
|
|
115
|
+
* This method is protected and should only be called from within your view model subclass.
|
|
116
|
+
* The updater function receives the current internal state and should return the new internal state.
|
|
117
|
+
* After updating, the derived state is automatically recomputed via `computeDerivedState`.
|
|
118
|
+
* Always return a new state object to ensure immutability.
|
|
119
|
+
*
|
|
120
|
+
* @param updater - Function that receives current internal state and returns new internal state
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* ```typescript
|
|
124
|
+
* this.update((currentState) => ({
|
|
125
|
+
* ...currentState,
|
|
126
|
+
* count: currentState.count + 1
|
|
127
|
+
* }));
|
|
128
|
+
* ```
|
|
129
|
+
*/
|
|
130
|
+
protected update(updater: Updater<S>) {
|
|
131
|
+
this._internalState = updater(this._internalState);
|
|
132
|
+
this._state = this.computeDerivedState(this._internalState);
|
|
133
|
+
|
|
134
|
+
for (const listener of this._listeners) {
|
|
135
|
+
listener();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Compute the derived state from the internal state.
|
|
141
|
+
*
|
|
142
|
+
* This abstract method must be implemented by subclasses to transform
|
|
143
|
+
* the internal state into the derived state that will be exposed to
|
|
144
|
+
* subscribers. This method is called automatically after each state
|
|
145
|
+
* update and during initialization.
|
|
146
|
+
*
|
|
147
|
+
* @param state - The current internal state
|
|
148
|
+
* @returns The derived state with any computed properties
|
|
149
|
+
*
|
|
150
|
+
* @example
|
|
151
|
+
* ```typescript
|
|
152
|
+
* computeDerivedState({ count }: CounterState): CounterDerivedState {
|
|
153
|
+
* return {
|
|
154
|
+
* count,
|
|
155
|
+
* isEven: count % 2 === 0,
|
|
156
|
+
* isPositive: count > 0,
|
|
157
|
+
* };
|
|
158
|
+
* }
|
|
159
|
+
* ```
|
|
160
|
+
*/
|
|
161
|
+
abstract computeDerivedState(state: S): D;
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Get the current derived state.
|
|
165
|
+
*
|
|
166
|
+
* This returns the derived state computed by `computeDerivedState`,
|
|
167
|
+
* not the internal state.
|
|
168
|
+
*
|
|
169
|
+
* @returns The current derived state
|
|
170
|
+
*/
|
|
171
|
+
get state(): D {
|
|
172
|
+
return this._state;
|
|
173
|
+
}
|
|
174
|
+
}
|
package/src/index.ts
CHANGED