svstate 1.0.1 → 1.2.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 +70 -1
- package/dist/index.js +2 -10
- package/dist/proxy.js +1 -5
- package/dist/state.svelte.js +15 -18
- package/dist/validators.js +4 -10
- package/package.json +10 -9
package/README.md
CHANGED
|
@@ -73,6 +73,7 @@ const customer = $state({
|
|
|
73
73
|
- ⚡ **Fires effects** when any property changes (with full context)
|
|
74
74
|
- ⏪ **Snapshots & undo** for complex editing workflows
|
|
75
75
|
- 🎯 **Tracks dirty state** automatically
|
|
76
|
+
- 🔧 **Supports methods** on state objects for computed values and formatting
|
|
76
77
|
|
|
77
78
|
```typescript
|
|
78
79
|
import { createSvState, stringValidator, numberValidator } from 'svstate';
|
|
@@ -104,6 +105,8 @@ npm install svstate
|
|
|
104
105
|
|
|
105
106
|
**Requirements:** Node.js ≥20, Svelte 5
|
|
106
107
|
|
|
108
|
+
**Note:** This package is distributed as ESM (ES Modules) only.
|
|
109
|
+
|
|
107
110
|
---
|
|
108
111
|
|
|
109
112
|
## 🎯 Core Features
|
|
@@ -332,6 +335,70 @@ const { data } = createSvState(formData, actuators, {
|
|
|
332
335
|
|
|
333
336
|
---
|
|
334
337
|
|
|
338
|
+
### 6️⃣ State Objects with Methods
|
|
339
|
+
|
|
340
|
+
State objects can include methods that operate on `this`. Methods are preserved through snapshots and undo operations, making it easy to encapsulate computed values and formatting logic:
|
|
341
|
+
|
|
342
|
+
```typescript
|
|
343
|
+
import { createSvState, numberValidator } from 'svstate';
|
|
344
|
+
|
|
345
|
+
// Define state with methods
|
|
346
|
+
type InvoiceData = {
|
|
347
|
+
unitPrice: number;
|
|
348
|
+
quantity: number;
|
|
349
|
+
subtotal: number;
|
|
350
|
+
tax: number;
|
|
351
|
+
total: number;
|
|
352
|
+
calculateTotals: (taxRate?: number) => void;
|
|
353
|
+
formatCurrency: (value: number) => string;
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
const createInvoice = (): InvoiceData => ({
|
|
357
|
+
unitPrice: 0,
|
|
358
|
+
quantity: 1,
|
|
359
|
+
subtotal: 0,
|
|
360
|
+
tax: 0,
|
|
361
|
+
total: 0,
|
|
362
|
+
calculateTotals(taxRate = 0.08) {
|
|
363
|
+
this.subtotal = this.unitPrice * this.quantity;
|
|
364
|
+
this.tax = this.subtotal * taxRate;
|
|
365
|
+
this.total = this.subtotal + this.tax;
|
|
366
|
+
},
|
|
367
|
+
formatCurrency(value: number) {
|
|
368
|
+
return `$${value.toFixed(2)}`;
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
const {
|
|
373
|
+
data,
|
|
374
|
+
state: { errors }
|
|
375
|
+
} = createSvState(createInvoice(), {
|
|
376
|
+
validator: (source) => ({
|
|
377
|
+
unitPrice: numberValidator(source.unitPrice).required().positive().getError(),
|
|
378
|
+
quantity: numberValidator(source.quantity).required().integer().min(1).getError()
|
|
379
|
+
}),
|
|
380
|
+
effect: ({ property }) => {
|
|
381
|
+
// Call method directly on state when inputs change
|
|
382
|
+
if (property === 'unitPrice' || property === 'quantity') {
|
|
383
|
+
data.calculateTotals();
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
// In template: use methods for formatting
|
|
389
|
+
// {data.formatCurrency(data.subtotal)} → "$99.00"
|
|
390
|
+
// {data.formatCurrency(data.total)} → "$106.92"
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
**Key features:**
|
|
394
|
+
|
|
395
|
+
- 🔧 Methods can modify `this` properties (triggers validation/effects)
|
|
396
|
+
- 📸 Methods preserved through `rollback()` and `reset()`
|
|
397
|
+
- 🎯 Call methods from effects to compute derived values
|
|
398
|
+
- 📐 Encapsulate formatting and business logic in state object
|
|
399
|
+
|
|
400
|
+
---
|
|
401
|
+
|
|
335
402
|
## 🏗️ Complete Examples
|
|
336
403
|
|
|
337
404
|
### Example 1: ERP Customer Form with Nested Addresses
|
|
@@ -696,7 +763,7 @@ Creates a supercharged state object.
|
|
|
696
763
|
**Returns:**
|
|
697
764
|
| Property | Type | Description |
|
|
698
765
|
|----------|------|-------------|
|
|
699
|
-
| `data` | `T` | Deep reactive proxy — bind directly |
|
|
766
|
+
| `data` | `T` | Deep reactive proxy — bind directly, methods preserved |
|
|
700
767
|
| `execute(params?)` | `(P?) => Promise<void>` | Run the configured action |
|
|
701
768
|
| `rollback(steps?)` | `(n?: number) => void` | Undo N changes (default: 1) |
|
|
702
769
|
| `reset()` | `() => void` | Return to initial state |
|
|
@@ -784,6 +851,7 @@ const { data, state } = createSvState<UserData, UserErrors, object>(
|
|
|
784
851
|
| Undo/Redo | ❌ DIY | ✅ Built-in |
|
|
785
852
|
| Dirty tracking | ❌ DIY | ✅ Automatic |
|
|
786
853
|
| Action loading states | ❌ DIY | ✅ Built-in |
|
|
854
|
+
| State with methods | ⚠️ Manual cloning | ✅ Automatic |
|
|
787
855
|
|
|
788
856
|
**svstate is for:**
|
|
789
857
|
|
|
@@ -798,6 +866,7 @@ const { data, state } = createSvState<UserData, UserErrors, object>(
|
|
|
798
866
|
## 📚 Resources
|
|
799
867
|
|
|
800
868
|
- 🎮 [Live Demo](https://bcsabaengine.github.io/svstate/) — Try it in your browser
|
|
869
|
+
- 🛠️ [SvelteKit Example](https://github.com/BCsabaEngine/svstate-kit) — Example SvelteKit application using svstate
|
|
801
870
|
- 📖 [Documentation](https://github.com/BCsabaEngine/svstate)
|
|
802
871
|
- 🐛 [Report Issues](https://github.com/BCsabaEngine/svstate/issues)
|
|
803
872
|
- 💬 [Discussions](https://github.com/BCsabaEngine/svstate/discussions)
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
exports.stringValidator = exports.numberValidator = exports.dateValidator = exports.arrayValidator = exports.createSvState = void 0;
|
|
4
|
-
var state_svelte_1 = require("./state.svelte");
|
|
5
|
-
Object.defineProperty(exports, "createSvState", { enumerable: true, get: function () { return state_svelte_1.createSvState; } });
|
|
6
|
-
var validators_1 = require("./validators");
|
|
7
|
-
Object.defineProperty(exports, "arrayValidator", { enumerable: true, get: function () { return validators_1.arrayValidator; } });
|
|
8
|
-
Object.defineProperty(exports, "dateValidator", { enumerable: true, get: function () { return validators_1.dateValidator; } });
|
|
9
|
-
Object.defineProperty(exports, "numberValidator", { enumerable: true, get: function () { return validators_1.numberValidator; } });
|
|
10
|
-
Object.defineProperty(exports, "stringValidator", { enumerable: true, get: function () { return validators_1.stringValidator; } });
|
|
1
|
+
export { createSvState } from './state.svelte';
|
|
2
|
+
export { arrayValidator, dateValidator, numberValidator, stringValidator } from './validators';
|
package/dist/proxy.js
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ChangeProxy = void 0;
|
|
4
1
|
const isProxiable = (value) => typeof value === 'object' &&
|
|
5
2
|
value !== null &&
|
|
6
3
|
!(value instanceof Date) &&
|
|
@@ -11,7 +8,7 @@ const isProxiable = (value) => typeof value === 'object' &&
|
|
|
11
8
|
!(value instanceof RegExp) &&
|
|
12
9
|
!(value instanceof Error) &&
|
|
13
10
|
!(value instanceof Promise);
|
|
14
|
-
const ChangeProxy = (source, changed) => {
|
|
11
|
+
export const ChangeProxy = (source, changed) => {
|
|
15
12
|
const createProxy = (target, parentPath) => new Proxy(target, {
|
|
16
13
|
get(object, property) {
|
|
17
14
|
const value = object[property];
|
|
@@ -36,4 +33,3 @@ const ChangeProxy = (source, changed) => {
|
|
|
36
33
|
const data = createProxy(source, '');
|
|
37
34
|
return data;
|
|
38
35
|
};
|
|
39
|
-
exports.ChangeProxy = ChangeProxy;
|
package/dist/state.svelte.js
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
exports.createSvState = createSvState;
|
|
4
|
-
const store_1 = require("svelte/store");
|
|
5
|
-
const proxy_1 = require("./proxy");
|
|
1
|
+
import { derived, get, writable } from 'svelte/store';
|
|
2
|
+
import { ChangeProxy } from './proxy';
|
|
6
3
|
const checkHasErrors = (validator) => Object.values(validator).some((item) => (typeof item === 'string' ? !!item : checkHasErrors(item)));
|
|
7
4
|
const hasAnyErrors = ($errors) => !!$errors && checkHasErrors($errors);
|
|
8
5
|
const deepClone = (object) => {
|
|
@@ -12,7 +9,7 @@ const deepClone = (object) => {
|
|
|
12
9
|
return new Date(object);
|
|
13
10
|
if (Array.isArray(object))
|
|
14
11
|
return object.map((item) => deepClone(item));
|
|
15
|
-
const cloned =
|
|
12
|
+
const cloned = Object.create(Object.getPrototypeOf(object));
|
|
16
13
|
for (const key of Object.keys(object))
|
|
17
14
|
cloned[key] = deepClone(object[key]);
|
|
18
15
|
return cloned;
|
|
@@ -23,18 +20,18 @@ const defaultOptions = {
|
|
|
23
20
|
allowConcurrentActions: false,
|
|
24
21
|
persistActionError: false
|
|
25
22
|
};
|
|
26
|
-
function createSvState(init, actuators, options) {
|
|
23
|
+
export function createSvState(init, actuators, options) {
|
|
27
24
|
const usedOptions = { ...defaultOptions, ...options };
|
|
28
25
|
const { validator, effect } = actuators ?? {};
|
|
29
|
-
const errors =
|
|
30
|
-
const hasErrors =
|
|
31
|
-
const isDirty =
|
|
32
|
-
const actionInProgress =
|
|
33
|
-
const actionError =
|
|
34
|
-
const snapshots =
|
|
26
|
+
const errors = writable();
|
|
27
|
+
const hasErrors = derived(errors, hasAnyErrors);
|
|
28
|
+
const isDirty = writable(false);
|
|
29
|
+
const actionInProgress = writable(false);
|
|
30
|
+
const actionError = writable();
|
|
31
|
+
const snapshots = writable([{ title: 'Initial', data: deepClone(init) }]);
|
|
35
32
|
const stateObject = $state(init);
|
|
36
33
|
const createSnapshot = (title, replace = true) => {
|
|
37
|
-
const currentSnapshots =
|
|
34
|
+
const currentSnapshots = get(snapshots);
|
|
38
35
|
const createdSnapshot = { title, data: deepClone(stateObject) };
|
|
39
36
|
const lastSnapshot = currentSnapshots.at(-1);
|
|
40
37
|
if (replace && lastSnapshot && lastSnapshot.title === title)
|
|
@@ -63,7 +60,7 @@ function createSvState(init, actuators, options) {
|
|
|
63
60
|
});
|
|
64
61
|
}
|
|
65
62
|
};
|
|
66
|
-
const data =
|
|
63
|
+
const data = ChangeProxy(stateObject, (target, property, currentValue, oldValue) => {
|
|
67
64
|
if (!usedOptions.persistActionError)
|
|
68
65
|
actionError.set(undefined);
|
|
69
66
|
isDirty.set(true);
|
|
@@ -75,7 +72,7 @@ function createSvState(init, actuators, options) {
|
|
|
75
72
|
if (validator)
|
|
76
73
|
errors.set(validator(data));
|
|
77
74
|
const execute = async (parameters) => {
|
|
78
|
-
if (!usedOptions.allowConcurrentActions &&
|
|
75
|
+
if (!usedOptions.allowConcurrentActions && get(actionInProgress))
|
|
79
76
|
return;
|
|
80
77
|
actionError.set(undefined);
|
|
81
78
|
actionInProgress.set(true);
|
|
@@ -95,7 +92,7 @@ function createSvState(init, actuators, options) {
|
|
|
95
92
|
}
|
|
96
93
|
};
|
|
97
94
|
const rollback = (steps = 1) => {
|
|
98
|
-
const currentSnapshots =
|
|
95
|
+
const currentSnapshots = get(snapshots);
|
|
99
96
|
if (currentSnapshots.length <= 1)
|
|
100
97
|
return;
|
|
101
98
|
const targetIndex = Math.max(0, currentSnapshots.length - 1 - steps);
|
|
@@ -108,7 +105,7 @@ function createSvState(init, actuators, options) {
|
|
|
108
105
|
errors.set(validator(data));
|
|
109
106
|
};
|
|
110
107
|
const reset = () => {
|
|
111
|
-
const currentSnapshots =
|
|
108
|
+
const currentSnapshots = get(snapshots);
|
|
112
109
|
const initialSnapshot = currentSnapshots[0];
|
|
113
110
|
if (!initialSnapshot)
|
|
114
111
|
return;
|
package/dist/validators.js
CHANGED
|
@@ -1,16 +1,10 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.stringValidator = stringValidator;
|
|
4
|
-
exports.numberValidator = numberValidator;
|
|
5
|
-
exports.arrayValidator = arrayValidator;
|
|
6
|
-
exports.dateValidator = dateValidator;
|
|
7
1
|
const prepareOps = {
|
|
8
2
|
trim: (s) => s.trim(),
|
|
9
3
|
normalize: (s) => s.replaceAll(/\s{2,}/g, ' '),
|
|
10
4
|
upper: (s) => s.toUpperCase(),
|
|
11
5
|
lower: (s) => s.toLowerCase()
|
|
12
6
|
};
|
|
13
|
-
function stringValidator(input, ...prepares) {
|
|
7
|
+
export function stringValidator(input, ...prepares) {
|
|
14
8
|
let error = '';
|
|
15
9
|
const setError = (message) => {
|
|
16
10
|
if (!error)
|
|
@@ -113,7 +107,7 @@ function stringValidator(input, ...prepares) {
|
|
|
113
107
|
};
|
|
114
108
|
return builder;
|
|
115
109
|
}
|
|
116
|
-
function numberValidator(input) {
|
|
110
|
+
export function numberValidator(input) {
|
|
117
111
|
let error = '';
|
|
118
112
|
const setError = (message) => {
|
|
119
113
|
if (!error)
|
|
@@ -185,7 +179,7 @@ function numberValidator(input) {
|
|
|
185
179
|
};
|
|
186
180
|
return builder;
|
|
187
181
|
}
|
|
188
|
-
function arrayValidator(input) {
|
|
182
|
+
export function arrayValidator(input) {
|
|
189
183
|
let error = '';
|
|
190
184
|
const setError = (message) => {
|
|
191
185
|
if (!error)
|
|
@@ -227,7 +221,7 @@ function arrayValidator(input) {
|
|
|
227
221
|
};
|
|
228
222
|
return builder;
|
|
229
223
|
}
|
|
230
|
-
function dateValidator(input) {
|
|
224
|
+
export function dateValidator(input) {
|
|
231
225
|
let error = '';
|
|
232
226
|
const setError = (message) => {
|
|
233
227
|
if (!error)
|
package/package.json
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "svstate",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Supercharged $state() for Svelte 5: deep reactive proxy with validation, cross-field rules, computed & side-effects",
|
|
5
5
|
"author": "BCsabaEngine",
|
|
6
6
|
"license": "ISC",
|
|
7
|
+
"type": "module",
|
|
7
8
|
"main": "./dist/index.js",
|
|
8
9
|
"types": "./dist/index.d.ts",
|
|
9
10
|
"exports": {
|
|
10
11
|
".": {
|
|
11
12
|
"types": "./dist/index.d.ts",
|
|
12
|
-
"
|
|
13
|
+
"import": "./dist/index.js"
|
|
13
14
|
}
|
|
14
15
|
},
|
|
15
16
|
"engines": {
|
|
@@ -53,21 +54,21 @@
|
|
|
53
54
|
],
|
|
54
55
|
"devDependencies": {
|
|
55
56
|
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
|
56
|
-
"@types/node": "^25.0.
|
|
57
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
58
|
-
"@typescript-eslint/parser": "^8.
|
|
59
|
-
"@vitest/coverage-v8": "^4.0.
|
|
57
|
+
"@types/node": "^25.0.10",
|
|
58
|
+
"@typescript-eslint/eslint-plugin": "^8.54.0",
|
|
59
|
+
"@typescript-eslint/parser": "^8.54.0",
|
|
60
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
60
61
|
"eslint": "^9.39.2",
|
|
61
62
|
"eslint-config-prettier": "^10.1.8",
|
|
62
63
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
|
63
64
|
"eslint-plugin-unicorn": "^62.0.0",
|
|
64
65
|
"nodemon": "^3.1.11",
|
|
65
|
-
"prettier": "^3.8.
|
|
66
|
-
"svelte": "^5.
|
|
66
|
+
"prettier": "^3.8.1",
|
|
67
|
+
"svelte": "^5.48.5",
|
|
67
68
|
"ts-node": "^10.9.2",
|
|
68
69
|
"tsx": "^4.21.0",
|
|
69
70
|
"typescript": "^5.9.3",
|
|
70
|
-
"vitest": "^4.0.
|
|
71
|
+
"vitest": "^4.0.18"
|
|
71
72
|
},
|
|
72
73
|
"peerDependencies": {
|
|
73
74
|
"svelte": "^5.0.0"
|