@mmstack/form-core
Version:
[](https://www.npmjs.com/package/@mmstack/form-core) [](https://github.com/mihajm/mmstack/blob/master/packages/form/core/LICEN
249 lines (243 loc) • 12.6 kB
TypeScript
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 };