@signaltree/ng-forms 7.1.4 β†’ 7.3.1

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.
Files changed (2) hide show
  1. package/README.md +192 -75
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,81 +1,132 @@
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
+ import { signalTree, form } from '@signaltree/core';
81
+ import { email } from '@signaltree/ng-forms';
82
+
83
+ // Pure signal forms - works without Angular forms module
84
+ const tree = signalTree({
85
+ login: form({
86
+ initial: { email: '', password: '' },
87
+ validators: { email: email() },
88
+ }),
89
+ });
90
+
91
+ // Full functionality without Angular FormGroup
92
+ tree.$.login.$.email.set('user@test.com');
93
+ tree.$.login.valid(); // Reactive validation
94
+ tree.$.login.validate(); // Trigger validation
95
+ tree.$.login.submit(fn); // Submit handling
96
+ tree.$.login.wizard?.next(); // Wizard navigation (if configured)
56
97
  ```
57
98
 
58
- ### **πŸ”— Reactive Forms Bridge**
99
+ **Use when**: SSR, unit tests, simple forms, non-Angular environments
100
+
101
+ ### form() + formBridge()
102
+
59
103
  ```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
104
+ // Add Angular FormGroup bridge
105
+ const tree = signalTree({
106
+ profile: form({ initial: { name: '' } }),
107
+ }).with(formBridge());
108
+
109
+ // Now you get FormGroup access
110
+ const formGroup = tree.getAngularForm('profile')?.formGroup;
111
+ // Or attached directly: (tree.$.profile as any).formGroup
66
112
  ```
67
113
 
68
- ### **βš™οΈ Declarative Configuration**
114
+ **Use when**: Need `[formGroup]` directives, Angular validators, conditional field disabling
115
+
116
+ ### form() + formBridge() + withFormHistory()
117
+
69
118
  ```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
- }
119
+ const tree = signalTree({
120
+ editor: form({ initial: { content: '' } }),
121
+ })
122
+ .with(formBridge())
123
+ .with(withFormHistory({ capacity: 50 }));
124
+
125
+ tree.undo();
126
+ tree.redo();
76
127
  ```
77
128
 
78
- **Use Angular 21 signal forms for simple forms. Use ng-forms for enterprise apps with complex state, persistence, and workflow requirements.**
129
+ **Use when**: Complex editors, need undo/redo
79
130
 
80
131
  ## Installation
81
132
 
@@ -89,9 +140,9 @@ pnpm add @signaltree/core @signaltree/ng-forms
89
140
 
90
141
  ```typescript
91
142
  import { Component } from '@angular/core';
92
- import { createFormTree, validators } from '@signaltree/ng-forms';
143
+ import { createFormTree, required, email } from '@signaltree/ng-forms';
93
144
 
