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 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
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
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;
@@ -1,8 +1,5 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
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 = (0, store_1.writable)();
30
- const hasErrors = (0, store_1.derived)(errors, hasAnyErrors);
31
- const isDirty = (0, store_1.writable)(false);
32
- const actionInProgress = (0, store_1.writable)(false);
33
- const actionError = (0, store_1.writable)();
34
- const snapshots = (0, store_1.writable)([{ title: 'Initial', data: deepClone(init) }]);
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 = (0, store_1.get)(snapshots);
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 = (0, proxy_1.ChangeProxy)(stateObject, (target, property, currentValue, oldValue) => {
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 && (0, store_1.get)(actionInProgress))
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 = (0, store_1.get)(snapshots);
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 = (0, store_1.get)(snapshots);
108
+ const currentSnapshots = get(snapshots);
112
109
  const initialSnapshot = currentSnapshots[0];
113
110
  if (!initialSnapshot)
114
111
  return;
@@ -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.1",
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
- "default": "./dist/index.js"
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.9",
57
- "@typescript-eslint/eslint-plugin": "^8.53.0",
58
- "@typescript-eslint/parser": "^8.53.0",
59
- "@vitest/coverage-v8": "^4.0.17",
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.0",
66
- "svelte": "^5.46.4",
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.17"
71
+ "vitest": "^4.0.18"
71
72
  },
72
73
  "peerDependencies": {
73
74
  "svelte": "^5.0.0"