@signaltree/ng-forms 7.1.3 β†’ 7.3.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
@@ -1,81 +1,129 @@
1
1
  # @signaltree/ng-forms
2
2
 
3
- **Tree-structured signal forms for Angular 21+**. When Angular's native signal forms aren't enoughβ€”add persistence, wizards, history tracking, and nested state management.
3
+ **Angular FormGroup bridge for SignalTree's `form()` marker**. Adds reactive forms integration, conditional fields, and undo/redo to tree-integrated forms.
4
4
 
5
5
  **Bundle size: 3.38KB gzipped**
6
6
 
7
- ## Why ng-forms?
7
+ ## Architecture: form() + formBridge()
8
8
 
9
- Angular 21 introduced native signal forms with `FormField<T>`, which work great for simple, flat forms. **ng-forms is for complex forms** that need:
9
+ SignalTree v7 introduces a layered forms architecture:
10
10
 
11
- ### **🌲 Tree-Structured State**
12
- ```typescript
13
- // Angular 21: Flat fields, no relationships
14
- const name = formField('');
15
- const email = formField('');
16
-
17
- // ng-forms: Hierarchical structure that mirrors your data model
18
- const form = createFormTree({
19
- user: { name: '', email: '' },
20
- address: { street: '', city: '', zip: '' }
21
- });
22
- // Access nested: form.$.user.name()
23
- // Validate paths: 'address.zip'
24
11
  ```
25
-
26
- ### **πŸ’Ύ Auto-Persistence**
27
- ```typescript
28
- // Angular 21: No persistence, build it yourself
29
- // ng-forms: Built-in with debouncing
30
- const form = createFormTree(initialState, {
31
- persistKey: 'checkout-draft',
32
- storage: localStorage,
33
- persistDebounceMs: 500
34
- });
35
- // Auto-saves changes, restores on init
12
+ @signaltree/core @signaltree/ng-forms
13
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
14
+ β”‚ form() marker β”‚ β”‚ formBridge() β”‚
15
+ β”‚ ─────────────────────── β”‚ ───► β”‚ enhancer that: β”‚
16
+ β”‚ β€’ Signal-based fields β”‚ β”‚ β€’ Creates FormGroup β”‚
17
+ β”‚ β€’ Sync/async validators β”‚ β”‚ β€’ Bidirectional sync β”‚
18
+ β”‚ β€’ Persistence β”‚ β”‚ β€’ Conditional fields β”‚
19
+ β”‚ β€’ Wizard navigation β”‚ β”‚ β€’ Angular validators β”‚
20
+ β”‚ β€’ dirty/valid/submittingβ”‚ β”‚ β”‚
21
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ withFormHistory() β”‚
22
+ Works standalone! β”‚ β€’ Undo/redo β”‚
23
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
36
24
  ```
37
25
 
38
- ### **πŸ§™ Wizard & Multi-Step Forms**
26
+ **Key insight**: `form()` is self-sufficient. `formBridge()` adds Angular-specific capabilities.
27
+
28
+ ## Quick Start (Recommended Pattern)
29
+
39
30
  ```typescript
40
- // Angular 21: Build from scratch
41
- // ng-forms: First-class wizard support
42
- const wizard = createWizardForm([
43
- { fields: ['profile.name', 'profile.email'] },
44
- { fields: ['address.street', 'address.city'] }
45
- ], initialValues);
46
- wizard.nextStep(); // Automatic field visibility management
31
+ import { signalTree, form } from '@signaltree/core';
32
+ import { formBridge } from '@signaltree/ng-forms';
33
+
34
+ // Define forms in your tree
35
+ const tree = signalTree({
36
+ checkout: {
37
+ shipping: form({
38
+ initial: { name: '', address: '', zip: '' },
39
+ validators: {
40
+ zip: (v) => (/^\d{5}$/.test(String(v)) ? null : 'Invalid ZIP'),
41
+ },
42
+ persist: 'checkout-shipping',
43
+ }),
44
+ payment: form({
45
+ initial: { card: '', cvv: '' },
46
+ wizard: { steps: ['card', 'review'] },
47
+ }),
48
+ },
49
+ }).with(
50
+ formBridge({
51
+ conditionals: [{ when: (v) => v.checkout.sameAsBilling, fields: ['checkout.shipping.*'] }],
52
+ })
53
+ );
54
+
55
+ // Use in components
56
+ @Component({
57
+ template: `
58
+ <!-- Option 1: Use form() signals directly -->
59
+ <input [value]="tree.$.checkout.shipping.$.name()" (input)="tree.$.checkout.shipping.$.name.set($event.target.value)" />
60
+
61
+ <!-- Option 2: Use Angular FormGroup -->
62
+ <form [formGroup]="shippingForm">
63
+ <input formControlName="name" />
64
+ </form>
65
+ `,
66
+ })
67
+ class CheckoutComponent {
68
+ tree = inject(CHECKOUT_TREE);
69
+
70
+ // Get the FormGroup bridge
71
+ shippingForm = this.tree.getAngularForm('checkout.shipping')?.formGroup;
72
+ }
47
73
  ```
48
74
 
49
- ### **↩️ History / Undo-Redo**
75
+ ## When to Use Each Layer
76
+
77
+ ### form() alone (no ng-forms needed)
78
+
50
79
  ```typescript
51
- // Angular 21: Not available
52
- // ng-forms: Built-in
53
- const form = withFormHistory(createFormTree(initialState));
54
- form.undo();
55
- form.redo();
80
+ // Pure signal forms - works without Angular forms module
81
+ const tree = signalTree({
82
+ login: form({
83
+ initial: { email: '', password: '' },
84
+ validators: { email: validators.email() },
85
+ }),
86
+ });
87
+
88
+ // Full functionality without Angular FormGroup
89
+ tree.$.login.$.email.set('user@test.com');
90
+ tree.$.login.valid(); // Reactive validation
91
+ tree.$.login.validate(); // Trigger validation
92
+ tree.$.login.submit(fn); // Submit handling
93
+ tree.$.login.wizard?.next(); // Wizard navigation (if configured)
56
94
  ```
57
95
 
58
- ### **πŸ”— Reactive Forms Bridge**
96
+ **Use when**: SSR, unit tests, simple forms, non-Angular environments
97
+
98
+ ### form() + formBridge()
99
+
59
100
  ```typescript
60
- // Angular 21: New API, migration required
61
- // ng-forms: Works with existing FormGroup/FormControl
62
- <form [formGroup]="profile.form" (ngSubmit)="save()">
63
- <input formControlName="name" />
64
- </form>
65
- // Signals AND reactive forms, incremental migration
101
+ // Add Angular FormGroup bridge
102
+ const tree = signalTree({
103
+ profile: form({ initial: { name: '' } }),
104
+ }).with(formBridge());
105
+
106
+ // Now you get FormGroup access
107
+ const formGroup = tree.getAngularForm('profile')?.formGroup;
108
+ // Or attached directly: (tree.$.profile as any).formGroup
66
109
  ```
67
110
 
68
- ### **βš™οΈ Declarative Configuration**
111
+ **Use when**: Need `[formGroup]` directives, Angular validators, conditional field disabling
112
+
113
+ ### form() + formBridge() + withFormHistory()
114
+
69
115
  ```typescript
70
- // Angular 21: Per-field imperative setup
71
- // ng-forms: Centralized, glob-pattern configs
72
- fieldConfigs: {
73
- 'email': { validators: [validators.email()], debounceMs: 300 },
74
- 'payment.card.*': { validators: validators.required() }
75
- }
116
+ const tree = signalTree({
117
+ editor: form({ initial: { content: '' } }),
118
+ })
119
+ .with(formBridge())
120
+ .with(withFormHistory({ capacity: 50 }));
121
+
122
+ tree.undo();
123
+ tree.redo();
76
124
  ```
77
125
 
78
- **Use Angular 21 signal forms for simple forms. Use ng-forms for enterprise apps with complex state, persistence, and workflow requirements.**
126
+ **Use when**: Complex editors, need undo/redo
79
127
 
80
128
  ## Installation
81
129
 
@@ -175,11 +223,13 @@ The returned `FormTree` exposes:
175
223
  **ng-forms complements Angular 21's native signal forms**β€”use both in the same app:
176
224
 
177
225
  ### Use Angular 21 `FormField<T>` for:
226
+
178
227
  - βœ… Simple, flat forms (login, search)
179
228
  - βœ… Single-field validation
180
229
  - βœ… Maximum type safety
181
230
 