94
- interface ProfileForm {
145
+ interface ProfileForm extends Record<string, unknown> {
95
146
  name: string;
96
147
  email: string;
97
148
  marketing: boolean;
@@ -134,9 +185,9 @@ export class ProfileFormComponent {
134
185
  persistKey: 'profile-form',
135
186
  storage: this.storage,
136
187
  fieldConfigs: {
137
- name: { validators: validators.required('Name is required') },
188
+ name: { validators: [required('Name is required')] },
138
189
  email: {
139
- validators: [validators.required(), validators.email()],
190
+ validators: [required(), email()],
140
191
  debounceMs: 150,
141
192
  },
142
193
  },
@@ -175,11 +226,13 @@ The returned `FormTree` exposes:
175
226
  **ng-forms complements Angular 21's native signal forms**β€”use both in the same app:
176
227
 
177
228
  ### Use Angular 21 `FormField<T>` for:
229
+
178
230
  - βœ… Simple, flat forms (login, search)
179
231
  - βœ… Single-field validation
180
232
  - βœ… Maximum type safety
181
233
 
182
234
  ### Use ng-forms `createFormTree()` for:
235
+
183
236
  - βœ… Nested object structures (user + address + payment)
184
237
  - βœ… Forms with persistence/auto-save
185
238
  - βœ… Wizard/multi-step flows
@@ -188,6 +241,7 @@ The returned `FormTree` exposes:
188
241
  - βœ… Migration from reactive forms
189
242
 
190
243
  ### Hybrid Example: Simple Fields + Complex Tree
244
+
191
245
  ```typescript
192
246
  import { formField } from '@angular/forms';
193
247
  import { createFormTree } from '@signaltree/ng-forms';
@@ -205,8 +259,8 @@ class CheckoutComponent {
205
259
  }, {
206
260
  persistKey: 'checkout-draft',
207
261
  fieldConfigs: {
208
- 'shipping.zip': { validators: validators.zipCode() },
209
- '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 }
210
264
  }
211
265
  });
212
266
 
@@ -215,6 +269,7 @@ class CheckoutComponent {
215
269
  ```
216
270
 
217
271
  ### Connecting to Reactive Forms
272
+
218
273
  ```ts
219
274
  import { toWritableSignal } from '@signaltree/core';
220
275
 
@@ -235,7 +290,7 @@ const checkout = createFormTree(initialState, {
235
290
  },
236
291
  fieldConfigs: {
237
292
  'payment.card.number': { debounceMs: 200 },
238
- 'preferences.*': { validators: validators.required() },
293
+ 'preferences.*': { validators: [required()] },
239
294
  },
240
295
  conditionals: [
241
296
  {
@@ -325,21 +380,83 @@ Use `SignalValueDirective` to keep standalone signals and `ngModel` fields align
325
380
 
326
381
  ## When to use ng-forms vs Angular 21 signal forms
327
382
 
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) |
383
+ | Scenario | Recommendation |
384
+ | ------------------------------------------ | ---------------------------------------- |
385
+ | Login form (2-3 fields) | βœ… Angular 21 `FormField` |
386
+ | Search bar with filters | βœ… Angular 21 `FormField` |
387
+ | User profile with nested address | βœ… **ng-forms** (tree structure) |
388
+ | Checkout flow (shipping + payment + items) | βœ… **ng-forms** (persistence + wizard) |
389
+ | Multi-step onboarding (5+ steps) | βœ… **ng-forms** (wizard API) |
390
+ | Form with auto-save drafts | βœ… **ng-forms** (built-in persistence) |
391
+ | Complex editor with undo/redo | βœ… **ng-forms** (history tracking) |
392
+ | Migrating from reactive forms | βœ… **ng-forms** (FormGroup bridge) |
393
+ | Dynamic form with conditional fields | βœ… **ng-forms** (conditionals config) |
394
+ | Form synced with global app state | βœ… **ng-forms** (SignalTree integration) |
340
395
 
341
396
  **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
397
 
398
+ ## Migration from createFormTree()
399
+
400
+ `createFormTree()` is deprecated in favor of the composable `form()` + `formBridge()` pattern.
401
+
402
+ ### Before (deprecated)
403
+
404
+ ```typescript
405
+ import { createFormTree, email } from '@signaltree/ng-forms';
406
+
407
+ const form = createFormTree(
408
+ {
409
+ name: '',
410
+ email: '',
411
+ },
412
+ {
413
+ validators: { email: email() },
414
+ persistKey: 'profile-form',
415
+ }
416
+ );
417
+
418
+ // Access
419
+ form.$.name.set('John');
420
+ form.form; // FormGroup
421
+ ```
422
+
423
+ ### After (recommended)
424
+
425
+ ```typescript
426
+ import { signalTree, form } from '@signaltree/core';
427
+ import { formBridge, email } from '@signaltree/ng-forms';
428
+
429
+ const tree = signalTree({
430
+ profile: form({
431
+ initial: { name: '', email: '' },
432
+ validators: { email: email() },
433
+ persist: 'profile-form',
434
+ }),
435
+ }).with(formBridge());
436
+
437
+ // Access
438
+ tree.$.profile.$.name.set('John');
439
+ tree.getAngularForm('profile')?.formGroup; // FormGroup
440
+ // Or: (tree.$.profile as any).formGroup
441
+ ```
442
+
443
+ ### Key differences
444
+
445
+ | Aspect | createFormTree() | form() + formBridge() |
446
+ | -------------------- | ----------------------- | ---------------------------- |
447
+ | **Standalone** | Always needs Angular | form() works without Angular |
448
+ | **Tree integration** | Separate from app state | Lives in your main tree |
449
+ | **DevTools** | Separate | Inherits tree DevTools |
450
+ | **Composability** | Limited | Add enhancers freely |
451
+ | **Tree-shaking** | All-or-nothing | Only what you use |
452
+
453
+ ### Migration steps
454
+
455
+ 1. Move form state into your SignalTree using `form()` marker
456
+ 2. Add `.with(formBridge())` to your tree
457
+ 3. Update access patterns: `form.$.field` β†’ `tree.$.formName.$.field`
458
+ 4. Update FormGroup access: `form.form` β†’ `tree.getAngularForm('path')?.formGroup`
459
+
343
460
  ## Links
344
461
 
345
462
  - [SignalTree Documentation](https://signaltree.io)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signaltree/ng-forms",
3
- "version": "7.1.4",
3
+ "version": "7.3.1",
4
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",