19.2.1 • Published 5 months ago

@mmstack/form-adapters v19.2.1

Weekly downloads
-
License
MIT
Repository
github
Last release
5 months ago

@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.

npm version License

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-validation

Core 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:

  • XState Type: Each adapter exports a specific type (e.g., StringState, SelectState<T>) describing its structure, inheriting from FormControlSignal.
  • type Discriminator: State objects include a type property (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 validator function 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 injectValidators from @mmstack/form-validation and often LOCALE_ID).
  • Offers a convenient API accepting simplified validation options (e.g., { validation: () => ({ required: true, minLength: 5 }) }) which use the corresponding XxxValidatorOptions type from @mmstack/form-validation.
  • Automatically creates the final validator function 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 TypeState Type ExportValue TypeKey Added Properties/SignalsCreation Functions
StringStringState<TParent>string \| nullplaceholder, autocompletecreateStringState, injectCreateStringState
TextareaTextareaState<TParent>string \| nullplaceholder, autocomplete, rows, minRows, maxRowscreateTextareaState, injectCreateTextareaState
AutocompleteAutocompleteState<TParent>string \| nullplaceholder, autocomplete, options (filtered string[]), displayWithcreateAutocompleteState, injectCreateAutocompleteState
NumberNumberState<TParent>number \| nullplaceholder, step, localizedValue, setLocalizedValue, inputType, keydownHandlercreateNumberState, injectCreateNumberState
DateDateState<TParent, TDate>TDate \| nullplaceholder, min, maxcreateDateState, injectCreateDateState
TimeTimeState<TParent, TDate>TDate \| nullplaceholder, min, maxcreateTimeState, injectCreateTimeState
DateTimeDateTimeState<TParent, TDate>TDate \| nullplaceholder, min, max, timeControl, dateControlcreateDateTimeState, injectCreateDateTimeState
BooleanBooleanState<TParent>boolean(Type discriminator)createBooleanState, injectCreateBooleanState
ToggleToggleState<TParent>boolean(Type discriminator)createToggleState, injectCreateToggleState
SelectSelectState<T, TParent>Tplaceholder, options (T[]), valueLabel, identify, display, equalcreateSelectState, injectCreateSelectState
Multi-SelectMultiSelectState<T[], TParent>T[]placeholder, options (Tnumber), valueLabel (joined), identify, display (elem), equal (elem)createMultiSelectState, injectCreateMultiSelectState
Button GroupButtonGroupState<T, TParent>Toptions (T[]), valueLabel, identify, display, equal (inherits Select minus placeholder)createButtonGroupState, injectCreateButtonGroupState
SearchSearchState<T, TParent>Tplaceholder, query, request, identify, displayWith, valueLabel, valueId, onSelectedcreateSearchState, 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:

  1. Accept the corresponding XState object as an @Input().
  2. 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.).
  3. Call state methods like state.value.set() or state.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!

19.2.1

5 months ago

19.2.0

5 months ago

19.1.7

5 months ago

19.1.6

5 months ago

19.1.5

6 months ago

19.1.4

6 months ago

19.1.3

6 months ago

19.1.2

7 months ago

19.1.1

7 months ago

19.1.0

7 months ago

19.0.15

7 months ago

19.0.14

7 months ago

19.0.13

7 months ago

19.0.12

7 months ago

19.0.11

7 months ago

19.0.10

7 months ago

19.0.9

7 months ago

19.0.8

7 months ago

19.0.7

7 months ago

19.0.6

7 months ago

19.0.5

7 months ago

19.0.4

7 months ago

19.0.3

7 months ago

19.0.1

7 months ago

19.0.0

7 months ago