UNPKG

@mmstack/form-core

Version:

[![npm version](https://badge.fury.io/js/%40mmstack%2Fform-core.svg)](https://www.npmjs.com/package/@mmstack/form-core) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/mihajm/mmstack/blob/master/packages/form/core/LICEN

249 lines (243 loc) 12.6 kB
import { DerivedSignal } from '@mmstack/primitives'; export { DerivedSignal, derived, isDerivation } from '@mmstack/primitives'; import { WritableSignal, Signal, CreateSignalOptions, ValueEqualityFn } from '@angular/core'; /** * Represents the type of a form control. * - `control`: A single form control (e.g., an input field). * - `array`: An array of form controls (like Angular's `FormArray`). * - `group`: A group of form controls (like Angular's `FormGroup`). */ type ControlType = 'control' | 'array' | 'group'; /** * Represents a reactive form control. It holds the value, validation status, and other * metadata for a form field. This is the core building block for creating reactive forms * with signals. * * @typeParam T - The type of the form control's value. * @typeParam TParent - The type of the parent form control's value (if this control is part of a group or array). Defaults to `undefined`. * @typeParam TControlType - The type of the control ('control', 'array', or 'group'). Defaults to 'control'. * @typeParam TPartialValue - The type of the partial value, used for patching. */ type FormControlSignal<T, TParent = undefined, TControlType extends ControlType = 'control', TPartialValue = T | undefined> = { /** A unique identifier for the control. Used for tracking in `@for` loops. */ id: string; /** The main value signal for the control. */ value: WritableSignal<T>; /** A signal indicating whether the control's value has been changed. */ dirty: Signal<boolean>; /** A signal indicating whether the control has been interacted with (e.g., blurred). */ touched: Signal<boolean>; /** A signal containing the current validation error message (empty string if valid). */ error: Signal<string>; /** A signal indicating whether the control is pending */ pending: Signal<boolean>; /** A signal indicating whether the control is disabled. */ /** A signal indicating whether the control is in a valid state (without errors & not pending) */ valid: Signal<boolean>; disabled: Signal<boolean>; /** A signal indicating whether the control is read-only. */ readonly: Signal<boolean>; /** A signal indicating whether the control is required. */ required: Signal<boolean>; /** A signal containing the label for the control. */ label: Signal<string>; /** A signal containing the hint text for the control. */ hint: Signal<string>; /** Marks the control as touched. */ markAsTouched: () => void; /** Marks the control and all its child controls (if any) as touched. */ markAllAsTouched: () => void; /** Marks the control as pristine (not touched). */ markAsPristine: () => void; /** Marks the control and all its child controls (if any) as pristine. */ markAllAsPristine: () => void; /** * Resets the control to a new value and sets a new initial value. This is intended for * scenarios where the underlying data is updated externally (e.g., data coming from * the server). If the control is not dirty, the value is updated. If the control *is* * dirty, the value is *not* updated (preserving user changes). */ reconcile: (newValue: T) => void; /** * Similar to `reconcile`, but forces the update even if the control is dirty. */ forceReconcile: (newValue: T) => void; /** Resets the control's value to its initial value. */ reset: () => void; /** Resets the control's value and initial value. */ resetWithInitial: (initial: T) => void; /** * The derivation function used to create this control if it's part of a `formGroup` or `formArray`. * @internal */ from?: DerivedSignal<TParent, T>['from']; /** The equality function used to compare values. Defaults to `Object.is`. */ equal: (a: T, b: T) => boolean; /** The type of the control ('control', 'array', or 'group'). */ controlType: TControlType; /** * A signal representing the partial value of the control, suitable for patching data on a server. * It contains the changed value if `dirty` is `true`. */ partialValue: Signal<TPartialValue>; }; type CreateFormControlOptions<T, TControlType extends ControlType = ControlType> = CreateSignalOptions<T> & { validator?: () => (value: T) => string; onTouched?: () => void; disable?: () => boolean; readonly?: () => boolean; required?: () => boolean; label?: () => string; id?: () => string; hint?: () => string; dirtyEquality?: ValueEqualityFn<T>; onReset?: () => void; controlType?: TControlType; overrideValidation?: () => string; pending?: () => boolean; }; /** * Creates a `FormControlSignal`, a reactive form control that holds a value and tracks its * validity, dirty state, touched state, and other metadata. * * @typeParam T - The type of the form control's value. * @typeParam TParent - The type of the parent form control's value (if this control is part of a group or array). * @typeParam TControlType - The type of the control. Defaults to `'control'`. * @typeParam TPartialValue - The type of value when patching * @param initial - The initial value of the control, or a `DerivedSignal` if this control is part of a `formGroup` or `formArray`. * @param options - Optional configuration options for the control. * @returns A `FormControlSignal` instance. * * @example * // Create a simple form control: * const name = formControl('Initial Name'); * * // Create a form control with validation: * const age = formControl(0, { * validator: () => (value) => value >= 18 ? '' : 'Must be at least 18', * }); * * // Create a derived form control (equivalent to the above, but more explicit): * const user = signal({ name: 'John Doe', age: 30 }); * const name = formControl(derived(user, { * from: (u) => u.name, * onChange: (newName) => user.update(u => ({...u, name: newName})) * })); * * // Create a form group with nested controls: * const user = signal({ name: 'John Doe', age: 30 }); * const form = formGroup(user, { * name: formControl(derived(user, 'name')), * age: formControl(derived(user, 'age')), * }) */ declare function formControl<T, TParent = undefined, TControlType extends ControlType = 'control', TPartialValue = T | undefined>(initial: DerivedSignal<TParent, T> | T, opt?: CreateFormControlOptions<T, TControlType>): FormControlSignal<T, TParent, TControlType, TPartialValue>; type SignalValue<T> = T extends Signal<infer U> ? U : never; type FormArraySignal<T, TIndividualState extends FormControlSignal<T, any, any, any> = FormControlSignal<T, any, any, any>, TParent = undefined> = FormControlSignal<T[], TParent, 'array', Exclude<SignalValue<TIndividualState['partialValue']>, null | undefined>[] | undefined> & { ownError: Signal<string>; children: Signal<TIndividualState[]>; push: (value: T) => void; remove: (index: number) => void; min: Signal<number>; max: Signal<number>; canAdd: Signal<boolean>; canRemove: Signal<boolean>; }; type CreateFormArraySignalOptions<T, TIndividualState extends FormControlSignal<T, any, any, any>> = Omit<CreateFormControlOptions<T[]>, 'equal'> & { min?: () => number; max?: () => number; equal?: (a: T, b: T) => boolean; toPartialValue?: (v: T) => Exclude<SignalValue<TIndividualState['partialValue']>, null | undefined>; }; declare function formArray<T, TIndividualState extends FormControlSignal<T, any, any, any> = FormControlSignal<T, any, any, any>, TParent = undefined>(initial: T[] | DerivedSignal<TParent, T[]>, factory: (val: DerivedSignal<T[], T>, idx: number) => TIndividualState, opt?: CreateFormArraySignalOptions<T, TIndividualState>): FormArraySignal<T, TIndividualState, TParent>; /** * Extracts the partial value types from a record of `FormControlSignal` instances. * This is used to construct the `partialValue` type for `FormGroupSignal`. * @internal */ type DerivationPartialValues<TDerivations extends Record<string, FormControlSignal<any, any, any, any>>> = { [K in keyof TDerivations]: Exclude<SignalValue<TDerivations[K]['partialValue']>, undefined>; }; /** * Represents a group of form controls, similar to Angular's `FormGroup`. It aggregates * the values and states of its child controls into a single object. * * @typeParam T - The type of the form group's value (an object). * @typeParam TDerivations - A record where keys are the names of the child controls and values are the `FormControlSignal` instances. * @typeParam TParent - The type of the parent form control's value (if this group is nested). */ type FormGroupSignal<T, TDerivations extends Record<string, FormControlSignal<any, T, any, any>>, TParent = undefined> = FormControlSignal<T, TParent, 'group', Partial<DerivationPartialValues<TDerivations>>> & { /** * A signal that holds a record of the child form controls. The keys are the names * of the controls, and the values are the `FormControlSignal` instances. */ children: Signal<TDerivations>; /** * A signal that holds the validation error message of the group itself (excluding child errors). */ ownError: Signal<string>; }; /** * Options for creating a `FormGroupSignal`. Extends `CreateFormControlOptions`. */ type CreateFormGroupOptions<T, TDerivations extends Record<string, FormControlSignal<any, T, any, any>>> = CreateFormControlOptions<T, 'group'> & { /** * An optional function to create a base object for the `partialValue` signal. * This can be used to pre-populate the partial value with default values or * to perform custom logic before merging in the partial values from child controls. */ createBasePartialValue?: (value: T) => Partial<DerivationPartialValues<TDerivations>>; }; /** * Creates a `FormGroupSignal`, which aggregates a set of child form controls into a single object. * * @typeParam T - The type of the form group's value (an object). * @typeParam TDerivations - A record where keys are the names of the child controls and values are the `FormControlSignal` instances. * @typeParam TParent - The type of the parent form control's value (if this group is nested within another group or array). * @param initial - The initial value of the form group (or a `WritableSignal` or `DerivedSignal` if the group is nested). * @param providedChildren - An object containing the child `FormControlSignal` instances, or a function that returns such an object. * Using a function allows for dynamic creation of child controls (e.g., in response to changes in other signals). * @param options - Optional configuration options for the form group. * @returns A `FormGroupSignal` instance. * * @example * // Create a simple form group: * const user = signal({ name: 'John Doe', age: 30 }); * const form = formGroup(user, { * name: formControl(derived(user, 'name')), * age: formControl(derived(user, 'age')), * }) * * // Create a nested form group: * const user = signal({ name: 'John', age: 30, address: {street: "Some street"} }); * * const address = derived(user, 'address'); * const userForm = formGroup(user, { * name: formControl(derived(user, 'name')), * age: formControl(derived(user, 'age')), * address: formGroup(address, { * street: formControl(derived(address, (address) => address.street), { * validator: () => (value) => value ? "" : "required!" * }) // you can create deeply nested structures. * }) * }); * * // Create a form group with dynamically created children replaced rare FormRecord requirements. * const showAddress = signal(false); * type Characteristic = { * valueType: 'string'; * value: string; * } | { * valueType: 'number'; * value: number; * } * const char = signal<Characteristic>({ valueType: 'string', value: '' }); * const charForm = formGroup(char, () => { * if (char().valueType === 'string) return createStringControl(char); * return createNumberControl(char); * }); * */ declare function formGroup<T, TDerivations extends Record<string, FormControlSignal<any, T, any, any>>, TParent = undefined>(initial: DerivedSignal<TParent, T> | T | WritableSignal<T>, providedChildren: (() => TDerivations) | TDerivations, opt?: CreateFormGroupOptions<T, TDerivations>): FormGroupSignal<T, TDerivations, TParent>; export { formArray, formControl, formGroup }; export type { ControlType, CreateFormArraySignalOptions, CreateFormControlOptions, CreateFormGroupOptions, FormArraySignal, FormControlSignal, FormGroupSignal };