@view-models/core 4.0.0 → 4.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 +54 -0
- package/dist/index.d.ts +36 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/derived.ts +44 -0
- package/src/index.ts +1 -0
package/README.md
CHANGED
|
@@ -106,6 +106,60 @@ function Counter({ model }) {
|
|
|
106
106
|
}
|
|
107
107
|
```
|
|
108
108
|
|
|
109
|
+
## Derived State
|
|
110
|
+
|
|
111
|
+
The `derived` utility creates memoized mapper functions for computing derived state from your view model. This is useful when you need to transform or aggregate state values without recomputing on every render.
|
|
112
|
+
|
|
113
|
+
The `derived` function uses `Object.is` to compare inputs. When the input reference hasn't changed (which is guaranteed when using immutable updates), the cached output is returned without re-executing the mapper function.
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
import { ViewModel, derived } from "@view-models/core";
|
|
117
|
+
|
|
118
|
+
type CartState = {
|
|
119
|
+
items: Array<{ id: string; price: number; quantity: number }>;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
class CartViewModel extends ViewModel<CartState> {
|
|
123
|
+
constructor() {
|
|
124
|
+
super({ items: [] });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
addItem(item: { id: string; price: number; quantity: number }) {
|
|
128
|
+
super.update({
|
|
129
|
+
items: [...super.state.items, item],
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Create a derived mapper that computes cart statistics
|
|
135
|
+
const selectCartStats = derived((state: CartState) => ({
|
|
136
|
+
total: state.items.reduce((sum, item) => sum + item.price * item.quantity, 0),
|
|
137
|
+
itemCount: state.items.reduce((sum, item) => sum + item.quantity, 0),
|
|
138
|
+
}));
|
|
139
|
+
|
|
140
|
+
// Use it to compute derived state
|
|
141
|
+
const cart = new CartViewModel();
|
|
142
|
+
cart.addItem({ id: "1", price: 10, quantity: 2 });
|
|
143
|
+
cart.addItem({ id: "2", price: 15, quantity: 1 });
|
|
144
|
+
const stats = selectCartStats(cart.state); // { total: 35, itemCount: 3 }
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Derived mappers are particularly useful with framework hooks like `useDerivedState` (from [@view-models/react](https://github.com/sunesimonsen/view-models-react)):
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
function CartSummary({ cart }) {
|
|
151
|
+
// Only recomputes when cart.state reference changes
|
|
152
|
+
const stats = useDerivedState(cart, selectCartStats);
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
<div>
|
|
156
|
+
<p>Items: {stats.itemCount}</p>
|
|
157
|
+
<p>Total: ${stats.total}</p>
|
|
158
|
+
</div>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
109
163
|
## API Reference
|
|
110
164
|
|
|
111
165
|
For detailed API documentation, see [docs](./docs).
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,37 @@
|
|
|
1
|
+
type Mapper<I, O> = (input: I) => O;
|
|
2
|
+
/**
|
|
3
|
+
* A branded type for derived state mapper functions created by the `derived` utility.
|
|
4
|
+
* This type ensures that mapper functions are properly memoized before being used
|
|
5
|
+
* with hooks like `useDerivedState`.
|
|
6
|
+
*/
|
|
7
|
+
type DerivedMapper<I, O> = Mapper<I, O> & {
|
|
8
|
+
__brand: "derived";
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Creates a memoized derived state mapper function that caches results based on input identity.
|
|
12
|
+
*
|
|
13
|
+
* The memoization uses `Object.is` to compare inputs, making it ideal for use with
|
|
14
|
+
* immutable state objects. When the input reference hasn't changed, the cached output
|
|
15
|
+
* is returned without re-executing the function.
|
|
16
|
+
*
|
|
17
|
+
* @template I - The input type
|
|
18
|
+
* @template O - The output type
|
|
19
|
+
* @param fn - A pure function that transforms input to output
|
|
20
|
+
* @returns A memoized derived mapper function
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* const selectStats = derived(({ items }: AppState) => ({
|
|
25
|
+
* total: items.reduce((sum, item) => sum + item.price, 0),
|
|
26
|
+
* count: items.length
|
|
27
|
+
* }));
|
|
28
|
+
*
|
|
29
|
+
* // Use with useDerivedState
|
|
30
|
+
* const stats = selectStats(model.state);
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
declare const derived: <I, O>(fn: Mapper<I, O>) => DerivedMapper<I, O>;
|
|
34
|
+
|
|
1
35
|
/**
|
|
2
36
|
* Function that gets called when the state changes.
|
|
3
37
|
*/
|
|
@@ -83,5 +117,5 @@ declare abstract class ViewModel<S extends object> {
|
|
|
83
117
|
get state(): S;
|
|
84
118
|
}
|
|
85
119
|
|
|
86
|
-
export { ViewModel };
|
|
87
|
-
export type { ViewModelListener };
|
|
120
|
+
export { ViewModel, derived };
|
|
121
|
+
export type { DerivedMapper, ViewModelListener };
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
const t={},s=s=>{let e=t,r=t;return i=>e!==t&&Object.is(e,i)?r:r=s(e=i)};class e{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{e as ViewModel,s as derived};
|
|
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 partial - 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(partial: Partial<S>) {\n this._state = { ...this._state, ...partial };\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","partial","forEach","l","state"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/derived.ts","../src/ViewModel.ts"],"sourcesContent":["type Mapper<I, O> = (input: I) => O;\n\n/**\n * A branded type for derived state mapper functions created by the `derived` utility.\n * This type ensures that mapper functions are properly memoized before being used\n * with hooks like `useDerivedState`.\n */\nexport type DerivedMapper<I, O> = Mapper<I, O> & { __brand: \"derived\" };\n\nconst UNINITIALIZED = {};\n\n/**\n * Creates a memoized derived state mapper function that caches results based on input identity.\n *\n * The memoization uses `Object.is` to compare inputs, making it ideal for use with\n * immutable state objects. When the input reference hasn't changed, the cached output\n * is returned without re-executing the function.\n *\n * @template I - The input type\n * @template O - The output type\n * @param fn - A pure function that transforms input to output\n * @returns A memoized derived mapper function\n *\n * @example\n * ```ts\n * const selectStats = derived(({ items }: AppState) => ({\n * total: items.reduce((sum, item) => sum + item.price, 0),\n * count: items.length\n * }));\n *\n * // Use with useDerivedState\n * const stats = selectStats(model.state);\n * ```\n */\nexport const derived = <I, O>(fn: Mapper<I, O>): DerivedMapper<I, O> => {\n let lastInput: I | typeof UNINITIALIZED = UNINITIALIZED;\n let lastOutput: O | typeof UNINITIALIZED = UNINITIALIZED;\n return ((input: I) => {\n if (lastInput !== UNINITIALIZED && Object.is(lastInput, input)) {\n return lastOutput as O;\n }\n return (lastOutput = fn((lastInput = input)));\n }) as DerivedMapper<I, O>;\n};\n","/**\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 partial - 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(partial: Partial<S>) {\n this._state = { ...this._state, ...partial };\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":["UNINITIALIZED","derived","fn","lastInput","lastOutput","input","Object","is","ViewModel","subscribe","listener","this","_listeners","add","delete","constructor","initialState","Set","_state","update","partial","forEach","l","state"],"mappings":"AASA,MAAMA,EAAgB,CAAA,EAyBTC,EAAiBC,IAC5B,IAAIC,EAAsCH,EACtCI,EAAuCJ,EAC3C,OAASK,GACHF,IAAcH,GAAiBM,OAAOC,GAAGJ,EAAWE,GAC/CD,EAEDA,EAAaF,EAAIC,EAAYE,UCPnBG,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,CAAOC,GACfT,KAAKO,EAAS,IAAKP,KAAKO,KAAWE,GACnCT,KAAKC,EAAWS,QAASC,GAAMA,IACjC,CAOA,SAAIC,GACF,OAAOZ,KAAKO,CACd"}
|
package/package.json
CHANGED
package/src/derived.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
type Mapper<I, O> = (input: I) => O;
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A branded type for derived state mapper functions created by the `derived` utility.
|
|
5
|
+
* This type ensures that mapper functions are properly memoized before being used
|
|
6
|
+
* with hooks like `useDerivedState`.
|
|
7
|
+
*/
|
|
8
|
+
export type DerivedMapper<I, O> = Mapper<I, O> & { __brand: "derived" };
|
|
9
|
+
|
|
10
|
+
const UNINITIALIZED = {};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Creates a memoized derived state mapper function that caches results based on input identity.
|
|
14
|
+
*
|
|
15
|
+
* The memoization uses `Object.is` to compare inputs, making it ideal for use with
|
|
16
|
+
* immutable state objects. When the input reference hasn't changed, the cached output
|
|
17
|
+
* is returned without re-executing the function.
|
|
18
|
+
*
|
|
19
|
+
* @template I - The input type
|
|
20
|
+
* @template O - The output type
|
|
21
|
+
* @param fn - A pure function that transforms input to output
|
|
22
|
+
* @returns A memoized derived mapper function
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* const selectStats = derived(({ items }: AppState) => ({
|
|
27
|
+
* total: items.reduce((sum, item) => sum + item.price, 0),
|
|
28
|
+
* count: items.length
|
|
29
|
+
* }));
|
|
30
|
+
*
|
|
31
|
+
* // Use with useDerivedState
|
|
32
|
+
* const stats = selectStats(model.state);
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export const derived = <I, O>(fn: Mapper<I, O>): DerivedMapper<I, O> => {
|
|
36
|
+
let lastInput: I | typeof UNINITIALIZED = UNINITIALIZED;
|
|
37
|
+
let lastOutput: O | typeof UNINITIALIZED = UNINITIALIZED;
|
|
38
|
+
return ((input: I) => {
|
|
39
|
+
if (lastInput !== UNINITIALIZED && Object.is(lastInput, input)) {
|
|
40
|
+
return lastOutput as O;
|
|
41
|
+
}
|
|
42
|
+
return (lastOutput = fn((lastInput = input)));
|
|
43
|
+
}) as DerivedMapper<I, O>;
|
|
44
|
+
};
|
package/src/index.ts
CHANGED