Formity Documentation

repository·main·Indexed 19 days ago

https://github.com/martiserra99/formity

A React-based framework for creating complex, logic-driven multi-step forms. Formity provides programmatic control over form flow using variables, conditions, loops, and switches, while remaining library-agnostic and compatible with tools like React Hook Form, Formik, and TanStack Form. It is distributed via a monorepo containing @formity/react for form building and @formity/system for core logic and utilities.

Tokens
15.8K
Snippets
42
Records
63
Agent score
64%

What's inside Formity

  1. Overview of Formity

    main

    Formity is a React library designed for building advanced multi-step forms. It provides full control over form flow by allowing developers to implement custom logic using variables, conditions, and loops, making forms highly dynamic.

    Formity is distributed via a monorepo containing two primary packages:

    • @formity/react: The main React library for building forms.
    • @formity/system: A helper library containing core logic and system utilities.
  2. Key capabilities of Formity

    main

    Formity is built around three core pillars for form development:

    1. Advanced Logic: Supports sophisticated flow control through conditions, loops, and variables, allowing the form to adapt dynamically to user input.
    2. Library Agnostic Integration: It is designed to integrate with any existing form-handling library, including React Hook Form, Formik, and TanStack Form.
    3. Advanced Type Inference: Provides deep TypeScript support with advanced type inference to ensure high type safety and improved autocomplete during development.
  3. Add eslint-plugin-react to your configuration

    main

    To use React-specific linting rules, install eslint-plugin-react and update your eslint.config.js. You must specify the React version in the settings object and include the plugin and its recommended rules (including jsx-runtime) in your configuration.

    // eslint.config.js
    import react from 'eslint-plugin-react'
    
    export default tseslint.config({
      // Set the react version
      settings: { react: { version: '18.3' } },
      plugins: {
        // Add the react plugin
        react,
      },
      rules: {
        // other rules...
        // Enable its recommended rules
        ...react.configs.recommended.rules,
        ...react.configs['jsx-runtime'].rules,
      },
    })
  4. Configure type-aware ESLint rules for production

    main

    For production applications, it is recommended to enable type-aware linting to catch more complex errors. This involves two steps: configuring parserOptions to point to your TypeScript configuration files and upgrading your recommended rule sets.

    1. Update parserOptions: Set the project property to include your tsconfig files and define the tsconfigRootDir using import.meta.dirname.
    2. Upgrade Rule Sets: Replace tseslint.configs.recommended with tseslint.configs.recommendedTypeChecked or tseslint.configs.strictTypeChecked. You can also optionally add ...tseslint.configs.stylisticTypeChecked.
    export default tseslint.config({
      languageOptions: {
        // other options...
        parserOptions: {
          project: ['./tsconfig.node.json', './tsconfig.app.json'],
          tsconfigRootDir: import.meta.dirname,
        },
      },
    })
  5. Implement conditional branching with s.Switch

    main

    The s.Switch abstraction allows you to create multi-step forms that branch into different paths based on previous user input.

    To use it, define a switch object within your Flow array. A switch consists of:

    1. branches: An array of branch objects. Each branch contains:
      • case: A predicate function that receives the current form state and returns true if the branch should be taken.
      • then: An array of steps (e.g., form or return) to execute if the case matches.
    2. default: An array of steps to execute if none of the branches match the current state.

    This pattern is useful for complex logic where the next set of questions depends entirely on a previous answer (e.g., asking "Why not?" if a user selects "No").

    import type { s } from "@formity/react";
    
    // Example structure within a Flow definition
    const myFlow = [
      {
        form: { /* initial step */ }
      },
      {
        switch: {
          branches: [
            {
              case: ({ choice }) => choice === 'yes',
              then: [
                { form: { /* step for 'yes' path */ } },
                { return: (data) => ({ choice: 'yes', reason: data.reason }) }
              ]
            }
          ],
          default: [
            { form: { /* fallback step */ } },
            { return: (data) => ({ choice: 'unknown', reason: data.reason }) }
          ]
        }
      }
    ];
  6. Define a loop in a Formity Flow

    main

    You can implement iterative logic within a Flow using the loop property. A loop requires a while condition and a do array of steps to execute in each iteration.

    Inside the while condition, you have access to the current state of variables. The do array can contain steps that update variables, render forms, or perform other logic. To maintain state across iterations (like an index or an accumulator), you must explicitly update those variables in a step within the do block.

    Common patterns include:

    1. Condition: Using a while function that checks a variable (e.g., i < list.length).
    2. Iteration Step: Using a variables step to extract the current item from a list based on the index.
    3. Form Step: Using a form step to collect data for the current iteration.
    4. Update Step: Using a variables step at the end of the do block to increment the index and append the new data to an accumulator array.
    export const loopFlow: Flow<LoopSchema> = [
      // 1. Initialize variables
      { variables: () => ({ i: 0, results: [] }) },
      
      // 2. Define the loop
      { 
        loop: {
          while: ({ i, items }) => i < items.length,
          do: [
            // Step A: Set current item context
            { variables: ({ i, items }) => ({ currentItem: items[i] }) },
            
            // Step B: Render the form for this item
            { 
              form: {
                fields: ({ currentItem }) => ({ value: ["field", [currentItem.id]] }),
                render: ({ fields, values }) => (
                  <Form {...fields}>
                    {/* Your UI components */}
                  </Form>
                )
              }
            },
    
            // Step C: Update index and accumulator
            { 
              variables: ({ i, results, currentItem, value }) => ({
                i: i + 1,
                results: [...results, { id: currentItem.id, value }]
              })
            }
          ]
        }
      },
    
      // 3. Return final results
      { return: ({ results }) => ({ results }) }
    ];
  7. Define a multi-step form flow with Formity

    main

    Formity uses a declarative Flow structure to define multi-step forms. A Flow is an array of step objects that can include forms, conditional logic, loops, variable management, and data yielding.

    Each step in the struct array defines a specific part of the user journey. Common step types include:

    • s.Form: Renders a form with specific fields and validation.
    • s.Yield: Defines how data is passed forward (next) or backward (back) between steps.
    • s.Condition: Branches the flow based on existing data using then and else arrays.
    • s.Variables: Manages local state/variables within a branch of the flow.
    • s.Loop: Repeats a sequence of steps based on a while condition.
    • s.Return: Finalizes the flow by transforming accumulated data into a final result object.

    To use this, you define a Schema type that describes the shape of the data at each step and then implement the flow array.

    import type { Flow, s } from "@formity/react";
    
    type Schema = {
      struct: [
        s.Form<{ name: string }>,
        s.Yield<{ next: [{ type: 'next'; data: { name: string } }]; back: [{ type: 'back'; data: { name: string } }]; }>,
        s.Return<{ name: string }>
      ];
      // ...
    };
    
    export const flow: Flow<Schema> = [
      {
        form: {
          fields: () => ({ name: ["", []] }),
          render: ({ fields, ...rest }) => (
            <Form defaultValues={fields} ...>
              <TextField name="name" ... />
            </Form>
          )
        }
      },
      // ...
    ];
  8. Define conditional branching in a Formity Flow

    main

    You can create multi-step logic in a Flow using the s.Condition structure. A condition block evaluates a predicate function (if) and branches the form flow into either a then array or an else array based on the result.

    • if: A function that receives the current form state and returns a boolean.
    • then: An array of steps to execute if the condition is true. This typically includes a form step followed by a return step to merge the new data into the main state.
    • else: An array of steps to execute if the condition is false.
    • return: A step used within a branch to map the local branch data back into the global form schema.

    This pattern allows for complex, nested logic where the final data object is a composition of the paths taken through the flow.

    import type { Flow, s } from "@formity/react";
    
    export const conditionFlow: Flow<ConditionSchema> = [
      {
        form: {
          fields: () => ({ softwareDeveloper: [true, []] }),
          render: ({ fields, ...rest }) => (
            <Form ... />
          ),
        },
      },
      {
        condition: {
          if: ({ softwareDeveloper }) => softwareDeveloper,
          then: [
            {
              form: {
                fields: () => ({ languages: [[], []] }),
                render: ({ fields, ...rest }) => (
                  <Form ... />
                ),
              },
            },
            {
              return: ({ languages }) => ({
                softwareDeveloper: true,
                languages,
              }),
            },
          ],
          else: [
            {
              form: {
                fields: () => ({ interested: ["maybe", []] }),
                render: ({ fields, ...rest }) => (
                  <Form ... />
                ),
              },
            },
            {
              return: ({ interested }) => ({
                softwareDeveloper: false,
                interested,
              }),
            },
          ],
        },
      },
    ];
  9. Understand the Flow and Module types

    main

    Formity uses two primary high-level types to define the structure and behavior of multi-step forms and modules. These types leverage TypeScript's type inference to map the form's structure (struct) to the data available during rendering.

    Flow<T>

    Used for a standalone multi-step form. It is derived from a Schema and determines the rendered output based on:

    • render: The type of the rendered output for each step.
    • struct: The structural definition of the form steps.
    • inputs: Values provided via the inputs prop.
    • params: Contextual values accessible during rendering.

    Module<T>

    Used for a reusable multi-step form module. It is derived from a ModuleSchema and includes an additional property:

    • values: The values collected throughout the module's lifecycle.

    Both types ensure that the data passed to rendering functions (like fields, variables, or condition checks) is strictly typed based on the form's structure.

  10. Understand the Formity Memory structure

    main

    Formity uses a hierarchical Memory structure to track field values and state across multi-step forms, nested elements, and logic blocks. The structure is recursive, allowing for complex forms containing lists, conditions, loops, and modules.

    At the top level, a Memory is a ListMemory. The state is composed of ItemMemory objects, which can be either a FormMemory (representing actual form fields) or a NestMemory (representing logical containers like loops or switches).

  11. Use condition, loop, and switch for advanced logic

    main

    Formity allows for complex branching and repetition within the form structure:

    condition

    Provides an if predicate. If true, the then branch is executed; otherwise, the else branch is executed.

    condition: {
      if: (values) => values.isRegistered === true,
      then: [...],
      else: [...]
    }

    loop

    Repeats a set of steps as long as the while predicate returns true.

    loop: {
      while: (values) => values.items.length > 0,
      do: [...]
    }

    switch

    Evaluates multiple branches. It checks case predicates in order and falls back to a default branch.

      branches: [
        { case: (values) => values.type === 'A', then: [...] },
        { case: (values) => values.type === 'B', then: [...] }
      ],
      default: [...]
    }
  12. Understand the Position type for form navigation

    main

    In Formity, a Position represents a specific location within a nested element of a multi-step form. It is a discriminated union used to track where a user or the form logic is currently situated within complex structures like lists, conditions, loops, or modules. When navigating or managing state, you will use the type field to determine which specific position structure you are working with.

    // Example of a Position object representing a slot in a list
    const currentPosition: Position = {
      type: "list",
      slot: 2
    };
    
    // Example of a Position object representing a branch in a condition
    const conditionPosition: Position = {
      type: "condition",
      branch: "then",
      slot: 0
    };