@mmstack/form-adapters v19.2.1
@mmstack/form-adapters
Provides a collection of headless, reusable state adapters for common form field types. Built upon @mmstack/form-core and integrating with @mmstack/form-validation, this library allows you to define reactive form state logic independently from any specific UI component library.
Use these adapters to manage state for inputs like text fields, selects, date pickers, checkboxes, etc., and then bind that state to your chosen UI components. They are extended & re-exported by specific ui libraries like @mmstack/form-material.
Installation
npm install @mmstack/form-adapters @mmstack/form-core @mmstack/form-validationCore Concept: State Adapters
Each "adapter" represents the complete reactive state for a specific type of form field. It bundles the core signals (value, error, disabled, touched, label, hint etc.) from @mmstack/form-core's FormControlSignal with additional signals, properties, and helper functions relevant to that field type (e.g., placeholder for text, options for select, step for number).
Key characteristics:
XStateType: Each adapter exports a specific type (e.g.,StringState,SelectState<T>) describing its structure, inheriting fromFormControlSignal.typeDiscriminator: State objects include atypeproperty (e.g.,{ type: 'string' },{ type: 'select' }) for easy identification in code or templates.- Creation Factories: Each adapter provides functions to create instances of its state object, typically with and without relying on Angular's Dependency Injection.
createXState vs injectCreateXState
All adapters offer two ways to create their state object:
createXState(...)
- Pure function: Does not use Angular's Dependency Injection (DI).
- Requires manual configuration, including providing a complete
validatorfunction via its options if validation is needed. - Use Case: Useful when creating state outside an Angular injection context, or when you need absolute control over validator creation without using
@mmstack/form-validation's DI features.
injectCreateXState()
- DI-based: Returns a factory function that uses Angular's DI (specifically
injectValidatorsfrom@mmstack/form-validationand oftenLOCALE_ID). - Offers a convenient API accepting simplified
validationoptions (e.g.,{ validation: () => ({ required: true, minLength: 5 }) }) which use the correspondingXxxValidatorOptionstype from@mmstack/form-validation. - Automatically creates the final
validatorfunction using globally configured (and potentially localized) validation rules. - May provide enhanced features like error message/tooltip splitting.
- Use Case: The recommended approach within Angular applications for easy integration with validation and localization.
Available Adapters
This library provides state adapters for various common form field types:
| Adapter Type | State Type Export | Value Type | Key Added Properties/Signals | Creation Functions |
|---|---|---|---|---|
| String | StringState<TParent> | string \| null | placeholder, autocomplete | createStringState, injectCreateStringState |
| Textarea | TextareaState<TParent> | string \| null | placeholder, autocomplete, rows, minRows, maxRows | createTextareaState, injectCreateTextareaState |
| Autocomplete | AutocompleteState<TParent> | string \| null | placeholder, autocomplete, options (filtered string[]), displayWith | createAutocompleteState, injectCreateAutocompleteState |
| Number | NumberState<TParent> | number \| null | placeholder, step, localizedValue, setLocalizedValue, inputType, keydownHandler | createNumberState, injectCreateNumberState |
| Date | DateState<TParent, TDate> | TDate \| null | placeholder, min, max | createDateState, injectCreateDateState |
| Time | TimeState<TParent, TDate> | TDate \| null | placeholder, min, max | createTimeState, injectCreateTimeState |
| DateTime | DateTimeState<TParent, TDate> | TDate \| null | placeholder, min, max, timeControl, dateControl | createDateTimeState, injectCreateDateTimeState |
| Boolean | BooleanState<TParent> | boolean | (Type discriminator) | createBooleanState, injectCreateBooleanState |
| Toggle | ToggleState<TParent> | boolean | (Type discriminator) | createToggleState, injectCreateToggleState |
| Select | SelectState<T, TParent> | T | placeholder, options (T[]), valueLabel, identify, display, equal | createSelectState, injectCreateSelectState |
| Multi-Select | MultiSelectState<T[], TParent> | T[] | placeholder, options (Tnumber), valueLabel (joined), identify, display (elem), equal (elem) | createMultiSelectState, injectCreateMultiSelectState |
| Button Group | ButtonGroupState<T, TParent> | T | options (T[]), valueLabel, identify, display, equal (inherits Select minus placeholder) | createButtonGroupState, injectCreateButtonGroupState |
| Search | SearchState<T, TParent> | T | placeholder, query, request, identify, displayWith, valueLabel, valueId, onSelected | createSearchState, injectCreateSearchState |
Refer to JSDoc comments for detailed descriptions of state properties and options.
Usage Example (Using injectCreate...)
// Example Factory Provider or Component logic
import { Component, computed, inject, signal, isSignal, type Signal } from '@angular/core';
import { formGroup, derived, type DerivedSignal } from '@mmstack/form-core';
import {
// Import desired adapter factories and state types
injectCreateStringState,
StringState,
injectCreateNumberState,
NumberState,
injectCreateSelectState,
SelectState,
injectCreateBooleanState,
BooleanState,
// ... import other needed adapter types and factories
} from '@mmstack/form-adapters';
// Validation options come from @mmstack/form-validation
import { StringValidatorOptions, NumberValidatorOptions } from '@mmstack/form-validation';
// Assume Validation is configured globally via provideValidatorConfig
interface Settings {
notifyByEmail: boolean;
email: string | null;
maxItems: number | null;
defaultView: 'list' | 'grid';
}
// Factory function using injected adapters
function injectSettingsFormState<TParent = undefined>(
// Can accept raw value, signal, or derived signal
initialValue: Settings | DerivedSignal<TParent, Settings> = { notifyByEmail: true, email: null, maxItems: 10, defaultView: 'list' },
) {
// Get injected factories within an injection context
const createBoolean = injectCreateBooleanState();
const createString = injectCreateStringState();
const createNumber = injectCreateNumberState();
const createSelect = injectCreateSelectState();
// Ensure we have a WritableSignal (or DerivedSignal) for formGroup
const settingsSignal = isSignal(initialValue) ? initialValue : signal(initialValue); // Create a signal if raw value passed
// Create the form group using derived controls
return formGroup(settingsSignal, {
// Use derived() to link child state to parent signal properties
notifyByEmail: createBoolean(derived(settingsSignal, 'notifyByEmail'), {
label: () => 'Notify by Email',
// No validation for boolean here
}),
// Conditionally validate email based on the notifyByEmail flag
email: createString(derived(settingsSignal, 'email'), {
label: () => 'Notification Email',
// Validation rules depend on another control's value from the PARENT signal
validation: () => ({
// Only require email if notifications are enabled
required: settingsSignal().notifyByEmail, // Read from parent signal
pattern: settingsSignal().notifyByEmail ? 'email' : undefined,
}),
}),
maxItems: createNumber(derived(settingsSignal, 'maxItems'), {
label: () => 'Max Items per Page',
validation: () => ({ required: true, min: 5, max: 100, integer: true }),
}),
defaultView: createSelect<'list' | 'grid'>(derived(settingsSignal, 'defaultView'), {
label: () => 'Default View',
options: () => ['list', 'grid'],
display: () => (value) => value === 'list' ? 'List view' : 'Grid view'
// Add required validation for the select
validation: () => ({ required: true }),
}),
});
}Validation Integration
The injectCreateXState factories are designed to work seamlessly with @mmstack/form-validation. Pass a validation function in the options object. This function should return the appropriate XxxValidatorOptions object (e.g., StringValidatorOptions, NumberValidatorOptions, ArrayValidatorOptions for MultiSelect). The factory uses the injected validators service to create the final validation logic, respecting global configuration like localization.
// Example: Get factory and configure validation
const createNum = injectCreateNumberState();
const countState = createNum(0, {
label: () => 'Count',
// Pass NumberValidatorOptions via the validation function
validation: () => ({ required: true, min: 0, max: 10 }),
});Using Adapters with UI Components
This library provides only the reactive state (headless). You need separate UI components (from libraries like @mmstack/form-material, Angular Material itself, PrimeNG, Bootstrap, etc., or your own custom components) to render the actual form fields.
These UI components would typically:
- Accept the corresponding
XStateobject as an@Input(). - Bind their internal HTML elements (e.g.,
<input>,<select>,<mat-select>) to the signals and properties provided by the state object (state.value,state.label,state.placeholder,state.disabled,state.error,state.options, etc.). - Call state methods like
state.value.set()orstate.markAsTouched()in response to user interactions.
SignalErrorValidator Directive
This library also exports SignalErrorValidator, a directive useful for integrating the error() signal from any form state adapter with template-driven forms (ngModel), particularly when using UI component libraries like Angular Material's <mat-form-field> that rely on NgControl's validation status derived from ngModel. Bind the state's error() signal to the [mmSignalError] input on an element that also has ngModel.
<input matInput [(ngModel)]="myStringState.value" [mmSignalError]="myStringState.error()" (blur)="myStringState.markAsTouched()" #myNgModel="ngModel" /> <mat-error>{{ myStringState.error() }}</mat-error>Refer to the JSDoc for SignalErrorValidator for more details.
Contributing
Contributions, issues, and feature requests are welcome!
5 months ago
5 months ago
5 months ago
5 months ago
6 months ago
6 months ago
6 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago