@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 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
- profile = createFormTree<ProfileForm>(
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
- // Persist values to your API or service layer here
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 `FormGroup` for templates and directives
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signaltree/ng-forms",
3
- "version": "7.3.4",
3
+ "version": "7.6.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",
@@ -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';
package/dist/constants.js DELETED
@@ -1,6 +0,0 @@
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 };
@@ -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 };