Modular Forms

repository·main·Indexed 22 days ago

https://github.com/fabian-hiller/modular-forms

A lightweight, headless, and type-safe form management and validation library for JavaScript frameworks including React, Preact, SolidJS, and Qwik. It features a modular design with small bundle sizes, fine-grained DOM updates, and native HTML form field support. Note: Modular Forms is currently in maintenance mode; the official successor is Formisch.

Tokens
58.3K
Snippets
141
Records
315
Agent score
71%

What's inside modular-forms

  1. Overview of Modular Forms

    main

    Modular Forms is a type-safe, high-performance JavaScript library designed to validate and handle various types of forms across multiple frameworks. It is built on SolidJS, Qwik, Preact, and React.

    Key characteristics include:

    • Headless UI: The library handles logic and validation, while you define the visual styling.
    • Modular Design: Small bundle size (starting at 3 KB) because you only import what you need.
    • Fine-grained Updates: Fast performance via fine-grained DOM updates.
    • Type Safety: Full autocompletion in editors.
    • Progressive Enhancement: Supports forms with actions.
    • No Dependencies: Aside from the chosen framework.
    • Native Support: Works with all native HTML form fields and supports validation for everything from emails to files.
    IMPORTANT

    Modular Forms is in maintenance mode. For new projects, the official successor is Formisch.

  2. How Modular Forms achieves small bundle sizes

    main

    Modular Forms avoids the 'monolithic hook' pattern. In many libraries, every functionality needs access to the form state, so everything is returned in one large object.

    Modular Forms breaks this pattern by separating state creation from functionality. You use createForm or useForm to initialize the state, and then you only import and use the specific modules/methods required for your form. This ensures your bundle size scales with the complexity of your form's requirements rather than the library's total feature set.

  3. How input validation works in Modular Forms

    main

    Modular Forms provides two primary ways to handle input validation:

    • Atomic Validation Functions: You can use small, specialized validation functions that are imported only when needed (e.g., for email, URL, or MIME type validation). This keeps the bundle size minimal.
    • Schema-based Validation: For users who prefer schema-driven development, the library supports validating inputs using schemas like Zod.
  4. Supplement special data types for progressive enhancement

    main

    When a form is submitted without JavaScript, certain data types (arrays, booleans, files, numbers, and dates) may lose their type information during the standard HTML form submission process.

    To ensure the submitted data matches your schema and can be validated on the server, you must supplement the paths to these types in the formAction$ options object. This allows Modular Forms to transform the submitted data back into the expected types server-side.

    export const useFormAction = formAction$<SpecialForm>(
      (values) => {
        // Runs on server
      },
      {
        validate: valiForm$(SpecialSchema),
        arrays: ['checkbox.array', 'file.list', 'select.array'],
        booleans: ['checkbox.boolean'],
        files: ['file.item', 'file.list'],
        numbers: ['number', 'range'],
      }
    );
  5. How toCustom and toCustom$ work

    main

    The toCustom (or toCustom$ for Qwik) function allows you to define custom transformation logic.

    It accepts two parameters:

    1. A transformation function: This is executed if the current event type matches the one specified in the second parameter. The function receives:
      • value: The current value of the field.
      • event (in Solid/Preact/React) or element (in Qwik): The event object or element reference. You can use event.currentTarget (Solid/Preact) or event.target (React) to access the input element.
    2. TransformOptions: An object specifying when the transformation should occur (e.g., { on: 'input' }).
  6. Use form submission state and dirty values

    main

    Modular Forms provides built-in properties to manage the submission lifecycle and data efficiency:

    • Loading State: Use loginForm.submitting to detect if a submission is in progress. This is useful for triggering loading animations or disabling submit buttons.
    • Dirty Values: Set the shouldDirty property on the Form component to instruct the form to return only the values that have been modified. This is useful for reducing network traffic during updates.
  7. Configure Form submission behavior

    main

    The Form component provides several properties to control how data is filtered and how the submission lifecycle behaves:

    Data Filtering (via values parameter)

    When your onSubmit or onSubmit$ handler is called, the values parameter is filtered based on these properties:

    • shouldActive: (Default: true) If true, only values of active fields are provided. Set to false to include all fields.
    • shouldTouched: (Default: false) If true, only values of fields that have been touched are provided.
    • shouldDirty: (Default: false) If true, only values of fields that have been modified (dirty) are provided.

    Submission Lifecycle

    • keepResponse: (Default: false) By default, the response of the form is reset before executing the submit handler. Set to true to prevent this reset.
    • shouldFocus: (Default: true) If true, the component automatically focuses the first field with a validation error. Set to false to disable this.

    Qwik Specifics

    • encType: Controls the encoding type. If JavaScript is available, it defaults to sending data as JSON via Fetch. You can change this to application/x-www-form-urlencoded or multipart/form-data to send data as FormData.
    • reloadDocument: (Default: false) If set to true, it prevents the default behavior of not reloading the page.
  8. Handle internal vs external value formats with transformations

    main

    The transformation API can be used alongside controlled fields to maintain values in a different format internally (e.g., in your form state/database) than how they are displayed to the user.

    A common use case is handling monetary amounts: storing values as integers (cents) in the database while displaying them as decimals (e.g., € or $) in the UI.

    To implement this:

    1. Pass the internal format (e.g., cents) to the <Field />.
    2. In the field's render function, convert the value for display (e.g., divide by 100).
    3. Use the transform prop with toCustom (or toCustom$ in Qwik) to convert the user's input back to the internal format (e.g., multiply by 100) before the state updates.
    // Example pattern for SolidJS
    <Field
      name="price"
      type="number"
      transform={toCustom((value) => value && value * 100, {
        on: 'input',
      })}
    >
      {(field, props) => {
        const getValue = createMemo<number | undefined>((prevValue) =>
          !Number.isNaN(field.value) ? field.value && field.value / 100 : prevValue
        );
        return <input {...props} type="number" value={getValue()} />;
      }}
    </Field>
  9. Understand the Modular Forms design philosophy

    main

    Modular Forms is designed around three core pillars to provide a high-quality developer and user experience:

    1. Performance via Native Reactivity: The library is built natively on the underlying UI framework (SolidJS, Qwik, Preact, or React with Preact Signals). It leverages the fine-grained reactivity of these frameworks to ensure that only the code necessary for a specific action is executed, avoiding the heavy re-renders common in traditional controlled-component form libraries.

    2. Minimal Bundle Size via Modularity: Unlike libraries that bundle all logic into a single hook or component, Modular Forms uses a modular architecture. By using createForm (Solid) or useForm (Qwik/Preact/React) to create the form state and passing that state to specific methods, you only import the code for the features you actually use.

    3. Type Safety and Validation: The library is built with TypeScript, allowing you to define field types via generics. This enables full autocompletion and type safety for field names and values. Validation is handled through small, importable functions or via schema-based validation (e.g., Zod).

  10. Understand Field state: touched, dirty, and active

    main

    The Field component tracks several state properties that are useful for validation and UI feedback:

    • Touched: A field becomes touched once its blur event has been triggered at least once.
    • Dirty: A field is dirty if its current value differs from its initial value.
    • Active: A field is active when it is part of the DOM.

    Note on Active State: By default, Modular Forms only includes active fields when performing validation or returning form values. This is useful for conditional forms where fields might be hidden/shown based on other inputs.

    To change this behavior, you can:

    • Use the keepActive property on a Field component.
    • Use the shouldActive property or option on the Form component.
    • Use specific methods to ignore the active state entirely.
  11. Manage FieldArray activity and state

    main

    Field arrays automatically detect if they are still in use and update their status accordingly. You can control this behavior using keepActive and keepState:

    • Preventing Inactivity: If you want a field array to remain active even when it is no longer in use, set keepActive={true}.
    • Resetting State on Inactivity: By default, the state of an inactive field array is maintained. If you want the field to be reset when it becomes inactive, set keepState={false}.