@signaltree/ng-forms 7.3.0 → 7.3.2

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
@@ -77,11 +77,14 @@ class CheckoutComponent {
77
77
  ### form() alone (no ng-forms needed)
78
78
 
79
79
  ```typescript
80
+ import { signalTree, form } from '@signaltree/core';
81
+ import { email } from '@signaltree/ng-forms';
82
+
80
83
  // Pure signal forms - works without Angular forms module
81
84
  const tree = signalTree({
82
85
  login: form({
83
86
  initial: { email: '', password: '' },
84
- validators: { email: validators.email() },
87
+ validators: { email: email() },
85
88
  }),
86
89
  });
87
90
 
@@ -137,9 +140,9 @@ pnpm add @signaltree/core @signaltree/ng-forms
137
140
 
138
141
  ```typescript
139
142
  import { Component } from '@angular/core';
140
- import { createFormTree, validators } from '@signaltree/ng-forms';
143
+ import { createFormTree, required, email } from '@signaltree/ng-forms';
141
144
 
142
- interface ProfileForm {
145
+ interface ProfileForm extends Record<string, unknown> {
143
146
  name: string;
144
147
  email: string;
145
148
  marketing: boolean;
@@ -182,9 +185,9 @@ export class ProfileFormComponent {
182
185
  persistKey: 'profile-form',
183
186
  storage: this.storage,
184
187
  fieldConfigs: {
185
- name: { validators: validators.required('Name is required') },
188
+ name: { validators: [required('Name is required')] },
186
189
  email: {
187
- validators: [validators.required(), validators.email()],
190
+ validators: [required(), email()],
188
191
  debounceMs: 150,
189
192
  },
190
193
  },
@@ -256,8 +259,8 @@ class CheckoutComponent {
256
259
  }, {
257
260
  persistKey: 'checkout-draft',
258
261
  fieldConfigs: {
259
- 'shipping.zip': { validators: validators.zipCode() },
260
- 'payment.card': { validators: validators.creditCard(), debounceMs: 300 }
262
+ 'shipping.zip': { validators: [(v) => /^\d{5}$/.test(String(v)) ? null : 'Invalid ZIP'] },
263
+ 'payment.card': { validators: [(v) => /^\d{13,19}$/.test(String(v)) ? null : 'Invalid card'], debounceMs: 300 }
261
264
  }
262
265
  });
263
266
 
@@ -287,7 +290,7 @@ const checkout = createFormTree(initialState, {
287
290
  },
288
291
  fieldConfigs: {
289
292
  'payment.card.number': { debounceMs: 200 },
290
- 'preferences.*': { validators: validators.required() },
293
+ 'preferences.*': { validators: [required()] },
291
294
  },
292
295
  conditionals: [
293
296
  {
@@ -399,7 +402,7 @@ Use `SignalValueDirective` to keep standalone signals and `ngModel` fields align
399
402
  ### Before (deprecated)
400
403
 
401
404
  ```typescript
402
- import { createFormTree } from '@signaltree/ng-forms';
405
+ import { createFormTree, email } from '@signaltree/ng-forms';
403
406
 
404
407
  const form = createFormTree(
405
408
  {
@@ -407,7 +410,7 @@ const form = createFormTree(
407
410
  email: '',
408
411
  },
409
412
  {
410
- validators: { email: validators.email() },
413
+ validators: { email: email() },
411
414
  persistKey: 'profile-form',
412
415
  }
413
416
  );
@@ -421,12 +424,12 @@ form.form; // FormGroup
421
424
 
422
425
  ```typescript
423
426
  import { signalTree, form } from '@signaltree/core';
424
- import { formBridge } from '@signaltree/ng-forms';
427
+ import { formBridge, email } from '@signaltree/ng-forms';
425
428
 
426
429
  const tree = signalTree({
427
430
  profile: form({
428
431
  initial: { name: '', email: '' },
429
- validators: { email: validators.email() },
432
+ validators: { email: email() },
430
433
  persist: 'profile-form',
431
434
  }),
432
435
  }).with(formBridge());
@@ -0,0 +1,74 @@
1
+ import { getChanges } from '../get-changes.js';
2
+
3
+ function createAuditTracker(tree, auditLog, config = {}) {
4
+ const {
5
+ getMetadata,
6
+ includePreviousValues = false,
7
+ filter,
8
+ maxEntries = 0
9
+ } = config;
10
+ let previousState = structuredClone(tree());
11
+ let isTracking = true;
12
+ const handleChange = () => {
13
+ if (!isTracking) return;
14
+ const currentState = tree();
15
+ const changes = getChanges(previousState, currentState);
16
+ if (Object.keys(changes).length > 0) {
17
+ if (filter && !filter(changes)) {
18
+ previousState = structuredClone(currentState);
19
+ return;
20
+ }
21
+ const entry = {
22
+ timestamp: Date.now(),
23
+ changes,
24
+ metadata: getMetadata?.()
25
+ };
26
+ if (includePreviousValues) {
27
+ const prevValues = {};
28
+ for (const key of Object.keys(changes)) {
29
+ prevValues[key] = previousState[key];
30
+ }
31
+ entry.previousValues = prevValues;
32
+ }
33
+ auditLog.push(entry);
34
+ if (maxEntries > 0 && auditLog.length > maxEntries) {
35
+ auditLog.splice(0, auditLog.length - maxEntries);
36
+ }
37
+ }
38
+ previousState = structuredClone(currentState);
39
+ };
40
+ let unsubscribe;
41
+ let pollingId;
42
+ if ('subscribe' in tree && typeof tree.subscribe === 'function') {
43
+ try {
44
+ unsubscribe = tree.subscribe(handleChange);
45
+ } catch {
46
+ pollingId = setInterval(handleChange, 100);
47
+ }
48
+ } else {
49
+ pollingId = setInterval(handleChange, 100);
50
+ }
51
+ return () => {
52
+ isTracking = false;
53
+ if (unsubscribe) {
54
+ unsubscribe();
55
+ }
56
+ if (pollingId) {
57
+ clearInterval(pollingId);
58
+ }
59
+ };
60
+ }
61
+ function createAuditCallback(auditLog, getMetadata) {
62
+ return (previousState, currentState) => {
63
+ const changes = getChanges(previousState, currentState);
64
+ if (Object.keys(changes).length > 0) {
65
+ auditLog.push({
66
+ timestamp: Date.now(),
67
+ changes,
68
+ metadata: getMetadata?.()
69
+ });
70
+ }
71
+ };
72
+ }
73
+
74
+ export { createAuditCallback, createAuditTracker };
@@ -0,0 +1 @@
1
+ export { createAuditCallback, createAuditTracker } from './audit.js';
@@ -0,0 +1,6 @@
1
+ const SHARED_DEFAULTS = Object.freeze({
2
+ PATH_CACHE_SIZE: 1000
3
+ });
4
+ const DEFAULT_PATH_CACHE_SIZE = SHARED_DEFAULTS.PATH_CACHE_SIZE;
5
+
6
+ export { DEFAULT_PATH_CACHE_SIZE, SHARED_DEFAULTS };
@@ -0,0 +1,24 @@
1
+ import { isObservable, firstValueFrom } from 'rxjs';
2
+
3
+ function unique(checkFn, message = 'Already exists') {
4
+ return async value => {
5
+ if (!value) return null;
6
+ const exists = await checkFn(value);
7
+ return exists ? message : null;
8
+ };
9
+ }
10
+ function debounce(validator, delayMs) {
11
+ let timeoutId;
12
+ return async value => {
13
+ return new Promise(resolve => {
14
+ clearTimeout(timeoutId);
15
+ timeoutId = setTimeout(async () => {
16
+ const maybeAsync = validator(value);
17
+ const result = isObservable(maybeAsync) ? await firstValueFrom(maybeAsync) : await maybeAsync;
18
+ resolve(result);
19
+ }, delayMs);
20
+ });
21
+ };
22
+ }
23
+
24
+ export { debounce, unique };