@signaltree/ng-forms 4.1.2 → 4.1.5

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,16 +1,89 @@
1
1
  # @signaltree/ng-forms
2
2
 
3
- Angular 20 signal forms meet SignalTree. `@signaltree/ng-forms` keeps your form state, validation, persistence, and wizard flows in sync with the rest of your application signals—no manual plumbing.
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.
4
4
 
5
5
  **Bundle size: 3.38KB gzipped**
6
6
 
7
+ ## Why ng-forms?
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:
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
+ ```
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
36
+ ```
37
+
38
+ ### **🧙 Wizard & Multi-Step Forms**
39
+ ```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
47
+ ```
48
+
49
+ ### **↩️ History / Undo-Redo**
50
+ ```typescript
51
+ // Angular 21: Not available
52
+ // ng-forms: Built-in
53
+ const form = withFormHistory(createFormTree(initialState));
54
+ form.undo();
55
+ form.redo();
56
+ ```
57
+
58
+ ### **🔗 Reactive Forms Bridge**
59
+ ```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
66
+ ```
67
+
68
+ ### **⚙️ Declarative Configuration**
69
+ ```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
+ }
76
+ ```
77
+
78
+ **Use Angular 21 signal forms for simple forms. Use ng-forms for enterprise apps with complex state, persistence, and workflow requirements.**
79
+
7
80
  ## Installation
8
81
 
9
82
  ```bash
10
83
  pnpm add @signaltree/core @signaltree/ng-forms
11
84
  ```
12
85
 
13
- > This package supports Angular 17+ with TypeScript 5.5+. Angular 17-19 support uses a legacy bridge that will be deprecated when Angular 21 is released. For the best experience, upgrade to Angular 20.3+ to use native Signal Forms.
86
+ > **Compatibility**: Angular 17+ with TypeScript 5.5+. Angular 21+ recommended for best experience. Works alongside Angular's native signal forms—use both where appropriate.
14
87
 
15
88
  ## Quick start
16
89
 
@@ -97,30 +170,58 @@ The returned `FormTree` exposes:
97
170
  - **Signal ↔ Observable bridge**: Convert signals to RxJS streams for interoperability
98
171
  - **Template-driven adapter**: `SignalValueDirective` bridges standalone signals with `ngModel`
99
172
 
100
- ## Signal Forms (Angular 20+)
173
+ ## Angular 21 Interoperability
101
174
 
102
- `@signaltree/ng-forms` now prefers Angular's experimental Signal Forms `connect()` API when available.
175
+ **ng-forms complements Angular 21's native signal forms**—use both in the same app:
103
176
 
104
- - Leaves in a SignalTree are native `WritableSignal<T>` and can be connected directly
105
- - For object slices, convert to a `WritableSignal<T>` via `toWritableSignal()` from `@signaltree/core`
177
+ ### Use Angular 21 `FormField<T>` for:
178
+ - Simple, flat forms (login, search)
179
+ - ✅ Single-field validation
180
+ - ✅ Maximum type safety
106
181
 
107
- ```ts
108
- import { toWritableSignal } from '@signaltree/core';
182
+ ### Use ng-forms `createFormTree()` for:
183
+ - Nested object structures (user + address + payment)
184
+ - ✅ Forms with persistence/auto-save
185
+ - ✅ Wizard/multi-step flows
186
+ - ✅ History/undo requirements
187
+ - ✅ Complex conditional logic
188
+ - ✅ Migration from reactive forms
109
189
 
110
- const values = createFormTree({
111
- user: { name: '', email: '' },
112
- });
113
-
114
- // Connect leaves directly
115
- nameControl.connect(values.$.user.name);
116
- emailControl.connect(values.$.user.email);
190
+ ### Hybrid Example: Simple Fields + Complex Tree
191
+ ```typescript
192
+ import { formField } from '@angular/forms';
193
+ import { createFormTree } from '@signaltree/ng-forms';
194
+
195
+ @Component({...})
196
+ class CheckoutComponent {
197
+ // Simple field: Use Angular 21 native
198
+ promoCode = formField('');
199
+
200
+ // Complex nested state: Use ng-forms
201
+ checkout = createFormTree({
202
+ shipping: { name: '', address: '', city: '', zip: '' },
203
+ payment: { card: '', cvv: '', expiry: '' },
204
+ items: [] as CartItem[]
205
+ }, {
206
+ persistKey: 'checkout-draft',
207
+ fieldConfigs: {
208
+ 'shipping.zip': { validators: validators.zipCode() },
209
+ 'payment.card': { validators: validators.creditCard(), debounceMs: 300 }
210
+ }
211
+ });
117
212
 
118
- // Or connect a whole slice
119
- const userSignal = toWritableSignal(values.values.$.user);
120
- userGroupControl.connect(userSignal);
213
+ // Both work together seamlessly
214
+ }
121
215
  ```
122
216
 
123
- Angular 20.3+ is preferred for native Signal Forms `connect()`. Angular 17-19 is supported via a legacy bridge that will be deprecated when Angular 21 is released.
217
+ ### Connecting to Reactive Forms
218
+ ```ts
219
+ import { toWritableSignal } from '@signaltree/core';
220
+
221
+ // Convert ng-forms signals to work with Angular's .connect()
222
+ const nameSignal = toWritableSignal(formTree.$.user.name);
223
+ reactiveControl.connect(nameSignal);
224
+ ```
124
225
 
125
226
  ## Form tree configuration
126
227
 
@@ -222,16 +323,27 @@ History tracking works at the FormGroup level so it plays nicely with external u
222
323
 
223
324
  Use `SignalValueDirective` to keep standalone signals and `ngModel` fields aligned in legacy sections while new pages migrate to forms-first APIs.
224
325
 
225
- ## When to reach for ng-forms
326
+ ## When to use ng-forms vs Angular 21 signal forms
327
+
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) |
226
340
 
227
- - Complex Angular forms that need to remain in sync with SignalTree application state
228
- - Workflows that require persistence, auto-save, or offline drafts
229
- - Multi-step wizards or surveys with dynamic branching
230
- - Applications that benefit from first-class signal APIs around Angular forms
341
+ **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.
231
342
 
232
343
  ## Links
233
344
 
234
345
  - [SignalTree Documentation](https://signaltree.io)
346
+ - [Angular 21 Migration Guide](./ANGULAR21-MIGRATION.md)
235
347
  - [Core Package](https://www.npmjs.com/package/@signaltree/core)
236
348
  - [GitHub Repository](https://github.com/JBorgia/signaltree)
237
349
  - [Demo Application](https://signaltree.io/examples)
@@ -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 };