0.9.1 • Published 4 months ago

@bedrockstreaming/form-builder v0.9.1

Weekly downloads
-
License
-
Repository
-
Last release
4 months ago

:construction_worker: form-builder

This library helps to dynamically build a form based on a definition schema and a dictionary of visual components.

:building_construction: Install

npm install @bedrockstreaming/form-builder

:rocket: Usage

In order to create a form using this library, you simply need to import the FormBuilder component, and instantiate it with the following props:

  • schema: an object that will contain all the fields you want to display in your form, as well as the steps and the stepsById.
  • dictionary: an object that will map all the fields of the schema with the React component you provide
  • onSubmit: the function you want to be called when the form is submitted

schema

You should provide a schema with the following structure:

import {
  DeepMap,
  DeepPartial,
  Path,
  PathValue,
  UnionLike,
  UnpackNestedValue,
} from 'react-hook-form';

export interface FormSchema {
  fields: {
    [key: string]: {
      id: string;
      type: string;
      meta?: {
        [key: string]: unknown;
      };
      dependsOn?: Array<
        | string
        | {
            key: string;
            value?: string | number | null | string[] | number[];
            callback: string;
          }
      >;
      validation?: {
        [key: string]: {
          key: string;
          type?: string;
          message: string;
          value?: unknown;
        };
      };
      defaultValue?:
        | UnpackNestedValue<PathValue<unknown, never>>
        | string
        | number
        | string[]
        | number[]
        | Path<string>;
    };
  };
  formMeta?: {
    shouldDisplayRequiredHint?: boolean;
  };
  steps: {
    [key: string]: {
      id: string;
      fieldsById: string[];
      submit: {
        label: string;
      };
      meta?: {
        [key: string]: unknown;
      };
    };
  };
  stepsById: string[];
}

See this stripped down example below of a single input form

const schema = {
  fields: {
    'some-unique-identifier': {
      id: 'some-unique-identifier',
      type: 'text',
    },
    // ...
  },
  steps: {
    'step-foo': {
      id: 'step-foo',
      fieldsById: ['some-unique-identifier'],
      submit: {
        label: 'foo', // submit button label
      },
    },
  },
  stepsById: ['step-foo'],
};

dictionary

You can then assign each type of field you have declared in your schema by configuring the dictionary prop:

const dictionary = {
  'some-unique-identifier': MyUniqueInput,
  submit: MySubmitButton, // mandatory field
  ...
};

Make sure the dictionary keys corresponds to your fields types.


Dictionary components (field components) must accept three props:

  • propRef: the field registered ref
  • name: the id of the field element
  • onChange: the onChange callback

Here are all the base props that will be passed to every FormField.

export interface FormFieldProps {
  id: string;
  validation?: Validations;
  errors?: ErrorOption;
  setFieldValue?: SetFieldValue<string | number>;
  triggerValidationField?: (value: Path<FieldValues>) => void;
  propRef?: Ref;
  disabled?: boolean;
  label?: string;
  onClick?: (event: any) => void;
  isValidating?: boolean;
}

:::tip

You can leverage the meta field property to pass more values to your field !

:::

onSubmit

The onSubmit callback is called when submitting the form, it follows react-hook-form API.


By default, the form schema must include steps, even for a single one.

Single step forms

Example usage of a form that will display one text input.

import React from 'react';
import { FormBuilder } from '@bedrockstreaming/form-builder';

const schema = {
  fields: {
    foo: {
      id: 'foo',
      type: 'text',
      meta: {
        label: 'Your name',
      },
    },
  },
  steps: {
    'single-step-form': {
      fieldsById: ['foo'],
      id: 'single-step-form',
      submit: {
        label: 'submit',
      },
    },
  },
  stepsById: ['single-step-form'],
};

const dictionary = {
  text: ({ value, onChange, label }) => (
    <input type="text" value={value} onChange={onChange} placeholder={label} />
  ),
  submit: ({ label }) => <button type="submit">{label}</button>,
};

const FormWrapper = () => {
  const onSubmit = (fieldValues) => {
    return someAPICall(fieldValues).then(() => ...);
  };

  return (
    <div>
      <FormBuilder
        schema={schema}
        dictionary={dictionary}
        onSubmit={onSubmit}
      />
    </div>
  )
}

Multi steps forms

Example usage of a form that will display two steps.

import React from 'react';
import { FormBuilder } from '@bedrockstreaming/form-builder';

const schema = {
  fields: {
    foo: {
      id: 'foo',
      type: 'text',
      meta: {
        label: 'some label',
      },
    },
    bar: {
      id: 'bar',
      type: 'text',
      meta: {
        label: 'some label',
      },
    },
  },
  steps: {
    'multi-step-form-1': {
      fieldsById: ['foo'],
      id: 'multi-step-form-1',
      submit: {
        label: 'next',
      },
    },
    'multi-step-form-2': {
      fieldsById: ['bar'],
      id: 'multi-step-form-2',
      submit: {
        label: 'submit',
      },
    },
  },
  stepsById: ['multi-step-form-1', 'multi-step-form-2'],
};