182
231
  ### Use ng-forms `createFormTree()` for:
232
+
183
233
  - βœ… Nested object structures (user + address + payment)
184
234
  - βœ… Forms with persistence/auto-save
185
235
  - βœ… Wizard/multi-step flows
@@ -188,6 +238,7 @@ The returned `FormTree` exposes:
188
238
  - βœ… Migration from reactive forms
189
239
 
190
240
  ### Hybrid Example: Simple Fields + Complex Tree
241
+
191
242
  ```typescript
192
243
  import { formField } from '@angular/forms';
193
244
  import { createFormTree } from '@signaltree/ng-forms';
@@ -215,6 +266,7 @@ class CheckoutComponent {
215
266
  ```
216
267
 
217
268
  ### Connecting to Reactive Forms
269
+
218
270
  ```ts
219
271
  import { toWritableSignal } from '@signaltree/core';
220
272
 
@@ -325,21 +377,83 @@ Use `SignalValueDirective` to keep standalone signals and `ngModel` fields align
325
377
 
326
378
  ## When to use ng-forms vs Angular 21 signal forms
327
379
 
328
- | Scenario | Recommendation |
329
- |----------|---------------|
330
- | Login form (2-3 fields) | βœ… Angular 21 `FormField` |
331
- | Search bar with filters | βœ… Angular 21 `FormField` |
332
- | User profile with nested address | βœ… **ng-forms** (tree structure) |
333
- | Checkout flow (shipping + payment + items) | βœ… **ng-forms** (persistence + wizard) |
334
- | Multi-step onboarding (5+ steps) | βœ… **ng-forms** (wizard API) |
335
- | Form with auto-save drafts | βœ… **ng-forms** (built-in persistence) |
336
- | Complex editor with undo/redo | βœ… **ng-forms** (history tracking) |
337
- | Migrating from reactive forms | βœ… **ng-forms** (FormGroup bridge) |
338
- | Dynamic form with conditional fields | βœ… **ng-forms** (conditionals config) |
339
- | Form synced with global app state | βœ… **ng-forms** (SignalTree integration) |
380
+ | Scenario | Recommendation |
381
+ | ------------------------------------------ | ---------------------------------------- |
382
+ | Login form (2-3 fields) | βœ… Angular 21 `FormField` |
383
+ | Search bar with filters | βœ… Angular 21 `FormField` |
384
+ | User profile with nested address | βœ… **ng-forms** (tree structure) |
385
+ | Checkout flow (shipping + payment + items) | βœ… **ng-forms** (persistence + wizard) |
386
+ | Multi-step onboarding (5+ steps) | βœ… **ng-forms** (wizard API) |
387
+ | Form with auto-save drafts | βœ… **ng-forms** (built-in persistence) |
388
+ | Complex editor with undo/redo | βœ… **ng-forms** (history tracking) |
389
+ | Migrating from reactive forms | βœ… **ng-forms** (FormGroup bridge) |
390
+ | Dynamic form with conditional fields | βœ… **ng-forms** (conditionals config) |
391
+ | Form synced with global app state | βœ… **ng-forms** (SignalTree integration) |
340
392
 
341
393
  **Rule of thumb**: If your form data is a nested object or needs workflow features (persistence/wizards/history), use ng-forms. For simple flat forms, Angular 21's native signal forms are perfect.
342
394
 
395
+ ## Migration from createFormTree()
396
+
397
+ `createFormTree()` is deprecated in favor of the composable `form()` + `formBridge()` pattern.
398
+
399
+ ### Before (deprecated)
400
+
401
+ ```typescript
402
+ import { createFormTree } from '@signaltree/ng-forms';
403
+
404
+ const form = createFormTree(
405
+ {
406
+ name: '',
407
+ email: '',
408
+ },
409
+ {
410
+ validators: { email: validators.email() },
411
+ persistKey: 'profile-form',
412
+ }
413
+ );
414
+
415
+ // Access
416
+ form.$.name.set('John');
417
+ form.form; // FormGroup
418
+ ```
419
+
420
+ ### After (recommended)
421
+
422
+ ```typescript
423
+ import { signalTree, form } from '@signaltree/core';
424
+ import { formBridge } from '@signaltree/ng-forms';
425
+
426
+ const tree = signalTree({
427
+ profile: form({
428
+ initial: { name: '', email: '' },
429
+ validators: { email: validators.email() },
430
+ persist: 'profile-form',
431
+ }),
432
+ }).with(formBridge());
433
+
434
+ // Access
435
+ tree.$.profile.$.name.set('John');
436
+ tree.getAngularForm('profile')?.formGroup; // FormGroup
437
+ // Or: (tree.$.profile as any).formGroup
438
+ ```
439
+
440
+ ### Key differences
441
+
442
+ | Aspect | createFormTree() | form() + formBridge() |
443
+ | -------------------- | ----------------------- | ---------------------------- |
444
+ | **Standalone** | Always needs Angular | form() works without Angular |
445
+ | **Tree integration** | Separate from app state | Lives in your main tree |
446
+ | **DevTools** | Separate | Inherits tree DevTools |
447
+ | **Composability** | Limited | Add enhancers freely |
448
+ | **Tree-shaking** | All-or-nothing | Only what you use |
449
+
450
+ ### Migration steps
451
+
452
+ 1. Move form state into your SignalTree using `form()` marker
453
+ 2. Add `.with(formBridge())` to your tree
454
+ 3. Update access patterns: `form.$.field` β†’ `tree.$.formName.$.field`
455
+ 4. Update FormGroup access: `form.form` β†’ `tree.getAngularForm('path')?.formGroup`
456
+
343
457
  ## Links
344
458
 
345
459
  - [SignalTree Documentation](https://signaltree.io)
@@ -0,0 +1,194 @@
1
+ import { inject, DestroyRef, computed } from '@angular/core';
2
+ import { FormControl, FormArray, FormGroup } from '@angular/forms';
3
+
4
+ function findFormSignals(node, path, results) {
5
+ if (node === null || node === undefined) return;
6
+ const hasForm = (typeof node === 'function' || typeof node === 'object') && node !== null && '$' in node && 'valid' in node && 'dirty' in node && 'validate' in node;
7
+ if (hasForm && typeof node.validate === 'function') {
8
+ results.push({
9
+ path,
10
+ formSignal: node
11
+ });
12
+ return;
13
+ }
14
+ if (typeof node === 'function' || typeof node === 'object') {
15
+ const entries = Object.entries(node);
16
+ for (const [key, value] of entries) {
17
+ if (key.startsWith('_') || key === 'set' || key === 'update') continue;
18
+ const childPath = path ? `${path}.${key}` : key;
19
+ findFormSignals(value, childPath, results);
20
+ }
21
+ }
22
+ }
23
+ function createFormGroupBridge(formSignal, config, cleanupCallbacks) {
24
+ const values = formSignal();
25
+ const formGroup = createFormGroupFromValues(values, config.fieldConfigs || {});
26
+ const angularErrors = computed(() => {
27
+ const result = {};
28
+ collectControlErrors(formGroup, '', result);
29
+ return result;
30
+ });
31
+ const asyncPending = computed(() => formGroup.pending);
32
+ const syncSignalToControl = () => {
33
+ const currentValues = formSignal();
34
+ patchFormGroupValues(formGroup, currentValues);
35
+ };
36
+ const syncControlToSignal = () => {
37
+ const formValues = formGroup.getRawValue();
38
+ formSignal.set(formValues);
39
+ };
40
+ const subscription = formGroup.valueChanges.subscribe(() => {
41
+ syncControlToSignal();
42
+ });
43
+ cleanupCallbacks.push(() => subscription.unsubscribe());
44
+ syncSignalToControl();
45
+ if (config.conditionals && config.conditionals.length > 0) {
46
+ const conditionalState = new Map();
47
+ const applyConditionals = () => {
48
+ const values = formGroup.getRawValue();
49
+ for (const {
50
+ when,
51
+ fields
52
+ } of config.conditionals) {
53
+ let visible = true;
54
+ try {
55
+ visible = when(values);
56
+ } catch {
57
+ visible = true;
58
+ }
59
+ for (const fieldPath of fields) {
60
+ const control = formGroup.get(fieldPath);
61
+ if (!control) continue;
62
+ const previous = conditionalState.get(fieldPath);
63
+ if (previous === visible) continue;
64
+ conditionalState.set(fieldPath, visible);
65
+ if (visible) {
66
+ control.enable({
67
+ emitEvent: true
68
+ });
69
+ } else {
70
+ control.disable({
71
+ emitEvent: false
72
+ });
73
+ }
74
+ }
75
+ }
76
+ };
77
+ applyConditionals();
78
+ const condSub = formGroup.valueChanges.subscribe(() => applyConditionals());
79
+ cleanupCallbacks.push(() => condSub.unsubscribe());
80
+ }
81
+ return {
82
+ formGroup,
83
+ formControl: path => {
84
+ const control = formGroup.get(path);
85
+ return control instanceof FormControl ? control : null;
86
+ },
87
+ angularErrors,
88
+ asyncPending
89
+ };
90
+ }
91
+ function createFormGroupFromValues(values, fieldConfigs, basePath = '') {
92
+ const controls = {};
93
+ for (const [key, value] of Object.entries(values)) {
94
+ const path = basePath ? `${basePath}.${key}` : key;
95
+ const fieldConfig = fieldConfigs[path] || {};
96
+ if (Array.isArray(value)) {
97
+ const arrayControls = value.map((item, index) => {
98
+ if (typeof item === 'object' && item !== null) {
99
+ return createFormGroupFromValues(item, fieldConfigs, `${path}.${index}`);
100
+ }
101
+ return new FormControl(item);
102
+ });
103
+ controls[key] = new FormArray(arrayControls);
104
+ } else if (typeof value === 'object' && value !== null) {
105
+ controls[key] = createFormGroupFromValues(value, fieldConfigs, path);
106
+ } else {
107
+ const validators = fieldConfig.validators ? Array.isArray(fieldConfig.validators) ? fieldConfig.validators : [fieldConfig.validators] : [];
108
+ const asyncValidators = fieldConfig.asyncValidators ? Array.isArray(fieldConfig.asyncValidators) ? fieldConfig.asyncValidators : [fieldConfig.asyncValidators] : [];
109
+ controls[key] = new FormControl(value, {
110
+ validators,
111
+ asyncValidators
112
+ });
113
+ }
114
+ }
115
+ return new FormGroup(controls);
116
+ }
117
+ function patchFormGroupValues(group, values, basePath) {
118
+ for (const [key, value] of Object.entries(values)) {
119
+ const control = group.get(key);
120
+ if (!control) continue;
121
+ if (control instanceof FormGroup && typeof value === 'object' && value !== null) {
122
+ patchFormGroupValues(control, value);
123
+ } else if (control instanceof FormArray && Array.isArray(value)) {
124
+ control.clear({
125
+ emitEvent: false
126
+ });
127
+ for (const item of value) {
128
+ if (typeof item === 'object' && item !== null) {
129
+ control.push(createFormGroupFromValues(item, {}));
130
+ } else {
131
+ control.push(new FormControl(item));
132
+ }
133
+ }
134
+ } else {
135
+ control.setValue(value, {
136
+ emitEvent: false
137
+ });
138
+ }
139
+ }
140
+ }
141
+ function collectControlErrors(control, path, result) {
142
+ if (control.errors) {
143
+ result[path || 'root'] = control.errors;
144
+ }
145
+ if (control instanceof FormGroup) {
146
+ for (const [key, child] of Object.entries(control.controls)) {
147
+ collectControlErrors(child, path ? `${path}.${key}` : key, result);
148
+ }
149
+ } else if (control instanceof FormArray) {
150
+ control.controls.forEach((child, index) => {
151
+ collectControlErrors(child, `${path}.${index}`, result);
152
+ });
153
+ }
154
+ }
155
+ function formBridge(config = {}) {
156
+ return tree => {
157
+ const cleanupCallbacks = [];
158
+ const formBridgeMap = new Map();
159
+ const formLocations = [];
160
+ findFormSignals(tree.$, '', formLocations);
161
+ for (const {
162
+ path,
163
+ formSignal
164
+ } of formLocations) {
165
+ const bridge = createFormGroupBridge(formSignal, config, cleanupCallbacks);
166
+ formBridgeMap.set(path, bridge);
167
+ formSignal['formGroup'] = bridge.formGroup;
168
+ formSignal['formControl'] = bridge.formControl;
169
+ formSignal['angularErrors'] = bridge.angularErrors;
170
+ formSignal['asyncPending'] = bridge.asyncPending;
171
+ }
172
+ const destroyRef = config.destroyRef ?? tryInjectDestroyRef();
173
+ if (destroyRef) {
174
+ destroyRef.onDestroy(() => {
175
+ cleanupCallbacks.forEach(fn => fn());
176
+ });
177
+ }
178
+ const enhanced = tree;
179
+ enhanced.formBridge = formBridgeMap;
180
+ enhanced.getAngularForm = function (path) {
181
+ return formBridgeMap.get(path) ?? null;
182
+ };
183
+ return enhanced;
184
+ };
185
+ }
186
+ function tryInjectDestroyRef() {
187
+ try {
188
+ return inject(DestroyRef);
189
+ } catch {
190
+ return undefined;
191
+ }
192
+ }
193
+
194
+ export { formBridge };
@@ -0,0 +1,2 @@
1
+ export { formBridge } from './form-bridge.js';
2
+ export { FORM_MARKER, isFormMarker } from '@signaltree/core';
package/dist/index.js CHANGED
@@ -2,3 +2,4 @@ export * from './core/ng-forms.js';
2
2
  export * from './core/validators.js';
3
3
  export * from './core/async-validators.js';
4
4
  export * from './history/index.js';
5
+ export * from './enhancer/index.js';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@signaltree/ng-forms",
3
- "version": "7.1.3",
4
- "description": "Complete Angular forms integration for SignalTree. FormTree creation, custom directives, validators, form state tracking, and RxJS bridge.",
3
+ "version": "7.3.0",
4
+ "description": "Angular forms as reactive JSON. Seamless SignalTree integration with FormTree creation, validators, and form state tracking.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "sideEffects": false,
@@ -0,0 +1,30 @@
1
+ import { DestroyRef, Signal } from '@angular/core';
2
+ import { AsyncValidatorFn, FormControl, FormGroup, ValidationErrors, ValidatorFn } from '@angular/forms';
3
+ import { ISignalTree } from '@signaltree/core';
4
+ export interface AngularConditionalField<T = unknown> {
5
+ when: (values: T) => boolean;
6
+ fields: string[];
7
+ }
8
+ export interface AngularFieldConfig {
9
+ debounceMs?: number;
10
+ validators?: ValidatorFn | ValidatorFn[];
11
+ asyncValidators?: AsyncValidatorFn | AsyncValidatorFn[];
12
+ }
13
+ export interface AngularFormsConfig<T = unknown> {
14
+ conditionals?: AngularConditionalField<T>[];
15
+ fieldConfigs?: Record<string, AngularFieldConfig>;
16
+ destroyRef?: DestroyRef;
17
+ validationBatchMs?: number;
18
+ }
19
+ export interface AngularFormBridge<T extends Record<string, unknown>> {
20
+ formGroup: FormGroup;
21
+ formControl(path: string): FormControl | null;
22
+ angularErrors: Signal<Record<string, ValidationErrors | null>>;
23
+ asyncPending: Signal<boolean>;
24
+ }
25
+ export interface AngularFormsMethods {
26
+ getAngularForm<T extends Record<string, unknown>>(path: string): AngularFormBridge<T> | null;
27
+ formBridge: Map<string, AngularFormBridge<Record<string, unknown>>>;
28
+ }
29
+ export declare function formBridge<TConfig = unknown>(config?: AngularFormsConfig<TConfig>): <T>(tree: ISignalTree<T>) => ISignalTree<T> & AngularFormsMethods;
30
+ export { FORM_MARKER, isFormMarker } from '@signaltree/core';
@@ -0,0 +1 @@
1
+ export * from './form-bridge';
package/src/index.d.ts CHANGED
@@ -2,3 +2,4 @@ export * from './core/ng-forms';
2
2
  export * from './core/validators';
3
3
  export * from './core/async-validators';
4
4
  export * from './history';
5
+ export * from './enhancer';
@@ -1,74 +0,0 @@
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 };
@@ -1 +0,0 @@
1
- export { createAuditCallback, createAuditTracker } from './audit.js';
@@ -1,11 +0,0 @@
1
- function getChanges(oldState, newState) {
2
- const changes = {};
3
- for (const key in newState) {
4
- if (oldState[key] !== newState[key]) {
5
- changes[key] = newState[key];
6
- }
7
- }
8
- return changes;
9
- }
10
-
11
- export { getChanges };