@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 +136 -24
- package/dist/constants.js +6 -0
- package/dist/core/async-validators.js +24 -0
- package/dist/core/ng-forms.js +880 -0
- package/dist/core/validators.js +56 -0
- package/dist/deep-clone.js +80 -0
- package/dist/history/history.js +113 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4 -0
- package/dist/lru-cache.js +64 -0
- package/dist/match-path.js +13 -0
- package/dist/merge-deep.js +26 -0
- package/dist/parse-path.js +13 -0
- package/dist/snapshots-equal.js +5 -0
- package/dist/tslib.es6.js +34 -0
- package/package.json +9 -30
- package/LICENSE +0 -54
package/README.md
CHANGED
|
@@ -1,16 +1,89 @@
|
|
|
1
1
|
# @signaltree/ng-forms
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
>
|
|
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
|
-
##
|
|
173
|
+
## Angular 21 Interoperability
|
|
101
174
|
|
|
102
|
-
|
|
175
|
+
**ng-forms complements Angular 21's native signal forms**—use both in the same app:
|
|
103
176
|
|
|
104
|
-
|
|
105
|
-
-
|
|
177
|
+
### Use Angular 21 `FormField<T>` for:
|
|
178
|
+
- ✅ Simple, flat forms (login, search)
|
|
179
|
+
- ✅ Single-field validation
|
|
180
|
+
- ✅ Maximum type safety
|
|
106
181
|
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
//
|
|
119
|
-
|
|
120
|
-
userGroupControl.connect(userSignal);
|
|
213
|
+
// Both work together seamlessly
|
|
214
|
+
}
|
|
121
215
|
```
|
|
122
216
|
|
|
123
|
-
|
|
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
|
|
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
|
-
|
|
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,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 };
|