const dictionary = {
  text: () => <input type="text" placeholder="Your name" />,
};

const LAST_STEP_INDEX = schema.stepsById.length - 1;

const FormWrapper = () => {
  const [stepIndex, setStepIndex] = useState(0);

  const onSubmit = (fieldValues) => {
    return someAPICall(fieldValues).then(() => ...)
  };

  return (
    <div>
      <FormBuilder
        schema={schema}
        dictionary={dictionary}
        currentStepIndex={stepIndex}
        onSubmit={onSubmit}
        onNextStep={() => setStepIndex((oldIndex) => oldIndex === LAST_STEP_INDEX ? oldIndex : oldIndex + 1)}
        onPreviousStep={() => setStepIndex((oldIndex) => oldIndex === 0 ? oldIndex : oldIndex - 1)}
      />
    </div>
  );
};

Navigating in a multi-step form

This library doesn't provide steps state management by default. You can implement your own step management logic through the onNextStep and onPreviousStep callbacks, there you can change the currentStepIndex prop passed to the FormBuilder as it is done in the previous example.

:bulb: If you are using redux, we have a slice ready for you :point_right: @bedrockstreaming/form-redux

Dictionary

Validation

To do fields validation, we use the native implementation of react-hooks-form. We leverage it through the validation field property.

When we want to perform a complex or very specific validation, even async, we need to:

  • Create an object containing the custom validation functions and pass it to the extraValidation prop of the form-Builder
  • Reference those extraValidation functions in the schema config
const extraValidation = {
  customValidationFunction1: (valueFromSchema) => (fieldValue) =>
    doCustomValidationHere(valueFromSchema, fieldValue),
};

const schema = {
  fields: {
    birthdate: {
      // [...]
      validation: {
        customValidationFunction1: {
          // <-- this is a custom validation
          key: 'customValidationFunction1',
          message: 'forms.register.birthdate.minAgeError',
          value: 13,
        },
        required: {
          // <-- this is a default validation (native to react-hook-form)
          key: 'required',
          message: 'forms.required.error',
          value: true,
        },
      },
    },
  },
};

const MyForm = () => (
  <FormBuilder
    schema={schema}
    extraValidation={extraValidation}
    dictionary={dictionary}
    onSubmit={onSubmit}
  />
);

// More info on the official react-hooks-form doc : https://react-hook-form.com/get-started#Applyvalidation

ConditionalFields

You can add a dependsOn entry in any of your field schema.

export interface FormField {
  // [...]
  dependsOn?: Array<
    | string // an other field id
    | {
        fieldId: string; // an other field id
        key: string; // validation key
        value?: string | number | null | string[] | number[]; // any serializable value, works the same way as validation
        validate?: boolean; // perform an extra validation "manually"
      }
  >;
}

Using strings

When using a string, corresponding to a field id, the form builder will hide the field until those target field ids have been touched and validated.

const schema = {
  fields: {
    someField: {
      id: 'someField',
      // ...
    },
    myConditionalField: {
      id: 'myConditionalField',
      dependsOn: [
        {
          key: 'someField',
          callback: 'customValidationFunction1',
          value: 13,
        },
      ],
    },
  },
};

Using objects

Otherwise, when using objects, you can either check for a specific validation error or leverage the extraValidation functions to execute a

const extraValidation = {
  customValidationFunction1: (valueFromSchema) => (fieldValue) =>
    doCustomValidationHere(valueFromSchema, fieldValue),
  customValidationFunction2: (valueFromSchema) => (fieldValue) =>
    valueFromSchema === fieldValue,
};

const schema = {
  fields: {
    someField: {
      id: 'someField',
      validation: {
        key: 'customValidationFunction2'
        value: 'foo',
        message: 'Some error message'
      }
      // ...
    },
    myConditionalField: {
      id: 'myConditionalField',
      dependsOn: [
        {
          fieldId: 'someField',
          key: 'customValidationFunction1',
          value: 13,
          validate: true,
        },
        {
          fieldId: 'someField',
          key: 'customValidationFunction2',
        },
      ],
    },
  },
};

Examples

For real-world usage, see the demo app and the form-editor lib.

Contributing

Running unit tests

Run yarn nx test form-builder to execute the unit tests via Jest.

0.9.1

4 months ago

0.9.0

4 months ago

0.8.5-bump-nx.0

1 year ago

0.8.4

2 years ago

0.8.3

2 years ago

0.8.2

2 years ago

0.8.1

2 years ago

0.8.0

2 years ago

0.7.1

2 years ago

0.7.0

2 years ago

0.6.1

2 years ago

0.6.0

2 years ago

0.5.1

2 years ago

0.5.0

2 years ago

0.4.1

2 years ago

0.4.0

2 years ago

0.3.0

2 years ago

0.2.1

2 years ago

0.2.0

2 years ago

0.1.0

2 years ago

0.0.1

2 years ago