@signaltree/ng-forms 7.3.0 → 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.
package/README.md CHANGED
@@ -77,11 +77,14 @@ class CheckoutComponent {
77
77
  ### form() alone (no ng-forms needed)
78
78
 
79
79
  ```typescript
80
+ import { signalTree, form } from '@signaltree/core';
81
+ import { email } from '@signaltree/ng-forms';
82
+
80
83
  // Pure signal forms - works without Angular forms module
81
84
  const tree = signalTree({
82
85
  login: form({
83
86
  initial: { email: '', password: '' },
84
- validators: { email: validators.email() },
87
+ validators: { email: email() },
85
88
  }),
86
89
  });
87
90
 
@@ -137,9 +140,9 @@ pnpm add @signaltree/core @signaltree/ng-forms
137
140
 
138
141
  ```typescript
139
142
  import { Component } from '@angular/core';
140
- import { createFormTree, validators } from '@signaltree/ng-forms';
143
+ import { createFormTree, required, email } from '@signaltree/ng-forms';
141
144
 
142
- interface ProfileForm {
145
+ interface ProfileForm extends Record<string, unknown> {
143
146
  name: string;
144
147
  email: string;
145
148
  marketing: boolean;
@@ -182,9 +185,9 @@ export class ProfileFormComponent {
182
185
  persistKey: 'profile-form',
183
186
  storage: this.storage,
184
187
  fieldConfigs: {
185
- name: { validators: validators.required('Name is required') },
188
+ name: { validators: [required('Name is required')] },
186
189
  email: {
187
- validators: [validators.required(), validators.email()],
190
+ validators: [required(), email()],
188
191
  debounceMs: 150,
189
192
  },
190
193
  },
@@ -256,8 +259,8 @@ class CheckoutComponent {
256
259
  }, {
257
260
  persistKey: 'checkout-draft',
258
261
  fieldConfigs: {
259
- 'shipping.zip': { validators: validators.zipCode() },
260
- '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 }
261
264
  }
262
265
  });
263
266
 
@@ -287,7 +290,7 @@ const checkout = createFormTree(initialState, {
287
290
  },
288
291
  fieldConfigs: {
289
292
  'payment.card.number': { debounceMs: 200 },
290
- 'preferences.*': { validators: validators.required() },
293
+ 'preferences.*': { validators: [required()] },
291
294
  },
292
295
  conditionals: [
293
296
  {
@@ -399,7 +402,7 @@ Use `SignalValueDirective` to keep standalone signals and `ngModel` fields align
399
402
  ### Before (deprecated)
400
403
 
401
404
  ```typescript
402
- import { createFormTree } from '@signaltree/ng-forms';
405
+ import { createFormTree, email } from '@signaltree/ng-forms';
403
406
 
404
407
  const form = createFormTree(
405
408
  {
@@ -407,7 +410,7 @@ const form = createFormTree(
407
410
  email: '',
408
411
  },
409
412
  {
410
- validators: { email: validators.email() },
413
+ validators: { email: email() },
411
414
  persistKey: 'profile-form',
412
415
  }
413
416
  );
@@ -421,12 +424,12 @@ form.form; // FormGroup
421
424
 
422
425
  ```typescript
423
426
  import { signalTree, form } from '@signaltree/core';
424
- import { formBridge } from '@signaltree/ng-forms';
427
+ import { formBridge, email } from '@signaltree/ng-forms';
425
428
 
426
429
  const tree = signalTree({
427
430
  profile: form({
428
431
  initial: { name: '', email: '' },
429
- validators: { email: validators.email() },
432
+ validators: { email: email() },
430
433
  persist: 'profile-form',
431
434
  }),
432
435
  }).with(formBridge());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signaltree/ng-forms",
3
- "version": "7.3.0",
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",
@@ -1,194 +0,0 @@
1
- import { inject, DestroyRef, computed } from '@angular/core';
2
- import { FormControl, FormArray, FormGroup } from '@angular/forms';
3
-
4
- function findFormSignals(node, path, results) {
5
- if (node === null || node === undefined) return;
6
- const hasForm = (typeof node === 'function' || typeof node === 'object') && node !== null && '$' in node && 'valid' in node && 'dirty' in node && 'validate' in node;
7
- if (hasForm && typeof node.validate === 'function') {
8
- results.push({
9
- path,
10
- formSignal: node
11
- });
12
- return;
13
- }
14
- if (typeof node === 'function' || typeof node === 'object') {
15
- const entries = Object.entries(node);
16
- for (const [key, value] of entries) {
17
- if (key.startsWith('_') || key === 'set' || key === 'update') continue;
18
- const childPath = path ? `${path}.${key}` : key;
19
- findFormSignals(value, childPath, results);
20
- }
21
- }
22
- }
23
- function createFormGroupBridge(formSignal, config, cleanupCallbacks) {
24
- const values = formSignal();
25
- const formGroup = createFormGroupFromValues(values, config.fieldConfigs || {});
26
- const angularErrors = computed(() => {
27
- const result = {};
28
- collectControlErrors(formGroup, '', result);
29
- return result;
30
- });
31
- const asyncPending = computed(() => formGroup.pending);
32
- const syncSignalToControl = () => {
33
- const currentValues = formSignal();
34
- patchFormGroupValues(formGroup, currentValues);
35
- };
36
- const syncControlToSignal = () => {
37
- const formValues = formGroup.getRawValue();
38
- formSignal.set(formValues);
39
- };
40
- const subscription = formGroup.valueChanges.subscribe(() => {
41
- syncControlToSignal();
42
- });
43
- cleanupCallbacks.push(() => subscription.unsubscribe());
44
- syncSignalToControl();
45
- if (config.conditionals && config.conditionals.length > 0) {
46
- const conditionalState = new Map();
47
- const applyConditionals = () => {
48
- const values = formGroup.getRawValue();
49
- for (const {
50
- when,
51
- fields
52
- } of config.conditionals) {
53
- let visible = true;
54
- try {
55
- visible = when(values);
56
- } catch {
57
- visible = true;
58
- }
59
- for (const fieldPath of fields) {
60
- const control = formGroup.get(fieldPath);
61
- if (!control) continue;
62
- const previous = conditionalState.get(fieldPath);
63
- if (previous === visible) continue;
64
- conditionalState.set(fieldPath, visible);
65
- if (visible) {
66
- control.enable({
67
- emitEvent: true
68
- });
69
- } else {
70
- control.disable({
71
- emitEvent: false
72
- });
73
- }
74
- }
75
- }
76
- };
77
- applyConditionals();
78
- const condSub = formGroup.valueChanges.subscribe(() => applyConditionals());
79
- cleanupCallbacks.push(() => condSub.unsubscribe());
80
- }
81
- return {
82
- formGroup,
83
- formControl: path => {
84
- const control = formGroup.get(path);
85
- return control instanceof FormControl ? control : null;
86
- },
87
- angularErrors,
88
- asyncPending
89
- };
90
- }
91
- function createFormGroupFromValues(values, fieldConfigs, basePath = '') {
92
- const controls = {};
93
- for (const [key, value] of Object.entries(values)) {
94
- const path = basePath ? `${basePath}.${key}` : key;
95
- const fieldConfig = fieldConfigs[path] || {};
96
- if (Array.isArray(value)) {
97
- const arrayControls = value.map((item, index) => {
98
- if (typeof item === 'object' && item !== null) {
99
- return createFormGroupFromValues(item, fieldConfigs, `${path}.${index}`);
100
- }
101
- return new FormControl(item);
102
- });
103
- controls[key] = new FormArray(arrayControls);
104
- } else if (typeof value === 'object' && value !== null) {
105
- controls[key] = createFormGroupFromValues(value, fieldConfigs, path);
106
- } else {
107
- const validators = fieldConfig.validators ? Array.isArray(fieldConfig.validators) ? fieldConfig.validators : [fieldConfig.validators] : [];
108
- const asyncValidators = fieldConfig.asyncValidators ? Array.isArray(fieldConfig.asyncValidators) ? fieldConfig.asyncValidators : [fieldConfig.asyncValidators] : [];
109
- controls[key] = new FormControl(value, {
110
- validators,
111
- asyncValidators
112
- });
113
- }
114
- }
115
- return new FormGroup(controls);
116
- }
117
- function patchFormGroupValues(group, values, basePath) {
118
- for (const [key, value] of Object.entries(values)) {
119
- const control = group.get(key);
120
- if (!control) continue;
121
- if (control instanceof FormGroup && typeof value === 'object' && value !== null) {
122
- patchFormGroupValues(control, value);
123
- } else if (control instanceof FormArray && Array.isArray(value)) {
124
- control.clear({
125
- emitEvent: false
126
- });
127
- for (const item of value) {
128
- if (typeof item === 'object' && item !== null) {
129
- control.push(createFormGroupFromValues(item, {}));
130
- } else {
131
- control.push(new FormControl(item));
132
- }
133
- }
134
- } else {
135
- control.setValue(value, {
136
- emitEvent: false
137
- });
138
- }
139
- }
140
- }
141
- function collectControlErrors(control, path, result) {
142
- if (control.errors) {
143
- result[path || 'root'] = control.errors;
144
- }
145
- if (control instanceof FormGroup) {
146
- for (const [key, child] of Object.entries(control.controls)) {
147
- collectControlErrors(child, path ? `${path}.${key}` : key, result);
148
- }
149
- } else if (control instanceof FormArray) {
150
- control.controls.forEach((child, index) => {
151
- collectControlErrors(child, `${path}.${index}`, result);
152
- });
153
- }
154
- }
155
- function formBridge(config = {}) {
156
- return tree => {
157
- const cleanupCallbacks = [];
158
- const formBridgeMap = new Map();
159
- const formLocations = [];
160
- findFormSignals(tree.$, '', formLocations);
161
- for (const {
162
- path,
163
- formSignal
164
- } of formLocations) {
165
- const bridge = createFormGroupBridge(formSignal, config, cleanupCallbacks);
166
- formBridgeMap.set(path, bridge);
167
- formSignal['formGroup'] = bridge.formGroup;
168
- formSignal['formControl'] = bridge.formControl;
169
- formSignal['angularErrors'] = bridge.angularErrors;
170
- formSignal['asyncPending'] = bridge.asyncPending;
171
- }
172
- const destroyRef = config.destroyRef ?? tryInjectDestroyRef();
173
- if (destroyRef) {
174
- destroyRef.onDestroy(() => {
175
- cleanupCallbacks.forEach(fn => fn());
176
- });
177
- }
178
- const enhanced = tree;
179
- enhanced.formBridge = formBridgeMap;
180
- enhanced.getAngularForm = function (path) {
181
- return formBridgeMap.get(path) ?? null;
182
- };
183
- return enhanced;
184
- };
185
- }
186
- function tryInjectDestroyRef() {
187
- try {
188
- return inject(DestroyRef);
189
- } catch {
190
- return undefined;
191
- }
192
- }
193
-
194
- export { formBridge };
@@ -1,2 +0,0 @@
1
- export { formBridge } from './form-bridge.js';
2
- export { FORM_MARKER, isFormMarker } from '@signaltree/core';
package/dist/index.js DELETED
@@ -1,5 +0,0 @@
1
- export * from './core/ng-forms.js';
2
- export * from './core/validators.js';
3
- export * from './core/async-validators.js';
4
- export * from './history/index.js';
5
- export * from './enhancer/index.js';
@@ -1,21 +0,0 @@
1
- import type { ISignalTree } from '@signaltree/core';
2
- export interface AuditEntry<T = unknown> {
3
- timestamp: number;
4
- changes: Partial<T>;
5
- previousValues?: Partial<T>;
6
- metadata?: AuditMetadata;
7
- }
8
- export interface AuditMetadata {
9
- userId?: string;
10
- source?: string;
11
- description?: string;
12
- [key: string]: unknown;
13
- }
14
- export interface AuditTrackerConfig<T> {
15
- getMetadata?: () => AuditMetadata;
16
- includePreviousValues?: boolean;
17
- filter?: (changes: Partial<T>) => boolean;
18
- maxEntries?: number;
19
- }
20
- export declare function createAuditTracker<T extends Record<string, unknown>>(tree: ISignalTree<T>, auditLog: AuditEntry<T>[], config?: AuditTrackerConfig<T>): () => void;
21
- export declare function createAuditCallback<T extends Record<string, unknown>>(auditLog: AuditEntry<T>[], getMetadata?: () => AuditMetadata): (previousState: T, currentState: T) => void;
@@ -1 +0,0 @@
1
- export { createAuditTracker, createAuditCallback, type AuditEntry, type AuditMetadata, type AuditTrackerConfig, } from './audit.js';
@@ -1,3 +0,0 @@
1
- import type { FormTreeAsyncValidatorFn } from './ng-forms';
2
- export declare function unique(checkFn: (value: unknown) => Promise<boolean>, message?: string): FormTreeAsyncValidatorFn<unknown>;
3
- export declare function debounce(validator: FormTreeAsyncValidatorFn<unknown>, delayMs: number): FormTreeAsyncValidatorFn<unknown>;
@@ -1,90 +0,0 @@
1
- import { DestroyRef, EventEmitter, OnInit, Signal, WritableSignal } from '@angular/core';
2
- import { AbstractControl, ControlValueAccessor, FormArray, FormGroup } from '@angular/forms';
3
- import { Observable } from 'rxjs';
4
- import type { ISignalTree, TreeConfig, TreeNode } from '@signaltree/core';
5
- export type FormTreeAsyncValidatorFn<T> = (value: T) => Observable<string | null> | Promise<string | null>;
6
- export type EnhancedArraySignal<T> = WritableSignal<T[]> & {
7
- push: (item: T) => void;
8
- removeAt: (index: number) => void;
9
- setAt: (index: number, value: T) => void;
10
- insertAt: (index: number, item: T) => void;
11
- move: (from: number, to: number) => void;
12
- clear: () => void;
13
- };
14
- export type FieldValidator = (value: unknown) => string | null;
15
- export interface FieldConfig {
16
- debounceMs?: number;
17
- validators?: Record<string, FieldValidator> | FieldValidator[];
18
- asyncValidators?: Record<string, FormTreeAsyncValidatorFn<unknown>> | Array<FormTreeAsyncValidatorFn<unknown>>;
19
- }
20
- export interface ConditionalField<T> {
21
- when: (values: T) => boolean;
22
- fields: string[];
23
- }
24
- export interface FormTreeOptions<T extends Record<string, unknown>> extends TreeConfig {
25
- validators?: SyncValidatorMap;
26
- asyncValidators?: AsyncValidatorMap;
27
- destroyRef?: DestroyRef;
28
- fieldConfigs?: Record<string, FieldConfig>;
29
- conditionals?: Array<ConditionalField<T>>;
30
- persistKey?: string;
31
- storage?: Pick<Storage, 'getItem' | 'setItem' | 'removeItem'>;
32
- persistDebounceMs?: number;
33
- validationBatchMs?: number;
34
- }
35
- export type FormTree<T extends Record<string, unknown>> = {
36
- state: TreeNode<T>;
37
- $: TreeNode<T>;
38
- form: FormGroup;
39
- errors: WritableSignal<Record<string, string>>;
40
- asyncErrors: WritableSignal<Record<string, string>>;
41
- touched: WritableSignal<Record<string, boolean>>;
42
- asyncValidating: WritableSignal<Record<string, boolean>>;
43
- dirty: WritableSignal<boolean>;
44
- valid: WritableSignal<boolean>;
45
- submitting: WritableSignal<boolean>;
46
- unwrap(): T;
47
- setValue(field: string, value: unknown): void;
48
- setValues(values: Partial<T>): void;
49
- reset(): void;
50
- submit<TResult>(submitFn: (values: T) => Promise<TResult>): Promise<TResult>;
51
- validate(field?: string): Promise<void>;
52
- getFieldError(field: string): Signal<string | undefined>;
53
- getFieldAsyncError(field: string): Signal<string | undefined>;
54
- getFieldTouched(field: string): Signal<boolean | undefined>;
55
- isFieldValid(field: string): Signal<boolean>;
56
- isFieldAsyncValidating(field: string): Signal<boolean | undefined>;
57
- fieldErrors: Record<string, Signal<string | undefined>>;
58
- fieldAsyncErrors: Record<string, Signal<string | undefined>>;
59
- values: ISignalTree<T>;
60
- destroy(): void;
61
- };
62
- export declare class FormValidationError extends Error {
63
- readonly errors: Record<string, string>;
64
- readonly asyncErrors: Record<string, string>;
65
- constructor(errors: Record<string, string>, asyncErrors: Record<string, string>);
66
- }
67
- type SyncValidatorMap = Record<string, (value: unknown) => string | null>;
68
- type AsyncValidatorMap = Record<string, FormTreeAsyncValidatorFn<unknown>>;
69
- export declare function createFormTree<T extends Record<string, unknown>>(initialValues: T, config?: FormTreeOptions<T>): FormTree<T>;
70
- export declare function createVirtualFormArray<T>(items: T[], visibleRange: {
71
- start: number;
72
- end: number;
73
- }, controlFactory?: (value: T, index: number) => AbstractControl): FormArray;
74
- export declare class SignalValueDirective implements ControlValueAccessor, OnInit {
75
- signalTreeSignalValue: WritableSignal<unknown>;
76
- signalTreeSignalValueChange: EventEmitter<unknown>;
77
- private elementRef;
78
- private renderer;
79
- private onChange;
80
- private onTouched;
81
- ngOnInit(): void;
82
- handleChange(event: Event): void;
83
- handleBlur(): void;
84
- writeValue(value: unknown): void;
85
- registerOnChange(fn: (value: unknown) => void): void;
86
- registerOnTouched(fn: () => void): void;
87
- setDisabledState?(isDisabled: boolean): void;
88
- }
89
- export declare const SIGNAL_FORM_DIRECTIVES: (typeof SignalValueDirective)[];
90
- export {};
@@ -1,9 +0,0 @@
1
- import type { FieldValidator } from './ng-forms';
2
- export declare function required(message?: string): FieldValidator;
3
- export declare function email(message?: string): FieldValidator;
4
- export declare function minLength(min: number, message?: string): FieldValidator;
5
- export declare function maxLength(max: number, message?: string): FieldValidator;
6
- export declare function pattern(regex: RegExp, message?: string): FieldValidator;
7
- export declare function min(min: number, message?: string): FieldValidator;
8
- export declare function max(max: number, message?: string): FieldValidator;
9
- export declare function compose(validators: FieldValidator[]): FieldValidator;
@@ -1,30 +0,0 @@
1
- import { DestroyRef, Signal } from '@angular/core';
2
- import { AsyncValidatorFn, FormControl, FormGroup, ValidationErrors, ValidatorFn } from '@angular/forms';
3
- import { ISignalTree } from '@signaltree/core';
4
- export interface AngularConditionalField<T = unknown> {
5
- when: (values: T) => boolean;
6
- fields: string[];
7
- }
8
- export interface AngularFieldConfig {
9
- debounceMs?: number;
10
- validators?: ValidatorFn | ValidatorFn[];
11
- asyncValidators?: AsyncValidatorFn | AsyncValidatorFn[];
12
- }
13
- export interface AngularFormsConfig<T = unknown> {
14
- conditionals?: AngularConditionalField<T>[];
15
- fieldConfigs?: Record<string, AngularFieldConfig>;
16
- destroyRef?: DestroyRef;
17
- validationBatchMs?: number;
18
- }
19
- export interface AngularFormBridge<T extends Record<string, unknown>> {
20
- formGroup: FormGroup;
21
- formControl(path: string): FormControl | null;
22
- angularErrors: Signal<Record<string, ValidationErrors | null>>;
23
- asyncPending: Signal<boolean>;
24
- }
25
- export interface AngularFormsMethods {
26
- getAngularForm<T extends Record<string, unknown>>(path: string): AngularFormBridge<T> | null;
27
- formBridge: Map<string, AngularFormBridge<Record<string, unknown>>>;
28
- }
29
- export declare function formBridge<TConfig = unknown>(config?: AngularFormsConfig<TConfig>): <T>(tree: ISignalTree<T>) => ISignalTree<T> & AngularFormsMethods;
30
- export { FORM_MARKER, isFormMarker } from '@signaltree/core';
@@ -1 +0,0 @@
1
- export * from './form-bridge';
@@ -1,21 +0,0 @@
1
- import { Signal } from '@angular/core';
2
- interface FormTree<T extends Record<string, unknown>> {
3
- form: any;
4
- unwrap(): T;
5
- destroy(): void;
6
- setValues(values: Partial<T>): void;
7
- }
8
- export interface FormHistory<T> {
9
- past: T[];
10
- present: T;
11
- future: T[];
12
- }
13
- export declare function withFormHistory<T extends Record<string, unknown>>(formTree: FormTree<T>, options?: {
14
- capacity?: number;
15
- }): FormTree<T> & {
16
- undo: () => void;
17
- redo: () => void;
18
- clearHistory: () => void;
19
- history: Signal<FormHistory<T>>;
20
- };
21
- export {};
@@ -1 +0,0 @@
1
- export { withFormHistory, type FormHistory } from './history';
package/src/index.d.ts DELETED
@@ -1,5 +0,0 @@
1
- export * from './core/ng-forms';
2
- export * from './core/validators';
3
- export * from './core/async-validators';
4
- export * from './history';
5
- export * from './enhancer';
@@ -1 +0,0 @@
1
- export * from './public-api';
@@ -1 +0,0 @@
1
- export * from './rxjs-bridge';
@@ -1,3 +0,0 @@
1
- import { Signal } from '@angular/core';
2
- import { Observable } from 'rxjs';
3
- export declare function toObservable<T>(signal: Signal<T>): Observable<T>;
@@ -1 +0,0 @@
1
- export { createWizardForm, type FormStep } from './wizard';
@@ -1,19 +0,0 @@
1
- import { Signal } from '@angular/core';
2
- type FormTreeOptions = Record<string, unknown>;
3
- interface FormTree<T extends Record<string, unknown>> {
4
- unwrap(): T;
5
- }
6
- export interface FormStep<T extends Record<string, unknown>> {
7
- fields: Array<keyof T | string>;
8
- validate?: (form: FormTree<T>) => Promise<boolean> | boolean;
9
- canSkip?: (values: T) => boolean;
10
- }
11
- export declare function createWizardForm<T extends Record<string, unknown>>(steps: FormStep<T>[], initialValues: T, config?: FormTreeOptions): FormTree<T> & {
12
- currentStep: Signal<number>;
13
- nextStep: () => Promise<boolean>;
14
- previousStep: () => void;
15
- goToStep: (index: number) => Promise<boolean>;
16
- canGoToStep: (index: number) => boolean;
17
- isFieldVisible: (field: keyof T | string) => Signal<boolean>;
18
- };
19
- export {};