UNPKG

signal-template-forms

Version:

A powerful, type-safe Angular forms library built with signals, providing reactive form management with excellent developer experience and performance.

376 lines (375 loc) 16.9 kB
import { Signal, Type, WritableSignal } from '@angular/core'; import { type LucideIconData } from 'lucide-angular'; import { Observable } from 'rxjs'; import { FormFieldType } from '../enums/form-field-type.enum'; import { FormStatus } from '../enums/form-status.enum'; import { type ThemeMode } from '../services/theme.service'; import { ConfigTypeForField } from './signal-field-configs.model'; export type FieldWithOptions<TModel> = Extract<SignalFormFieldBuilderInput<TModel>, { options: FormOption[]; }>; export interface FormOption<TResult = string | number | boolean | object, TIcon = unknown> { label: string; value: NonNullable<TResult>; icon?: string | Type<TIcon> | LucideIconData; } export type SignalValidatorFn<T, TModel> = (value: T, form: SignalFormContainer<TModel>) => string | null; export type SignalAsyncValidatorFn<T, TModel> = (value: T, form: SignalFormContainer<TModel>) => Observable<string | null> | Promise<string | null>; export type SignalValidator<TModel, TKey extends keyof TModel> = MetaValidatorFn<TModel[TKey], TModel> | SignalValidatorFn<TModel[TKey], TModel>; export type SignalAsyncValidator<TModel, TKey extends keyof TModel> = SignalAsyncValidatorFn<TModel[TKey], TModel>; export type ValidationTrigger = 'change' | 'blur' | 'submit'; export interface ValidationConfig { trigger?: ValidationTrigger; debounceMs?: number; validateAsyncOnInit?: boolean; } export type ComputedOptions<TModel> = { source: (form: SignalFormContainer<TModel>) => any; filterFn: (sourceValue: any, options: FormOption[], currentValue: any) => FormOption[]; }; /** * Styling configuration for field components */ export interface FieldStylingConfig<TModel> { /** Array of CSS classes to apply to the field wrapper */ modifierClass?: string[]; /** * Function that returns CSS classes for specific field parts based on form state * Return an object with classes for each part, or a string/array for wrapper-only styling */ stylesFn?: (field: SignalFormField<TModel>, form: SignalFormContainer<TModel>) => string | string[] | { wrapper?: string | string[]; label?: string | string[]; input?: string | string[]; error?: string | string[]; hint?: string | string[]; }; /** * Function that returns inline styles for specific field parts based on form state */ inlineStylesFn?: (field: SignalFormField<TModel>, form: SignalFormContainer<TModel>) => { wrapper?: { [key: string]: string; }; label?: { [key: string]: string; }; input?: { [key: string]: string; }; error?: { [key: string]: string; }; hint?: { [key: string]: string; }; }; } type IsPlainObject<T> = T extends object ? T extends (...args: any[]) => any ? false : T extends any[] ? false : T extends Date ? false : T extends FormOption ? false : true : false; type IsArrayOfPlainObjects<T> = T extends Array<infer U> ? IsPlainObject<U> : false; export type BaseFieldConfig<TModel, K extends keyof TModel> = { name: K; label: string; validators?: SignalValidator<TModel, K>[]; asyncValidators?: SignalAsyncValidator<TModel, K>[]; validationConfig?: ValidationConfig; computedValue?: (form: SignalFormContainer<TModel>) => TModel[K]; hidden?: boolean | ((form: SignalFormContainer<TModel>) => boolean); disabled?: boolean | ((form: SignalFormContainer<TModel>) => boolean); parser?: (value: string) => TModel[K]; styling?: FieldStylingConfig<TModel>; }; export type TextSignalField<TModel, K extends keyof TModel> = BaseFieldConfig<TModel, K> & { type: FormFieldType.TEXT; config?: ConfigTypeForField<FormFieldType.TEXT>; }; export type PasswordSignalField<TModel, K extends keyof TModel> = BaseFieldConfig<TModel, K> & { type: FormFieldType.PASSWORD; config?: ConfigTypeForField<FormFieldType.PASSWORD>; }; export type TextareaSignalField<TModel, K extends keyof TModel> = BaseFieldConfig<TModel, K> & { type: FormFieldType.TEXTAREA; config?: ConfigTypeForField<FormFieldType.TEXTAREA>; }; export type NumberSignalField<TModel, K extends keyof TModel> = BaseFieldConfig<TModel, K> & { type: FormFieldType.NUMBER; config?: ConfigTypeForField<FormFieldType.NUMBER>; }; export type SelectSignalField<TModel, K extends keyof TModel> = BaseFieldConfig<TModel, K> & { type: FormFieldType.SELECT; config?: ConfigTypeForField<FormFieldType.SELECT>; options: FormOption[]; computedOptions?: ComputedOptions<TModel>; }; export type CheckboxSignalField<TModel, K extends keyof TModel> = BaseFieldConfig<TModel, K> & { type: FormFieldType.CHECKBOX; config?: ConfigTypeForField<FormFieldType.CHECKBOX>; options: FormOption[]; computedOptions?: ComputedOptions<TModel>; }; export type CheckboxGroupSignalField<TModel, K extends keyof TModel> = BaseFieldConfig<TModel, K> & { type: FormFieldType.CHECKBOX_GROUP; config?: ConfigTypeForField<FormFieldType.CHECKBOX_GROUP>; options: FormOption[]; computedOptions?: ComputedOptions<TModel>; valueType?: 'array' | 'map'; }; export type DateTimeSignalField<TModel, K extends keyof TModel> = BaseFieldConfig<TModel, K> & { type: FormFieldType.DATETIME; config?: ConfigTypeForField<FormFieldType.DATETIME>; }; export type RadioSignalField<TModel, K extends keyof TModel> = BaseFieldConfig<TModel, K> & { type: FormFieldType.RADIO; config?: ConfigTypeForField<FormFieldType.RADIO>; options: FormOption[]; computedOptions?: ComputedOptions<TModel>; }; export type LoadOptionsFn = (search: string) => Observable<FormOption[]> | Promise<FormOption[]>; export type AutocompleteSignalField<TModel, K extends keyof TModel> = BaseFieldConfig<TModel, K> & { type: FormFieldType.AUTOCOMPLETE; config?: ConfigTypeForField<FormFieldType.AUTOCOMPLETE>; loadOptions: LoadOptionsFn; }; export type ColorSignalField<TModel, K extends keyof TModel> = BaseFieldConfig<TModel, K> & { type: FormFieldType.COLOR; config?: ConfigTypeForField<FormFieldType.COLOR>; }; export type SwitchSignalField<TModel, K extends keyof TModel> = BaseFieldConfig<TModel, K> & { type: FormFieldType.SWITCH; config?: ConfigTypeForField<FormFieldType.SWITCH>; }; export type SliderSignalField<TModel, K extends keyof TModel> = BaseFieldConfig<TModel, K> & { type: FormFieldType.SLIDER; config?: ConfigTypeForField<FormFieldType.SLIDER>; }; export type FileSignalField<TModel, K extends keyof TModel> = BaseFieldConfig<TModel, K> & { type: FormFieldType.FILE; config?: ConfigTypeForField<FormFieldType.FILE>; }; export type RatingSignalField<TModel, K extends keyof TModel> = BaseFieldConfig<TModel, K> & { type: FormFieldType.RATING; config?: ConfigTypeForField<FormFieldType.RATING>; }; export type MultiSelectSignalField<TModel, K extends keyof TModel> = BaseFieldConfig<TModel, K> & { type: FormFieldType.MULTISELECT; config?: ConfigTypeForField<FormFieldType.MULTISELECT>; options: FormOption[]; computedOptions?: ComputedOptions<TModel>; }; export type ChipListSignalField<TModel, K extends keyof TModel> = BaseFieldConfig<TModel, K> & { type: FormFieldType.CHIPLIST; config?: ConfigTypeForField<FormFieldType.CHIPLIST>; options: FormOption[]; computedOptions?: ComputedOptions<TModel>; }; export type SignalField<TModel, K extends keyof TModel> = TextSignalField<TModel, K> | PasswordSignalField<TModel, K> | TextareaSignalField<TModel, K> | NumberSignalField<TModel, K> | SelectSignalField<TModel, K> | CheckboxSignalField<TModel, K> | CheckboxGroupSignalField<TModel, K> | DateTimeSignalField<TModel, K> | RadioSignalField<TModel, K> | AutocompleteSignalField<TModel, K> | ColorSignalField<TModel, K> | SwitchSignalField<TModel, K> | SliderSignalField<TModel, K> | FileSignalField<TModel, K> | RatingSignalField<TModel, K> | MultiSelectSignalField<TModel, K> | ChipListSignalField<TModel, K>; export type BaseFieldState<TModel, TValue> = { error: WritableSignal<string | null>; asyncError: WritableSignal<string | null>; validating: WritableSignal<boolean>; touched: WritableSignal<boolean>; dirty: WritableSignal<boolean>; focus: WritableSignal<boolean>; value: WritableSignal<TValue>; getForm: () => SignalFormContainer<TModel>; isDisabled: Signal<boolean>; isHidden: Signal<boolean>; readonly path: string; }; export type SignalFormField<TModel, K extends keyof TModel = keyof TModel> = SignalField<TModel, K> & BaseFieldState<TModel, TModel[K]>; export type SignalFormFieldBuilderInput<TModel> = { [K in keyof TModel]: IsArrayOfPlainObjects<TModel[K]> extends true ? RepeatableGroupBuilderField<TModel, K> : IsPlainObject<TModel[K]> extends true ? NestedGroupBuilderField<TModel, K> : SignalField<TModel, K>; }[keyof TModel]; export type NestedGroupBuilderField<TModel, K extends keyof TModel> = { name: K; heading: string; subheading: string; fields: SignalFormFieldBuilderInput<TModel[K]>[]; hidden?: boolean | ((form: SignalFormContainer<TModel>) => boolean); disabled?: boolean | ((form: SignalFormContainer<TModel>) => boolean); config?: SignalFormConfig<TModel[K]>; }; export type RepeatableGroupBuilderField<TModel, K extends keyof TModel> = { name: K; heading: string; subheading?: string; type: FormFieldType.REPEATABLE_GROUP; fields: SignalFormFieldBuilderInput<ItemOf<TModel[K]>>[]; hidden?: boolean | ((form: SignalFormContainer<TModel>) => boolean); disabled?: boolean | ((form: SignalFormContainer<TModel>) => boolean); config?: SignalFormConfig<ItemOf<TModel[K]>>; minItems?: number; maxItems?: number; }; export type RepeatableGroupSignalFormField<TModel, K extends keyof TModel> = RepeatableGroupBuilderField<TModel, K> & { repeatableForms: WritableSignal<SignalFormContainer<ItemOf<TModel[K]>>[]>; addItem: (initial?: ItemOf<TModel[K]>) => void; removeItem: (index: number) => void; error: WritableSignal<boolean>; touched: WritableSignal<boolean>; dirty: WritableSignal<boolean>; value: Signal<ItemOf<TModel[keyof TModel]>[]>; parentForm?: Signal<SignalFormContainer<TModel>>; }; export type SignalFormConfig<TModel> = GridSignalFormConfig<TModel> | FlexSignalFormConfig; export interface SignalSteppedFormConfig<TModel> { form?: SignalFormConfig<TModel>; canSkipIncompleteSteps?: boolean; disableUponComplete?: boolean; } export type BaseSignalFormConfig = { view?: 'row' | 'stacked' | 'collapsable'; layout: 'flex' | 'grid-area'; disableUponComplete?: boolean; theme?: ThemeMode; allowDarkMode?: boolean; }; export interface GridSignalFormConfig<TModel> extends BaseSignalFormConfig { layout: 'grid-area'; gridArea: Array<keyof TModel | '.'>[]; } export interface FlexSignalFormConfig extends BaseSignalFormConfig { layout: 'flex'; } export type ErrorMessage<TModel> = { name: keyof TModel; message: string; path: string; field?: SignalFormField<unknown>; focusField?: () => void; trigger?: ValidationTrigger; }; export type DeepPartial<T> = { [K in keyof T]?: T[K] extends object ? T[K] extends Array<infer U> ? Array<DeepPartial<U>> : DeepPartial<T[K]> : T[K]; }; export type ItemOf<T> = T extends Array<infer U> ? U : never; export type InferFieldType<TModel, K extends keyof TModel> = TModel[K] extends Array<infer U> ? RepeatableGroupSignalFormField<TModel, K> & { repeatableForms: WritableSignal<SignalFormContainer<U>[]>; form: SignalFormContainer<U>; fields: SignalFormField<U>[]; } : TModel[K] extends object ? SignalFormField<TModel, K> & { form: SignalFormContainer<TModel[K]>; fields: SignalFormField<TModel[K]>[]; } : SignalFormField<TModel, K>; export interface SignalFormContainer<TModel, TParentModel = unknown> { title?: string; status: WritableSignal<FormStatus>; fields: SignalFormField<TModel>[]; getField<K extends keyof TModel>(key: K): InferFieldType<TModel, K>; getParent?(): SignalFormContainer<any, any> | undefined; parentForm: Signal<SignalFormContainer<any, any> | undefined>; anyTouched: Signal<boolean>; anyDirty: Signal<boolean>; hasSaved(): Signal<boolean>; getValue(): TModel; getRawValue(): TModel; getErrors(): ErrorMessage<TModel>[]; validateForm(): boolean; reset(): void; save(): void; value: Signal<TModel>; rawValue: Signal<TModel>; config: SignalFormConfig<TModel>; patchValue: (partialModel: DeepPartial<TModel>) => void; setValue: (model: TModel) => void; saveButtonDisabled: Signal<boolean>; } export interface SignalSteppedFormContainer<TModel> { currentStep: WritableSignal<number>; steps: SignalFormContainer<TModel>[]; value: Signal<TModel>; hasSaved: Signal<boolean>; getValue(): TModel; getErrors(): ErrorMessage<TModel>[]; getField<K extends keyof TModel>(key: K): SignalFormField<TModel, K>; validateStep(): boolean; validateAll(): boolean; isValidStep: () => boolean; reset(): void; save(): void; anyDirty: Signal<boolean>; anyTouched: Signal<boolean>; status: WritableSignal<FormStatus>; config?: SignalSteppedFormConfig<TModel>; saveButtonDisabled: Signal<boolean>; } export type ElementTypeForField<T extends FormFieldType> = T extends FormFieldType.CHECKBOX ? HTMLInputElement : T extends FormFieldType.CHECKBOX_GROUP ? HTMLInputElement : T extends FormFieldType.RADIO ? HTMLInputElement : T extends FormFieldType.NUMBER ? HTMLInputElement : T extends FormFieldType.TEXT ? HTMLInputElement : T extends FormFieldType.PASSWORD ? HTMLInputElement : T extends FormFieldType.TEXTAREA ? HTMLTextAreaElement : T extends FormFieldType.SELECT ? HTMLSelectElement : T extends FormFieldType.DATETIME ? HTMLInputElement : T extends FormFieldType.COLOR ? HTMLInputElement : T extends FormFieldType.SWITCH ? HTMLInputElement : T extends FormFieldType.SLIDER ? HTMLInputElement : T extends FormFieldType.FILE ? HTMLInputElement : T extends FormFieldType.RATING ? HTMLInputElement : T extends FormFieldType.MULTISELECT ? HTMLSelectElement : T extends FormFieldType.CHIPLIST ? HTMLInputElement : T extends FormFieldType.AUTOCOMPLETE ? HTMLInputElement : never; export interface FormBuilderArgs<TModel> { model: TModel; fields: SignalFormFieldBuilderInput<TModel>[]; title?: string; config?: SignalFormConfig<TModel>; onSave?: (value: TModel) => void; parentForm?: SignalFormContainer<unknown>; parentPath?: string; } export interface SteppedFormBuilderArgs<TModel> { model: TModel; steps: { fields: SignalFormFieldBuilderInput<TModel>[]; config?: SignalFormConfig<TModel>; title?: string; description?: string; }[]; onSave?: (value: TModel) => void; config?: SignalSteppedFormConfig<TModel>; } export interface ArrayFormBuilderArgs<TModel> { model: TModel[]; fields: SignalFormFieldBuilderInput<TModel>[]; title?: string; config?: SignalFormConfig<TModel>; onSave?: (value: TModel[]) => void; onItemAdd?: (item: TModel) => void; onItemRemove?: (index: number) => void; defaultItem?: Partial<TModel>; parentForm?: SignalFormContainer<unknown>; parentPath?: string; } export interface ArrayFormContainer<TModel> { title?: string; forms: () => SignalFormContainer<TModel>[]; value: () => TModel[]; addItem: (item?: Partial<TModel>) => void; removeItem: (index: number) => void; validateAll: () => boolean; getErrors: () => ErrorMessage<TModel>[]; save: () => void; reset: () => void; anyTouched: () => boolean; anyDirty: () => boolean; status: () => FormStatus; saveButtonDisabled: () => boolean; } export type FieldWithForm<TModel> = SignalFormField<TModel> & { form: SignalFormContainer<TModel[keyof TModel]>; }; export type RepeatableField<TModel> = SignalFormField<TModel> & { repeatableForms: WritableSignal<SignalFormContainer<any>[]>; }; export type CheckboxGroupField<TModel> = SignalFormField<TModel> & { type: FormFieldType.CHECKBOX_GROUP; valueType?: 'array' | 'map'; }; export interface FieldWithFormTraversal<T> { form: SignalFormContainer<T>; } export interface FieldWithRepeatableForms<T> { repeatableForms: () => SignalFormContainer<T>[]; } export interface RoleAttributes { role?: string; ariaAttributes: Record<string, string | boolean | null>; inputAttributes: Record<string, string | boolean | null>; } export type ExpandedAnimationEvent = AnimationEvent & { toState: string; }; export interface ValidatorMeta { required?: boolean; type?: string; } export type MetaValidatorFn<TVal, TModel> = SignalValidatorFn<TVal, TModel> & { readonly __meta?: ValidatorMeta; }; export {};