@signaltree/core 7.1.6 → 7.6.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.
@@ -0,0 +1,310 @@
1
+ import { signal, computed } from '@angular/core';
2
+ import { registerMarkerProcessor } from '../internals/materialize-markers.js';
3
+
4
+ const FORM_MARKER = Symbol('FORM_MARKER');
5
+ let formRegistered = false;
6
+ function form(config) {
7
+ if (!formRegistered) {
8
+ formRegistered = true;
9
+ registerMarkerProcessor(isFormMarker, createFormSignal);
10
+ }
11
+ return {
12
+ [FORM_MARKER]: true,
13
+ config
14
+ };
15
+ }
16
+ function isFormMarker(value) {
17
+ return value !== null && typeof value === 'object' && FORM_MARKER in value && value[FORM_MARKER] === true;
18
+ }
19
+ function createFormSignal(marker) {
20
+ const config = marker.config;
21
+ const initial = config.initial;
22
+ const valuesSignal = signal({
23
+ ...initial
24
+ });
25
+ const touchedSignal = signal(Object.keys(initial).reduce((acc, key) => {
26
+ acc[key] = false;
27
+ return acc;
28
+ }, {}));
29
+ const errorsSignal = signal({});
30
+ const submittingSignal = signal(false);
31
+ const dirty = computed(() => {
32
+ const current = valuesSignal();
33
+ const eq = config.equalityFn ?? defaultEquality;
34
+ return Object.keys(initial).some(key => !eq(current[key], initial[key]));
35
+ });
36
+ const valid = computed(() => {
37
+ const errs = errorsSignal();
38
+ return Object.values(errs).every(e => e === null || e === undefined);
39
+ });
40
+ const errorList = computed(() => {
41
+ const errs = errorsSignal();
42
+ return Object.values(errs).filter(e => e !== null && e !== undefined);
43
+ });
44
+ let persistTimeout = null;
45
+ const storage = config.storage !== undefined ? config.storage : typeof window !== 'undefined' ? window.localStorage : null;
46
+ function loadFromStorage() {
47
+ if (!config.persist || !storage) return;
48
+ try {
49
+ const stored = storage.getItem(config.persist);
50
+ if (stored) {
51
+ const parsed = JSON.parse(stored);
52
+ valuesSignal.set({
53
+ ...initial,
54
+ ...parsed
55
+ });
56
+ }
57
+ } catch {}
58
+ }
59
+ function saveToStorage() {
60
+ if (!config.persist || !storage) return;
61
+ try {
62
+ storage.setItem(config.persist, JSON.stringify(valuesSignal()));
63
+ } catch {}
64
+ }
65
+ function schedulePersist() {
66
+ if (!config.persist) return;
67
+ if (persistTimeout) clearTimeout(persistTimeout);
68
+ persistTimeout = setTimeout(saveToStorage, config.persistDebounceMs ?? 500);
69
+ }
70
+ loadFromStorage();
71
+ function createFieldAccessor(path, getValue, setValue) {
72
+ const accessor = () => getValue();
73
+ accessor.set = v => {
74
+ setValue(v);
75
+ schedulePersist();
76
+ };
77
+ accessor.update = fn => {
78
+ setValue(fn(getValue()));
79
+ schedulePersist();
80
+ };
81
+ return accessor;
82
+ }
83
+ function createFieldsProxy(values) {
84
+ const proxy = {};
85
+ for (const key of Object.keys(values)) {
86
+ const k = key;
87
+ const fieldAccessor = createFieldAccessor(key, () => valuesSignal()[k], v => valuesSignal.update(curr => ({
88
+ ...curr,
89
+ [k]: v
90
+ })));
91
+ const value = values[k];
92
+ if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
93
+ const nested = createFieldsProxy(value);
94
+ Object.assign(fieldAccessor, nested);
95
+ }
96
+ proxy[key] = fieldAccessor;
97
+ }
98
+ return proxy;
99
+ }
100
+ const fieldsProxy = createFieldsProxy(initial);
101
+ async function validateField(field) {
102
+ const value = valuesSignal()[field];
103
+ const validators = config.validators?.[field];
104
+ const asyncValidator = config.asyncValidators?.[field];
105
+ let error = null;
106
+ if (validators) {
107
+ const validatorArray = Array.isArray(validators) ? validators : [validators];
108
+ for (const validator of validatorArray) {
109
+ error = validator(value);
110
+ if (error) break;
111
+ }
112
+ }
113
+ if (!error && asyncValidator) {
114
+ error = await asyncValidator(value);
115
+ }
116
+ errorsSignal.update(errs => ({
117
+ ...errs,
118
+ [field]: error
119
+ }));
120
+ return error === null;
121
+ }
122
+ async function validateAll() {
123
+ const fields = Object.keys(initial);
124
+ const results = await Promise.all(fields.map(validateField));
125
+ return results.every(Boolean);
126
+ }
127
+ let wizard;
128
+ if (config.wizard) {
129
+ const wizardConfig = config.wizard;
130
+ const currentStepSignal = signal(0);
131
+ const stepName = computed(() => wizardConfig.steps[currentStepSignal()] ?? '');
132
+ const canNext = computed(() => currentStepSignal() < wizardConfig.steps.length - 1);
133
+ const canPrev = computed(() => currentStepSignal() > 0);
134
+ const isLastStep = computed(() => currentStepSignal() === wizardConfig.steps.length - 1);
135
+ const isFirstStep = computed(() => currentStepSignal() === 0);
136
+ async function validateCurrentStep() {
137
+ const stepIdx = currentStepSignal();
138
+ const stepNameStr = wizardConfig.steps[stepIdx];
139
+ const stepCfg = wizardConfig.stepConfig?.[stepNameStr];
140
+ if (stepCfg?.validate) {
141
+ const result = await stepCfg.validate();
142
+ if (!result) return false;
143
+ }
144
+ const stepFields = wizardConfig.stepFields?.[stepNameStr] ?? stepCfg?.fields ?? [];
145
+ for (const field of stepFields) {
146
+ const isValid = await validateField(field);
147
+ if (!isValid) return false;
148
+ }
149
+ return true;
150
+ }
151
+ wizard = {
152
+ currentStep: currentStepSignal.asReadonly(),
153
+ stepName,
154
+ steps: signal(wizardConfig.steps).asReadonly(),
155
+ canNext,
156
+ canPrev,
157
+ isLastStep,
158
+ isFirstStep,
159
+ async next() {
160
+ const valid = await validateCurrentStep();
161
+ if (!valid) return false;
162
+ const current = currentStepSignal();
163
+ if (current < wizardConfig.steps.length - 1) {
164
+ currentStepSignal.set(current + 1);
165
+ return true;
166
+ }
167
+ return false;
168
+ },
169
+ prev() {
170
+ const current = currentStepSignal();
171
+ if (current > 0) {
172
+ currentStepSignal.set(current - 1);
173
+ }
174
+ },
175
+ async goTo(step) {
176
+ const targetIdx = typeof step === 'number' ? step : wizardConfig.steps.indexOf(step);
177
+ if (targetIdx < 0 || targetIdx >= wizardConfig.steps.length) {
178
+ return false;
179
+ }
180
+ if (targetIdx > currentStepSignal()) {
181
+ const valid = await validateCurrentStep();
182
+ if (!valid) return false;
183
+ }
184
+ currentStepSignal.set(targetIdx);
185
+ return true;
186
+ },
187
+ reset() {
188
+ currentStepSignal.set(0);
189
+ }
190
+ };
191
+ }
192
+ const formSignalFn = () => valuesSignal();
193
+ formSignalFn.$ = fieldsProxy;
194
+ formSignalFn.set = values => {
195
+ valuesSignal.update(curr => ({
196
+ ...curr,
197
+ ...values
198
+ }));
199
+ schedulePersist();
200
+ };
201
+ formSignalFn.patch = values => {
202
+ valuesSignal.update(curr => ({
203
+ ...curr,
204
+ ...values
205
+ }));
206
+ schedulePersist();
207
+ };
208
+ formSignalFn.reset = () => {
209
+ valuesSignal.set({
210
+ ...initial
211
+ });
212
+ touchedSignal.set(Object.keys(initial).reduce((acc, key) => {
213
+ acc[key] = false;
214
+ return acc;
215
+ }, {}));
216
+ errorsSignal.set({});
217
+ wizard?.reset();
218
+ schedulePersist();
219
+ };
220
+ formSignalFn.clear = () => {
221
+ const empty = Object.keys(initial).reduce((acc, key) => {
222
+ const val = initial[key];
223
+ acc[key] = typeof val === 'string' ? '' : typeof val === 'number' ? 0 : Array.isArray(val) ? [] : val === null ? null : typeof val === 'object' ? {} : val;
224
+ return acc;
225
+ }, {});
226
+ valuesSignal.set(empty);
227
+ schedulePersist();
228
+ };
229
+ formSignalFn.valid = valid;
230
+ formSignalFn.dirty = dirty;
231
+ formSignalFn.submitting = submittingSignal.asReadonly();
232
+ formSignalFn.touched = touchedSignal.asReadonly();
233
+ formSignalFn.errors = errorsSignal.asReadonly();
234
+ formSignalFn.errorList = errorList;
235
+ formSignalFn.validate = validateAll;
236
+ formSignalFn.validateField = validateField;
237
+ formSignalFn.touch = field => {
238
+ touchedSignal.update(t => ({
239
+ ...t,
240
+ [field]: true
241
+ }));
242
+ };
243
+ formSignalFn.touchAll = () => {
244
+ touchedSignal.update(t => {
245
+ const updated = {
246
+ ...t
247
+ };
248
+ for (const key of Object.keys(t)) {
249
+ updated[key] = true;
250
+ }
251
+ return updated;
252
+ });
253
+ };
254
+ formSignalFn.submit = async handler => {
255
+ submittingSignal.set(true);
256
+ try {
257
+ const isValid = await validateAll();
258
+ if (!isValid) {
259
+ return null;
260
+ }
261
+ const result = await handler(valuesSignal());
262
+ return result;
263
+ } finally {
264
+ submittingSignal.set(false);
265
+ }
266
+ };
267
+ formSignalFn.wizard = wizard;
268
+ formSignalFn.persistNow = () => {
269
+ if (persistTimeout) clearTimeout(persistTimeout);
270
+ saveToStorage();
271
+ };
272
+ formSignalFn.reload = () => {
273
+ loadFromStorage();
274
+ };
275
+ formSignalFn.clearStorage = () => {
276
+ if (config.persist && storage) {
277
+ storage.removeItem(config.persist);
278
+ }
279
+ };
280
+ return formSignalFn;
281
+ }
282
+ function defaultEquality(a, b) {
283
+ if (a === b) return true;
284
+ if (a === null || b === null) return false;
285
+ if (typeof a !== typeof b) return false;
286
+ if (typeof a === 'object') {
287
+ return JSON.stringify(a) === JSON.stringify(b);
288
+ }
289
+ return false;
290
+ }
291
+ const validators = {
292
+ required: (message = 'This field is required') => value => value === null || value === undefined || value === '' ? message : null,
293
+ minLength: (min, message) => value => typeof value === 'string' && value.length < min ? message ?? `Must be at least ${min} characters` : null,
294
+ maxLength: (max, message) => value => typeof value === 'string' && value.length > max ? message ?? `Must be at most ${max} characters` : null,
295
+ min: (min, message) => value => typeof value === 'number' && value < min ? message ?? `Must be at least ${min}` : null,
296
+ max: (max, message) => value => typeof value === 'number' && value > max ? message ?? `Must be at most ${max}` : null,
297
+ email: (message = 'Invalid email address') => value => typeof value === 'string' && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) ? message : null,
298
+ pattern: (regex, message = 'Invalid format') => value => typeof value === 'string' && !regex.test(value) ? message : null,
299
+ when: (condition, validator) => (value, form) => {
300
+ if (!form) {
301
+ return null;
302
+ }
303
+ if (condition(form)) {
304
+ return validator(value);
305
+ }
306
+ return null;
307
+ }
308
+ };
309
+
310
+ export { FORM_MARKER, createFormSignal, form, isFormMarker, validators };
@@ -2,6 +2,31 @@ import { signal } from '@angular/core';
2
2
  import { registerMarkerProcessor } from '../internals/materialize-markers.js';
