@signaltree/ng-forms 7.3.4 → 7.6.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.
- package/README.md +57 -9
- package/package.json +1 -1
- package/dist/audit/audit.js +0 -74
- package/dist/audit/index.js +0 -1
- package/dist/constants.js +0 -6
- package/dist/core/async-validators.js +0 -24
- package/dist/core/ng-forms.js +0 -895
- package/dist/core/validators.js +0 -56
- package/dist/deep-clone.js +0 -80
- package/dist/enhancer/form-bridge.js +0 -195
- package/dist/get-changes.js +0 -11
- package/dist/history/history.js +0 -113
- package/dist/index.js +0 -5
- package/dist/lru-cache.js +0 -64
- package/dist/match-path.js +0 -13
- package/dist/merge-deep.js +0 -26
- package/dist/parse-path.js +0 -13
- package/dist/snapshots-equal.js +0 -5
- package/dist/tslib.es6.js +0 -34
- package/dist/wizard/wizard.js +0 -77
- package/src/audit/audit.d.ts +0 -21
- package/src/audit/index.d.ts +0 -1
- package/src/core/async-validators.d.ts +0 -3
- package/src/core/ng-forms.d.ts +0 -96
- package/src/core/validators.d.ts +0 -9
- package/src/enhancer/form-bridge.d.ts +0 -30
- package/src/enhancer/index.d.ts +0 -1
- package/src/history/history.d.ts +0 -21
- package/src/history/index.d.ts +0 -1
- package/src/index.d.ts +0 -5
- package/src/rxjs/index.d.ts +0 -1
- package/src/rxjs/public-api.d.ts +0 -1
- package/src/rxjs/rxjs-bridge.d.ts +0 -3
- package/src/wizard/index.d.ts +0 -1
- package/src/wizard/wizard.d.ts +0 -19
package/README.md
CHANGED
|
@@ -142,12 +142,6 @@ pnpm add @signaltree/core @signaltree/ng-forms
|
|
|
142
142
|
import { Component } from '@angular/core';
|
|
143
143
|
import { createFormTree, required, email } from '@signaltree/ng-forms';
|
|
144
144
|
|
|
145
|
-
interface ProfileForm extends Record<string, unknown> {
|
|
146
|
-
name: string;
|
|
147
|
-
email: string;
|
|
148
|
-
marketing: boolean;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
145
|
@Component({
|
|
152
146
|
selector: 'app-profile-form',
|
|
153
147
|
template: `
|
|
@@ -175,7 +169,8 @@ interface ProfileForm extends Record<string, unknown> {
|
|
|
175
169
|
export class ProfileFormComponent {
|
|
176
170
|
private storage = typeof window !== 'undefined' ? window.localStorage : undefined;
|
|
177
171
|
|
|
178
|
-
|
|
172
|
+
// Type is inferred from initial values - no interface needed!
|
|
173
|
+
profile = createFormTree(
|
|
179
174
|
{
|
|
180
175
|
name: '',
|
|
181
176
|
email: '',
|
|
@@ -196,7 +191,7 @@ export class ProfileFormComponent {
|
|
|
196
191
|
|
|
197
192
|
async save() {
|
|
198
193
|
await this.profile.submit(async (values) => {
|
|
199
|
-
//
|
|
194
|
+
// values is typed as { name: string; email: string; marketing: boolean }
|
|
200
195
|
console.log('Saving profile', values);
|
|
201
196
|
});
|
|
202
197
|
}
|
|
@@ -205,11 +200,64 @@ export class ProfileFormComponent {
|
|
|
205
200
|
|
|
206
201
|
The returned `FormTree` exposes:
|
|
207
202
|
|
|
208
|
-
- `form`: Angular `
|
|
203
|
+
- `form`: Angular `TypedFormGroup<T>` for templates and directives (fully typed!)
|
|
209
204
|
- `$` / `state`: signal-backed access to individual fields
|
|
210
205
|
- `errors`, `asyncErrors`, `valid`, `dirty`, `submitting`: writable signals for UI state
|
|
211
206
|
- Helpers such as `setValue`, `setValues`, `reset`, `validate`, and `submit`
|
|
212
207
|
|
|
208
|
+
## Type Inference
|
|
209
|
+
|
|
210
|
+
`createFormTree()` leverages recursive type inference—types flow from initial values:
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
// ✅ Simple case: types inferred automatically
|
|
214
|
+
const form = createFormTree({
|
|
215
|
+
name: '', // string
|
|
216
|
+
age: 0, // number
|
|
217
|
+
active: false, // boolean
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
form.$.name(); // string
|
|
221
|
+
form.$.age(); // number
|
|
222
|
+
form.form.controls.name; // FormControl<string>
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Union Types Need Assertions
|
|
226
|
+
|
|
227
|
+
When a field can be one of several specific values, TypeScript widens the inferred type to `string`. Use inline type assertions to preserve narrowness:
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
// ❌ Without assertion: resolution is inferred as string
|
|
231
|
+
const form = createFormTree({
|
|
232
|
+
resolution: 'PENDING', // Inferred as string, not the union
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// ✅ With assertion: resolution is the exact union type
|
|
236
|
+
const form = createFormTree({
|
|
237
|
+
resolution: 'PENDING' as 'PENDING' | 'APPROVED' | 'REJECTED',
|
|
238
|
+
category: null as CategoryType | null,
|
|
239
|
+
items: [] as string[],
|
|
240
|
+
});
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### TypedFormGroup
|
|
244
|
+
|
|
245
|
+
The `form` property returns `TypedFormGroup<T>`, which recursively maps your form shape to Angular controls:
|
|
246
|
+
|
|
247
|
+
```typescript
|
|
248
|
+
type TypedFormGroup<T> = FormGroup<{
|
|
249
|
+
[K in keyof T]: T[K] extends unknown[]
|
|
250
|
+
? FormArray<FormControl<T[K][number]>>
|
|
251
|
+
: T[K] extends object
|
|
252
|
+
? FormGroup<...> // Nested objects become nested FormGroups
|
|
253
|
+
: FormControl<T[K]>
|
|
254
|
+
}>;
|
|
255
|
+
|
|
256
|
+
// Result: full autocomplete and type checking
|
|
257
|
+
const form = createFormTree({ user: { name: '', email: '' } });
|
|
258
|
+
form.form.controls.user.controls.name.value; // string
|
|
259
|
+
```
|
|
260
|
+
|
|
213
261
|
## Core capabilities
|
|
214
262
|
|
|
215
263
|
- **Signal-synced forms**: Bidirectional sync between Angular FormControls and SignalTree signals
|
package/package.json
CHANGED
package/dist/audit/audit.js
DELETED
|
@@ -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 };
|
package/dist/audit/index.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { createAuditCallback, createAuditTracker } from './audit.js';
|
package/dist/constants.js
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
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 };
|