3
3
 
4
4
  const STORED_MARKER = Symbol('STORED_MARKER');
5
+ function isVersionedData(value) {
6
+ return value !== null && typeof value === 'object' && '__v' in value && typeof value.__v === 'number' && 'data' in value;
7
+ }
8
+ function createStorageKeys(prefix, keys) {
9
+ const result = {};
10
+ for (const [key, value] of Object.entries(keys)) {
11
+ if (typeof value === 'string') {
12
+ result[key] = `${prefix}:${value}`;
13
+ } else if (typeof value === 'object' && value !== null) {
14
+ result[key] = createStorageKeys(`${prefix}:${key}`, value);
15
+ }
16
+ }
17
+ return result;
18
+ }
19
+ function clearStoragePrefix(prefix, storage = typeof localStorage !== 'undefined' ? localStorage : null) {
20
+ if (!storage) return;
21
+ const keysToRemove = [];
22
+ for (let i = 0; i < storage.length; i++) {
23
+ const key = storage.key(i);
24
+ if (key && key.startsWith(`${prefix}:`)) {
25
+ keysToRemove.push(key);
26
+ }
27
+ }
28
+ keysToRemove.forEach(key => storage.removeItem(key));
29
+ }
5
30
  let storedRegistered = false;
6
31
  function stored(key, defaultValue, options = {}) {
7
32
  if (!storedRegistered) {
@@ -25,16 +50,71 @@ function createStoredSignal(marker) {
25
50
  options: {
26
51
  serialize = JSON.stringify,
27
52
  deserialize = JSON.parse,
28
- debounceMs = 100
53
+ debounceMs = 100,
54
+ version = 1,
55
+ migrate,
56
+ clearOnMigrationFailure = false
29
57
  }
30
58
  } = marker;
31
59
  const storage = marker.options.storage !== undefined ? marker.options.storage : typeof localStorage !== 'undefined' ? localStorage : null;
32
60
  let initialValue = defaultValue;
33
61
  if (storage) {
34
62
  try {
35
- const storedValue = storage.getItem(key);
36
- if (storedValue !== null) {
37
- initialValue = deserialize(storedValue);
63
+ const storedRaw = storage.getItem(key);
64
+ if (storedRaw !== null) {
65
+ const parsed = deserialize(storedRaw);
66
+ if (isVersionedData(parsed)) {
67
+ const storedVersion = parsed.__v;
68
+ let data = parsed.data;
69
+ if (storedVersion !== version && migrate) {
70
+ try {
71
+ data = migrate(data, storedVersion);
72
+ queueMicrotask(() => {
73
+ try {
74
+ const versionedData = {
75
+ __v: version,
76
+ data
77
+ };
78
+ storage.setItem(key, serialize(versionedData));
79
+ } catch {}
80
+ });
81
+ } catch (e) {
82
+ if (typeof ngDevMode === 'undefined' || ngDevMode) {
83
+ console.warn(`SignalTree: Migration failed for "${key}" from v${storedVersion} to v${version}`, e);
84
+ }
85
+ if (clearOnMigrationFailure) {
86
+ storage.removeItem(key);
87
+ }
88
+ data = defaultValue;
89
+ }
90
+ }
91
+ initialValue = data;
92
+ } else {
93
+ if (migrate && version > 0) {
94
+ try {
95
+ initialValue = migrate(parsed, 0);
96
+ queueMicrotask(() => {
97
+ try {
98
+ const versionedData = {
99
+ __v: version,
100
+ data: initialValue
101
+ };
102
+ storage.setItem(key, serialize(versionedData));
103
+ } catch {}
104
+ });
105
+ } catch (e) {
106
+ if (typeof ngDevMode === 'undefined' || ngDevMode) {
107
+ console.warn(`SignalTree: Migration failed for "${key}" from legacy to v${version}`, e);
108
+ }
109
+ if (clearOnMigrationFailure) {
110
+ storage.removeItem(key);
111
+ }
112
+ initialValue = defaultValue;
113
+ }
114
+ } else {
115
+ initialValue = parsed;
116
+ }
117
+ }
38
118
  }
39
119
  } catch (e) {
40
120
  if (typeof ngDevMode === 'undefined' || ngDevMode) {
@@ -43,14 +123,19 @@ function createStoredSignal(marker) {
43
123
  }
44
124
  }
45
125
  const sig = signal(initialValue);
126
+ const currentVersion = version;
46
127
  let pendingWrite = null;
47
128
  let pendingValue;
48
129
  const saveToStorage = value => {
49
130
  if (!storage) return;
131
+ const versionedData = {
132
+ __v: currentVersion,
133
+ data: value
134
+ };
50
135
  if (debounceMs === 0) {
51
136
  queueMicrotask(() => {
52
137
  try {
53
- storage.setItem(key, serialize(value));
138
+ storage.setItem(key, serialize(versionedData));
54
139
  } catch (e) {
55
140
  if (typeof ngDevMode === 'undefined' || ngDevMode) {
56
141
  console.warn(`SignalTree: Failed to save "${key}" to storage`, e);
@@ -67,7 +152,11 @@ function createStoredSignal(marker) {
67
152
  pendingWrite = null;
68
153
  queueMicrotask(() => {
69
154
  try {
70
- storage.setItem(key, serialize(pendingValue));
155
+ const finalData = {
156
+ __v: currentVersion,
157
+ data: pendingValue
158
+ };
159
+ storage.setItem(key, serialize(finalData));
71
160
  } catch (e) {
72
161
  if (typeof ngDevMode === 'undefined' || ngDevMode) {
73
162
  console.warn(`SignalTree: Failed to save "${key}" to storage`, e);
@@ -95,9 +184,14 @@ function createStoredSignal(marker) {
95
184
  storedSignal.reload = () => {
96
185
  if (!storage) return;
97
186
  try {
98
- const storedValue = storage.getItem(key);
99
- if (storedValue !== null) {
100
- sig.set(deserialize(storedValue));
187
+ const storedRaw = storage.getItem(key);
188
+ if (storedRaw !== null) {
189
+ const parsed = deserialize(storedRaw);
190
+ if (isVersionedData(parsed)) {
191
+ sig.set(parsed.data);
192
+ } else {
193
+ sig.set(parsed);
194
+ }
101
195
  } else {
102
196
  sig.set(defaultValue);
103
197
  }
@@ -105,7 +199,15 @@ function createStoredSignal(marker) {
105
199
  sig.set(defaultValue);
106
200
  }
107
201
  };
202
+ Object.defineProperty(storedSignal, 'key', {
203
+ value: key,
204
+ writable: false
205
+ });
206
+ Object.defineProperty(storedSignal, 'version', {
207
+ value: currentVersion,
208
+ writable: false
209
+ });
108
210
  return storedSignal;
109
211
  }
110
212
 
111
- export { STORED_MARKER, createStoredSignal, isStoredMarker, stored };
213
+ export { STORED_MARKER, clearStoragePrefix, createStorageKeys, createStoredSignal, isStoredMarker, stored };
@@ -349,6 +349,7 @@ function createBuilder(baseTree) {
349
349
  });
350
350
  Object.defineProperty(builder, 'with', {
351
351
  value: function (enhancer) {
352
+ finalize();
352
353
  const enhanced = baseTree.with(enhancer);
353
354
  const newBuilder = createBuilder(enhanced);
354
355
  for (const key of Object.keys(enhanced)) {
@@ -358,9 +359,6 @@ function createBuilder(baseTree) {
358
359
  } catch {}
359
360
  }
360
361
  }
361
- for (const factory of derivedQueue) {
362
- newBuilder.derived(factory);
363
- }
364
362
  return newBuilder;
365
363
  },
366
364
  enumerable: false,
package/dist/lib/utils.js CHANGED
@@ -260,5 +260,40 @@ function unwrap(node) {
260
260
  function snapshotState(state) {
261
261
  return unwrap(state);
262
262
  }
263
+ function applyState(stateNode, snapshot) {
264
+ if (snapshot === null || snapshot === undefined) return;
265
+ if (typeof snapshot !== 'object') return;
266
+ for (const key of Object.keys(snapshot)) {
267
+ const val = snapshot[key];
268
+ const target = stateNode[key];
269
+ if (isNodeAccessor(target)) {
270
+ if (val && typeof val === 'object') {
271
+ try {
272
+ applyState(target, val);
273
+ } catch {
274
+ try {
275
+ target(val);
276
+ } catch {}
277
+ }
278
+ } else {
279
+ try {
280
+ target(val);
281
+ } catch {}
282
+ }
283
+ } else if (isSignal(target)) {
284
+ try {
285
+ target.set?.(val);
286
+ } catch {
287
+ try {
288
+ target(val);
289
+ } catch {}
290
+ }
291
+ } else {
292
+ try {
293
+ stateNode[key] = val;
294
+ } catch {}
295
+ }
296
+ }
297
+ }
263
298
 
264
- export { composeEnhancers, createLazySignalTree, isAnySignal, isBuiltInObject, isEntityMapMarker, isNodeAccessor, snapshotState, toWritableSignal, unwrap };
299
+ export { applyState, composeEnhancers, createLazySignalTree, isAnySignal, isBuiltInObject, isEntityMapMarker, isNodeAccessor, snapshotState, toWritableSignal, unwrap };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signaltree/core",
3
- "version": "7.1.6",
3
+ "version": "7.6.0",
4
4
  "description": "Reactive JSON for Angular. JSON branches, reactive leaves. No actions. No reducers. No selectors.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/index.d.ts CHANGED
@@ -6,7 +6,8 @@ export { derivedFrom, externalDerived } from './lib/internals/derived-types';
6
6
  export type { SignalTreeBuilder } from './lib/internals/builder-types';
7
7
  export { isDerivedMarker, type DerivedMarker, type DerivedType, } from './lib/markers/derived';
8
8
  export { status, isStatusMarker, LoadingState, type StatusMarker, type StatusSignal, type StatusConfig, } from './lib/markers/status';
9
- export { stored, isStoredMarker, type StoredMarker, type StoredSignal, type StoredOptions, } from './lib/markers/stored';
9
+ export { stored, isStoredMarker, createStorageKeys, clearStoragePrefix, type StoredMarker, type StoredSignal, type StoredOptions, } from './lib/markers/stored';
10
+ export { form, isFormMarker, createFormSignal, validators, FORM_MARKER, type FormMarker, type FormSignal, type FormConfig, type FormFields, type FormWizard, type WizardConfig, type WizardStepConfig, type Validator, type AsyncValidator, } from './lib/markers/form';
10
11
  export { registerMarkerProcessor } from './lib/internals/materialize-markers';
11
12
  export { equal, deepEqual, isNodeAccessor, isAnySignal, toWritableSignal, parsePath, composeEnhancers, isBuiltInObject, createLazySignalTree, } from './lib/utils';
12
13
  export { createEditSession, type EditSession, type UndoRedoHistory, } from './lib/edit-session';
@@ -1,4 +1,19 @@
1
- import type { EntityConfig, EntityMapMarker } from '../types';
1
+ import { Signal } from '@angular/core';
2
+ import { isEntityMapMarker } from '../utils';
3
+ export { isEntityMapMarker };
4
+ import type { EntityConfig, EntityMapMarker, EntitySignal } from '../types';
5
+ export interface ComputedSliceConfig<E, R> {
6
+ compute: (entities: E[]) => R;
7
+ }
8
+ export type EntitySignalWithSlices<E, K extends string | number, Slices extends Record<string, unknown>> = EntitySignal<E, K> & {
9
+ [P in keyof Slices]: Signal<Slices[P]>;
10
+ };
11
+ export interface EntityMapBuilder<E, K extends string | number, Slices extends Record<string, unknown> = Record<string, never>> extends EntityMapMarker<E, K> {
12
+ __computedSlices?: EntityMapComputedSlices<E>;
13
+ __sliceTypes?: Slices;
14
+ computed<N extends string, R>(name: N, compute: (entities: E[]) => R): EntityMapBuilder<E, K, Slices & Record<N, R>>;
15
+ build(): EntityMapMarkerWithSlices<E, K, Slices>;
16
+ }
2
17
  export declare function entityMap<E, K extends string | number = E extends {
3
18
  id: infer I extends string | number;
4
- } ? I : string>(config?: EntityConfig<E, K>): EntityMapMarker<E, K>;
19
+ } ? I : string>(config?: EntityConfig<E, K>): EntityMapBuilder<E, K, Record<string, never>>;
@@ -0,0 +1,86 @@
1
+ import { Signal } from '@angular/core';
2
+ export declare const FORM_MARKER: unique symbol;
3
+ export type Validator<T> = (value: T) => string | null;
4
+ export type AsyncValidator<T> = (value: T) => Promise<string | null>;
5
+ export interface WizardStepConfig {
6
+ fields?: string[];
7
+ validate?: () => Promise<boolean> | boolean;
8
+ canSkip?: boolean;
9
+ }
10
+ export interface WizardConfig {
11
+ steps: string[];
12
+ stepConfig?: Record<string, WizardStepConfig>;
13
+ stepFields?: Record<string, string[]>;
14
+ }
15
+ export interface FormConfig<T extends Record<string, unknown>> {
16
+ initial: T;
17
+ persist?: string;
18
+ storage?: Storage | null;
19
+ persistDebounceMs?: number;
20
+ validators?: Partial<Record<keyof T, Validator<unknown> | Validator<unknown>[]>>;
21
+ asyncValidators?: Partial<Record<keyof T, AsyncValidator<unknown>>>;
22
+ wizard?: WizardConfig;
23
+ equalityFn?: (a: unknown, b: unknown) => boolean;
24
+ }
25
+ export interface FormMarker<T extends Record<string, unknown>> {
26
+ [FORM_MARKER]: true;
27
+ config: FormConfig<T>;
28
+ }
29
+ export interface FormWizard {
30
+ currentStep: Signal<number>;
31
+ stepName: Signal<string>;
32
+ steps: Signal<string[]>;
33
+ canNext: Signal<boolean>;
34
+ canPrev: Signal<boolean>;
35
+ isLastStep: Signal<boolean>;
36
+ isFirstStep: Signal<boolean>;
37
+ next(): Promise<boolean>;
38
+ prev(): void;
39
+ goTo(step: number | string): Promise<boolean>;
40
+ reset(): void;
41
+ }
42
+ export type FormFields<T> = {
43
+ [K in keyof T]: T[K] extends Record<string, unknown> ? FormFields<T[K]> & {
44
+ (): T[K];
45
+ set(value: T[K]): void;
46
+ } : {
47
+ (): T[K];
48
+ set(value: T[K]): void;
49
+ update(fn: (current: T[K]) => T[K]): void;
50
+ };
51
+ };
52
+ export interface FormSignal<T extends Record<string, unknown>> {
53
+ $: FormFields<T>;
54
+ (): T;
55
+ set(values: Partial<T>): void;
56
+ patch(values: Partial<T>): void;
57
+ reset(): void;
58
+ clear(): void;
59
+ valid: Signal<boolean>;
60
+ dirty: Signal<boolean>;
61
+ submitting: Signal<boolean>;
62
+ touched: Signal<Record<keyof T, boolean>>;
63
+ errors: Signal<Partial<Record<keyof T, string | null>>>;
64
+ errorList: Signal<string[]>;
65
+ validate(): Promise<boolean>;
66
+ validateField(field: keyof T): Promise<boolean>;
67
+ touch(field: keyof T): void;
68
+ touchAll(): void;
69
+ submit<R>(handler: (values: T) => Promise<R>): Promise<R | null>;
70
+ wizard?: FormWizard;
71
+ persistNow(): void;
72
+ reload(): void;
73
+ clearStorage(): void;
74
+ }
75
+ export declare function form<T extends Record<string, unknown>>(config: FormConfig<T>): FormMarker<T>;
76
+ export declare function isFormMarker(value: unknown): value is FormMarker<Record<string, unknown>>;
77
+ export declare const validators: {
78
+ required: (message?: string) => (value: unknown) => string | null;
79
+ minLength: (min: number, message?: string) => (value: unknown) => string | null;
80
+ maxLength: (max: number, message?: string) => (value: unknown) => string | null;
81
+ min: (min: number, message?: string) => (value: unknown) => string | null;
82
+ max: (max: number, message?: string) => (value: unknown) => string | null;
83
+ email: (message?: string) => (value: unknown) => string | null;
84
+ pattern: (regex: RegExp, message?: string) => (value: unknown) => string | null;
85
+ when: <T>(condition: (form: T) => boolean, validator: Validator<unknown>) => (value: unknown, form?: T) => string | null;
86
+ };
@@ -1,3 +1,4 @@
1
1
  export { isDerivedMarker, getDerivedMarkerSymbol, type DerivedMarker, type DerivedType, } from './derived';
2
2
  export { status, isStatusMarker, createStatusSignal, LoadingState, STATUS_MARKER, type StatusMarker, type StatusSignal, type StatusConfig, } from './status';
3
- export { stored, isStoredMarker, createStoredSignal, STORED_MARKER, type StoredMarker, type StoredSignal, type StoredOptions, } from './stored';
3
+ export { stored, isStoredMarker, createStoredSignal, createStorageKeys, clearStoragePrefix, STORED_MARKER, type StoredMarker, type StoredSignal, type StoredOptions, type MigrationFn, } from './stored';
4
+ export { form, isFormMarker, createFormSignal, validators, FORM_MARKER, type FormMarker, type FormSignal, type FormConfig, type FormFields, type FormWizard, type WizardConfig, type WizardStepConfig, type Validator, type AsyncValidator, } from './form';