0.14.2 • Published 20 days ago

@bolttech/form-engine v0.14.2

Weekly downloads
-
License
ISC
Repository
-
Last release
20 days ago

Form Engine - Low code forms

Achieve form logic re-usage with forms expressed in json format.

stable version 0.14.2

Note: We do not recommend using versions 0.11.5 and 0.11.6 due to bugs in asFormField that unfortunately appeared with them. Note: Unfortunately we don't have compatibility with Next.js 14. This is a work in progress.


  1. Basic setup
  2. Step by step
  3. Form Features
  4. React

Basic setup

Serve your forms in JSON to your frontend, and allow it to be agnostic of your forms logic.

3 simple steps

  1. Map your components to the form (Section - Build your mappers)
  2. Build json schema
  3. Use it

1. BUILD MAPPERS

import Input from 'Components/Input';
import Other from 'Components/Other';

const Mappings = {
  input: { component: Input },
  other: { component: Other },
};

const formBuilderPropsMapping = {
  //default prop names
  __default__: {
    getValue: 'onChange',
    setValue: 'value',
  },
  //component specific prop names
  other: {
    getValue: 'onChangeCallback',
    setValue: 'data',
    setErrorMessage: 'errorMessageArray',
    setErrorState: 'isErrored',
    onBlur: 'onBlurCallback',
    onFocus: 'onFocusCallback',
    onKeyUp: 'onKeyUpCallback',
    onKeyDown: 'onKeyDownCallback',
  },
};

export { Mappings, formBuilderPropsMapping };

2. BUILD SCHEMA

{
  "components": [
    {
      "component": "",
      "name": "",
      "children": [
        {
          "component": "${componentName}",
          "name": "${componentFormName}",
          "props": {
            "fullWidth": true
          }
        }
      ]
    }
  ]
}

USE IT (React version)

import { Mappings, formBuilderPropsMapping } from './my-component-mappings';
import { getFormSchema } from './my-api-wrapper';
...
const schema = useMemo(() => getFormSchema('myInstanceContext'), []);

...
<Form mappings={Mappings} propsMappings={formBuilderPropsMapping} schema={schema}>

Nexts steps ? Checkout what you can do in the storybook with npm run storybook or see the best effort readme :(

Step by step

Build your mappers

The form uses mappings to connect to UI components so that its easy to connect to any set of components.

You can build your own mappings file or you can use the bolttech and if you want extend it with your set of components.

In the mappings file you need to specify the component definition and a name to refer in the JSON's latter, and how the form will connect to component props.

See this example

import Input from '@bit/bolttech.components.ui.input';
import Checkbox from '@bit/bolttech.components.ui.checkbox';
import FormGroup from '@bit/bolttech.components.common.form-group';

const Mappings = {
  input: { component: Input },
  checkbox: { component: Checkbox },
  formGroup: { component: FormGroup },
};

const formBuilderPropsMapping = {
  input: {
    getValue: 'onChange',
    setValue: 'value',
    setErrorMessage: 'errorMessage',
    setErrorState: 'isErrored',
    onBlur: 'onBlur',
    onFocus: 'onFocus',
    onKeyUp: 'onKeyUp',
    onKeyDown: 'onKeyDown',
  },
  checkbox: {
    getValue: 'onChange',
    setValue: 'checked',
  },
};

export { Mappings, formBuilderPropsMapping };

Here you say to the form that you can use in your JSON the names input, checkbox and formGroup and you tell the form how to get the props it needs from them.

If you have lots of components with the same prop names, you can, and should use __default__ key. This key allows to reuse prop names.

Lets say 10 components use to value prop name to set the component value, and onChange prop name to expose value. You van set your mapper the following way

import Input from '@bit/bolttech.components.ui.input';
import Checkbox from '@bit/bolttech.components.ui.checkbox';

const Mappings = {
  input: { component: Input },
  checkbox: { component: Checkbox },
};

const formBuilderPropsMapping = {
  __default____: {
    getValue: 'onChange',
    setValue: 'value',
  },
  checkbox: {
    getValue: 'onChange',
    setValue: 'checked',
  },
};

export { Mappings, formBuilderPropsMapping };

Setup Form provider

After setting your own mappings you encapsulate your app of your form with the provider

<FormProvider mapper={Mappings} propsMapping={formBuilderPropsMapping}>
  {children}
</FormProvider>

DONE. NOW build your forms

Form Features

Inside the schema you can specify several actions for a field alone or that correlate and have side-effects between them.

Those actions support multiple lifecycle and this must be on an action item basis:

  • ON_FIELD_MOUNT
  • ON_FIELD_CHANGE
  • ON_FIELD_BLUR
  • ON_FIELD_FOCUS
  • ON_FIELD_KEYUP
  • ON_FIELD_KEYDOWN
  • ON_FIELD_CLICK
  • ON_FIELD_CLEARED
  • ON_FIELD_BINDED
  • AFTER_FIELD_API_CALL

All the actions are typed, so you will have help here seeing which lifecycles you have available

Per action, you will be able to combine multiple lifecycle methods

All the following features can be inserted in the same location on the schema

{
  "component": "input",
  "name": "fieldName",
  "props": {
    "label": "My field"
  }
  //...your feature goes here
}

Validations

Like the name say, this feature lets you validate the form in the several lifecycle events of the form.

{
  "validations": {
    "ON_FIELD_BLUR": {
      "email": true
    },
    "ON_FIELD_CHANGE": {
      "required": true
    }
  }
}

The above example will let form know that in each change the field must have something in it, and that on blur, the value must be a email.

Named validations

There are cases where you want to build your own validation, agregating several of giving it a specific name that you can refer later

{
  "validations": {
    "ON_FIELD_BLUR": {
      "blurRequire": {
        "require": true
      }
    },
    "ON_FIELD_CHANGE": {
      "email": true,
      "changeRequire": {
        "require": true
      },
      "changeRestOfValidations": {
        "length": 50
        //...
      }
    }
  }
}

Error Messages

You can also specify the error messages you want.

{
  "validations": {
    "ON_FIELD_BLUR": {
      "require": true
    },
    "ON_FIELD_CHANGE": {
      "email": true
    }
  },
  "errorMessages": {
    "default": "Default error message",
    "email": "Invalid e-mail"
  }
}

This schema part, will add messages to validations error.

  • Each time the field has an e-mail error it will send the "Invalid e-mail" message to the component
  • If there is and field error, but no message is specified, it will send what you have in default key. In this example, required error does not have message and will send "Default error message"

With named validations

If you have a named validation, you can use its name in the error messages, having better granularity on it.

{
  "validations": {
    "ON_FIELD_BLUR": {
      "blurRequire": {
        "require": true
      }
    },
    "ON_FIELD_CHANGE": {
      "email": true,
      "changeRequire": {
        "require": true
      },
      "changeRestOfValidations": {
        "length": 50
        //...
      }
    }
  },
  "errorMessages": {
    "default": "Default error message",
    "email": "Invalid e-mail",
    "blurRequire": "When you blur, this component is required",
    "changeRequire": "You should not leave the field blank",
    "changeRestOfValidations": "You are changing into an incorrect state"
  }
}

Available validations (TBD)

Refer to the TAvailableValidations types here:

{
  /**
   * The bool function is a validation function that checks if a given value indicating whether the validation has failed or not.
   *
   * @example - in a test environment
   * ```
   * const result = bool({
   *   validationValue: false,
   * });
   * console.log(result); // { fail: false }
   * ```
   *
   * @example - real json usage with field value
   * ```
   * {
   *   validations: {
   *      bool: '${fields.input.value}'
   *   },
   * },
   * ```
   *
   * @example - real json usage with iVar value
   * ```
   * {
   *   validations: {
   *      bool: '${global.validation}'
   *   },
   * },
   * ```
   */
  bool?: string | boolean;
  /**
   * Validation based on conditions
   *
   * @example - Compare own field to two. Origin and target default to field value
   * ```
   * conditions: {
   *   rule: 'and',
   *   set: [
   *     {
   *       condition: '===',
   *       target: '2',
   *     },
   *     {
   *       origin: '2',
   *       condition: '===',
   *     },
   *   ],
   * },
   * ```
   * @example - Binded to Postcode field value. Must be greater than or equal two
   * ```
   * conditions: {
   *   rule: 'or',
   *   set: [
   *     {
   *       origin: '${fields.postcode.value}',
   *       condition: '>',
   *       target: '2',
   *     },
   *     {
   *       origin: '${fields.postcode.value}',
   *       condition: '===',
   *       target: '2',
   *     },
   *   ],
   * },
   * ```
   * @example - Binded to Postcode field value. Must be equal to two
   * ```
   * conditions: {
   *   rule: 'or',
   *   set: [
   *     {
   *       origin: '${fields.postcode.value}',
   *       condition: '===',
   *       target: '2',
   *     },
   *   ],
   * },
   * ```
   */
  conditions?: TVAvailableValidationConditions;
  /**
   * Applies multiple validations on the given value based on the specified truth table rule.
   *
   * @param {Object} TVAvailableValidationMultipleValidations - The multiple validations object param.
   * @param {'AND' | 'OR' | 'NOT'} TVAvailableValidationMultipleValidations.rule - The rule to be applied based of truth table ('AND', 'OR', or 'NOT').
   * @param {Object} TVAvailableValidationMultipleValidations.validations - Object containing validation rules.
   *
   * @example - Validating with the expression AND where current field must have a value of 'yes' and input 2 must have a value of 'no'
   * ```
   * multipleValidations: {
   *   rule: 'AND',
   *   validations: {
   *      value: 'yes',
   *      conditions: {
   *        rule: 'and',
   *        set: [
   *          {
   *            origin: '${fields.input2.value}',
   *            condition: '===',
   *            target: 'no'
   *          },
   *        ],
   *      },
   *   },
   * },
   * ```
   * @example - Validating with the expression OR where current field must have a value of '1995-12-12' or be a valid date with regex
   * ```
   * multipleValidations: {
   *   rule: 'OR',
   *   validations: {
   *      value: '1995-12-12',
   *      regex: '^\d{4}-\d{2}-\d{2}$',
   *   },
   * },
   * ```
   * @example - Validating with the expression NOT where current field doesn't have a value of '1995-12-12' and not be a valid date with regex
   * ```
   * multipleValidations: {
   *   rule: 'NOT',
   *   validations: {
   *      value: '1995-12-12',
   *      regex: '^\d{4}-\d{2}-\d{2}$',
   *   },
   * },
   * ```
   */
  multipleValidations?: TVAvailableValidationMultipleValidations;
  /**
   * Between validations
   *
   * @example - Between ages
   * ```    type teste = Pick<TVAvailableValidations, 'date'>
   * between: {
   *   dates: [
   *     {
   *       operator: '>=',
   *       origin: {
   *         format: 'YYYYMMDD',
   *         intervals: {
   *           years: 18,
   *         },
   *       },
   *     },
   *     {
   *       operator: '<=',
   *       origin: {
   *         format: 'YYYYMMDD',
   *         intervals: {
   *           years: 75,
   *         },
   *       },
   *     }
   *   ]
   * }
   * ```
   *
   * @example - Between numbers
   * ```
   *  between: {
   *    start: '3',
   *    end: '4',
   *    isIncludedBoundaries: true
   *  },
   *  ```
   *
   */
  between?: {
    /**
     * Array of date validations. To make it possible, ensure that the conditions is > and < in both date validations.
     */
    dates?: TVAvailableValidations['date'][];
    /**
     * The first number of the validation. If the current value is grater than this number, the validation will pass.
     */
    start?: number;
    /**
     * The second number of the validation. If the current value is lower than this number, the validation will pass.
     */
    end?: number;
    /**
     * If it's true, the comparision is transformed to >= and <=. So, the numbers that were passed are included in the validation.
     */
    isIncludedBoundaries?: boolean;
  };
  /**
   * Dates validations
   *
   * @example - Dates should be different
   * ```
   * date: {
   *   operator: '!==',
   *   origin: {
   *     format: 'DDMMYYYY',
   *   },
   *   target: {
   *     format: 'DDMMYYYY',
   *     value: '10/10/2001',
   *   },
   * },*
   * ```
   *
   * @example - Compare only valid dates using intervals
   * ```
   *  date: {
   *    onlyValidDate: true,
   *    operator: '<',
   *    origin: {
   *      format: 'DD/MM/YYYY',
   *      intervals: {
   *        years: 1,
   *      },
   *    },
   *  },
   *  ```
   *
   * @example - Should have at max 85 years
   * ```
   * {
   *    operator: '>',
   *    origin: {
   *      format: 'DDMMYYYY',
   *      value: eightyFiveYearsDate,
   *      intervals: {
   *        years: 85,
   *      },
   *    },
   *  }
   * }
   *
   * ```
   */
  date?: {
    /**
     * Flag to force only valid dates. Valid dates must be of min length of 8
     */
    onlyValidDate?: boolean;
    /**
     * List of operations you can do
     * - between origin and target dates
     * - between origin and intervals
     */
    operator: TValidationDateOperators;
    /**
     *  The origin configurations
     */
    origin: {
      /**
       * Origin date value
       */
      value?: string | number;
      /**
       * The available date formats
       */
      format: TValidationDateFormats;
      /**
       * Intervals to compare with the original date.
       *
       * It will use todays date for comparison.
       *
       * @example
       *
       * origin date = 10/10/2022
       * interval.year = 1
       * operator = '==='
       *
       * It will compare (10/10/2022 + 1) with the current date and check if they are the same
       */
      intervals?: {
        years?: number;
        months?: number;
        days?: number;
      };
    };
    target?: {
      value: string | number;
      format: TValidationDateFormats;
    };
  };
  /**
   * Allow to validate if the input string is a valid date format
   */
  validDate?: TValidationDateFormats;
  /**
   * Allow to define a maximum length for the input to have no error
   */
  length?: number;
  /**
   * Will look into the input length and send an error if if not greater than this value
   */
  greaterThan?: number | string;

  /**
   * Specifies a regular expression pattern that the value should match.
   */
  regex?: string;

  /**
   * Specifies the maximum length of the value (if it's a string).
   */
  maxLength?: number;

  /**
   * Specifies the minimum length of the value (if it's a string).
   */
  minLength?: number;

  /**
   * Specifies whether the field is required.
   */
  required?: boolean;

  /**
   * Specifies that the value should contain only letters.
   */
  onlyLetters?: boolean;

  /**
   * Specifies a value that the field should match.
   */
  value?: string | number | boolean;

  /**
   * Specifies that the field should not be empty.
   */
  notEmpty?: boolean;

  /**
   * A callback function that performs custom validation on the value.
   * @param value - The value to be validated.
   * @returns An object with validation results.
   */
  callback?(value: string | number): { fail: boolean; errorMessage?: string };

  /**
   * Specifies a numeric range for the value.
   */
  numericRange?: { start: number | string; end: number | string };

  /**
   * Specifies whether the value should be a number.
   */
  isNumber?: boolean;

  /**
   * Specifies that the field should not have extra spaces.
   */
  hasNoExtraSpaces?: boolean;

  /**
   * Specifies that the value should be an email address.
   */
  email?: boolean;

  /**
   * Specifies that the value should be less than a given number or string.
   */
  lessThan?: number | string;

  /**
   * Specifies that the value should be a sequential number.
   */
  sequentialNumber?: boolean;

  /**
   * Specifies that the value should not contain repeated numbers.
   */
  repeatedNumbers?: boolean;

  /**
   * Specifies that the value should be a URL.
   */
  url?: boolean;

  /**
   * Specifies a path error type.
   */
  path?: TPathError;

  /**
   * Specifies that the value should be a valid credit card number.
   */
  isCreditCard?: string[];

  /**
   * Specifies that the value should be a valid credit card number with a specific length.
   */
  isCreditCardAndLength?: string[];

  /**
   * Specifies that the value should match a credit card code with available options.
   */
  isCreditCodeMatch?: { numberCard: string; availableOptions: string[] };

  /**
   * Specifies custom validation options for the field.
   */
  customValidation?: ICustomValidationValue[];

  /**
   * Specifies that spaces are not allowed in the field.
   */
  notAllowSpaces?: true;

  /**
   * Specifies that the value should be in a predefined list of values.
   */
  isInTheList?: string[] | number[] | string;

  /**
   * Specifies field validation rules for nested fields.
   */
  fields?: {
    /**
     * The rule for validating nested fields (e.g., 'every').
     */
    rule: 'every';

    /**
     * An array of nested field validation configurations.
     */
    set: {
      /**
       * The binding for the nested field.
       */
      bind: string;

      /**
       * The field name for the nested field.
       */
      fieldName: string;

      /**
       * Validation options for the nested field, excluding the 'fields' property.
       */
      validations: Omit<TVAvailableValidations, 'fields'>;
    }[];
  };

  /**
   * Specifies that the value should be existed.
   */
  exists?: string | number ;
}

Formatters

Formatting a field means mutating the field value to a given... format.

This options will allow you to force a give field to have the format you whant while the user is performing some action on the form.

NOTE - When receiving the values of the form, you will have the value with the specified format, not the raw value the user entered

You have several formatters. THe following example shows splitter that is a more generic one, allowing you to split the input text

BIG NOTE - Formatters won't execute when a value is deleting (eg. user pressed backspace to delete a character), to execute on every value change, use masks instead

{
  "formatters": {
    "ON_FIELD_MOUNT": {
      "splitter": [
        {
          "position": 2,
          "value": "/"
        },
        {
          "position": 5,
          "value": "/"
        }
      ]
    }
  }
}

The above example will split your word in position 2 and 5, adding there the /. This will give you a date format like 10/10/1987 (you would have to limit the input length. More on that on FILTERS)

Available Formatters (TBD)

Refer to the types on TSchema

Regex

Specifies a regular expression pattern that the value should match to be replaced.

{
  "formatters": {
    "ON_FIELD_MOUNT": {
      "regex": "[0-9]"
    }
  }
}

Callback

Specifies a function that will format the field

const removeDots = (value: string | number): string | number => {
  return value.split('.').join('');
};

const formatterInstance = {
  formatters: {
    ON_FIELD_CHANGE: {
      callback: removeDots,
    },
  },
};

Masks

Mask has the same functionality of formatter, but keed the original value for your program. Think of it like the password mask. You input something into your text input, mask that something with * but you need to read the original value. FOr Eg.

{
  "masks": {
    "ON_FIELD_BLUR": {
      "replaceAll": "*"
    },
    "ON_FIELD_FOCUS": {
      "cleanMask": true
    }
  }
}

In this example, you will

  • Mask a given text input from for example 123345 to ******
  • On Focus , you tell form to clean the mask with cleanMask directive.

Available Masks (TBD)

Refer to the types on TSchema

Callback

Specifies a function that will format the field

const removeDots = (value: string | number): string | number => {
  return value.split('.').join('');
};

const formatterInstance = {
  formatters: {
    ON_FIELD_CHANGE: {
      callback: removeDots,
    },
  },
};

Filters

Filters very predictable and work like the word says, they filter a given word to a given patter/directive.

Lets say you want a field to accept only numbers and with a max length o X.

{
  "filter": {
    "ON_FIELD_CHANGE": {
      "length": 4,
      "isNumber": true
    }
  }
}

This example will let you do just that. Only numbers and max length of 4

Available filters (TBS)

Refer to the types on TSchema

Visibility conditions

Field level

Sometimes you want to hide other fields based on certain conditions.

That is what this feature does.

Eg: You want to hide another field, when a given field originalField has a given value on it.

[
  {
    "name": "originalField",
    "component": "checkbox",
    "visibilityConditions": {
      "ON_FIELD_MOUNT": [
        {
          "validations": {
            "value": "Yes"
          },
          "fieldName": "targetField"
        }
      ],
      "ON_FIELD_CHANGE": [
        {
          "validations": {
            "value": "Yes"
          },
          "fieldName": "targetField"
        }
      ]
    },
    "props": {
      //...
    }
  },
  {
    "name": "targetField",
    "component": "input",
    "props": {
      //...
    }
  }
]

This example tells form to

  • On mount check if originalField has the value Yes
  • If it put the targetField visible
  • Otherwise, make it invisible

You can also for each visibility condition, apply it to multiple field names with fieldNames key that will accept an array.

{
  "visibilityConditions": {
    "ON_FIELD_MOUNT": [
      {
        "validations": {
          "value": "Yes"
        },
        "fieldNames": ["targetFieldOne", "targetFieldTwo"]
      }
    ]
  }
}

NOTE - When the field is hidden using this feature, the form will not try to validate it and will not be accounted to the general form state

Form Level

You can also use those in form level.

<Form
  iVars={{ roofUpdated: state }}
  initialValues={{ roofUpdatedYear: 'diogos' }}
  schema={{
    visibilityConditions: {
      ON_FORM_MOUNT: [
        {
          validations: {
            value: '${global.roofUpdated}',
          },
          fieldName: 'roofUpdatedYear',
        },
      ],
      ON_FIELD_CHANGE: [
        {
          validations: {
            value: 'abc',
          },
          fieldName: 'roofUpdatedYear',
        },
      ],
    },
    components: [{...}],
  }}
/>

in the above example we are applying the rule:

  • in form mount we will hide the value when the roofUpdatedYear equals to the iVar roofUpdated
  • in each field change we will hide the value when the roofUpdatedYear equals to abc
  • also you can use the prop rule to decide what type of validations rules to execute (and == every | or == some)
  • if you want to show an field only if all or at least one validation is true, use showOnlyIfTrue property.

Clear Fields

Guess what... THis will clear one or more form fields :)

Uses the same mechanism of VISIBILITY CONDITIONS.

Let's say you want to clear a given field when originalField has a given value.

{
  "clearFields": {
    "ON_FIELD_CHANGE": [
      {
        "validations": {
          "value": "Yes"
        },
        "field": "targetValue",
        "clearedValue": false
      }
    ]
  }
}

When form fires ON_CHANGE this will have the effect of having the field targetValue with the value false if originalField has value Yes.

Just like before, you can specify multiple fields with fields key for the same rule.

{
  "clearFields": {
    "ON_FIELD_CHANGE": [
      {
        "validations": {
          "value": "Yes"
        },
        "fields": ["targetValue"],
        "clearedValue": false
      }
    ]
  }
}

Additionally, you can specify multiple props to be changed or cleared from the fields that you specified before using clearedProps. Each prop will be changed using the respective field. For example, if I have fields['input', 'checkbox'] then I need to change the input label I can send it as clearedProps: [{ label: 'value' }] or clearedProps: { label: 'value' }. And only input props will be changed. It's useful when we have some API calls whose response will be used as a prop value in the component.

{
  "clearFields": {
    "ON_FIELD_CHANGE": [
      {
        "validations": {
          "value": "Yes"
        },
        "field": "targetValue",
        "clearedValue": false,
        "defaultClearedValue": true,
        "defaultClearedProps": {
          "disabled": false
        }
      }
    ]
  }
}

If you want to set a value when the validation passes you can set defaultClearedValue prop on the event to set a value on the cleared field. Useful after an API call to set a default response value or a value stored earlier. You can also set a defaultClearedProps attribute to change the props of the component if the validation passes.

For definition clearValues runs validations on the target field and not on the field it is declaring. Therefore, you can set useCurrentFieldValidation prop with true if you want validations to be performed on the current field.

Remember, clearValues only performs rehydration on the target field, that is, if your target field has an api call, for example, you must listen to the ON_FIELD_CLEARED event to execute such a call on the target field.

Api

This one will let you instruct form to call a give API at a given lifecycle method

{
  "api": {
    "ON_FIELD_CHANGE": [
      {
        "blockRequestWhenInvalid": true,
        "method": "GET",
        "url": "https://api.chucknorris.io/jokes/random",
        "scope": "chuck"
      }
    ]
  }
}

The above example will make form to call the API specified when the field where we gave the directory changes.

Keys

keytypeDescription
blockRequestWhenInvalidbooleanSpecify if this call should be blocked when the field is invalid (due to validations)
methodstringHTTP verb. Get, Post, Put or delete
urlstringThe api url
scopestringThis lets you put the api result inside the form scope in the given key. THis will allow to use the call result latter on on some field
bodyobjectBody to send to the API
headersobjectApi headers
fieldValueAsParamsobjectAn object with key (name of the field in the form) and value (name that I want to be in the query, which if nothing is passed, uses the name of the field as the key) and then it transforms it into a query string
fieldValueAsPathParamsarray of stringsan array of field names where he uses it as a key to capture the value and transforms it into path param
debounceTimenumberAllow you to debounce the api call by X seconds
preConditionsTValidationsAllow you to specify validations that should not fail in order to call the API

PreConditions

You can specify the pre-conditions that need to be met, in order for the request to start.

{
  "api": {
    "ON_FIELD_CHANGE": [
      {
        "method": "GET",
        "url": "https://api.chucknorris.io/jokes/random",
        "scope": "chuck",
        "preConditions": {
          "required": true,
          "value": "run"
        }
      }
    ]
  }
}

In the above example, the api specified will only be called

  1. When field has changes
  2. When field has values (required validation)
  3. When the field value is "run"

FieldValueAsParams

You can add as many query parameters as you want, but remember that it will search for a field that exists, otherwise it will not be included in the call.

{
  "api": {
    "ON_FIELD_CHANGE": [
      {
        "method": "GET",
        "url": "https://api.chucknorris.io/jokes",
        "scope": "chuck",
        "fieldValueAsParams": {
          "input": "name",
          "date": ""
        }
      }
    ]
  }
}

From the example above, the call will have the following structure:

# GET https://api.chucknorris.io/jokes?name=random&date=2013-20-01

When one of the parameters has a key but no value, the key will be reused in the query string.

FieldValueAsPathParams

With this parameter, it is possible to use the value of any form field as path param for the http call, as follows:

{
  "api": {
    "ON_FIELD_CHANGE": [
      {
        "method": "GET",
        "url": "https://api.chucknorris.io/jokes",
        "scope": "chuck",
        "fieldValueAsPathParams": ["input", "date"]
      }
    ]
  }
}

From the example above, the call will have the following structure:

# GET https://api.chucknorris.io/jokes/random/2013-20-01

So far he only puts one after the other, he still doesn't have the intelligence to choose where to put each.

Data binding

Form has a functionality to allow you to build your logic inside the schema via templating.

You can emit and register to data between fields. For example, in the following example field one will register to changes on field two and its label will have the field two value. This is accomplished with scopes.

[
  {
    "name": "one",
    "component": "input",
    "props": {
      "label": "${fields.two.value}"
    }
  },
  {
    "name": "one",
    "component": "input",
    "props": {
      //...
    }
  }
]

The subscription is done using the template first keys. In this case fields and two. Telling the engine that anytime the namespace fields and key two changes it should fire a notification to anyone interested. In this case, field one is interested

Scope

For templating to work, form relies on scope. The definition of scope is just a datastructures that has multiple keys each one with their context. The following table explain the namespaces

namespacedescription
globalThis namespace contains all the data that comes from the client implementing the Form and is injected in iVars
fieldsAutomatically generated scope. This namespace contains all the fields with everything that is done in them per field. Eg: value, errors, visible, mask etc. Refer to the types for more info
apiThis scope is where you can store the api responses with the api scope key.
hooksThis one is retrieved by the hooks configured on the client
configsAll the configs that the client gave to the form, will be stored here

Templating basically allows a given component to subscribe to any scope change, be notified and changed according to that. In the following example, the component named make is subscribed to api namespace on data key.

{
  "name": "make",
  "component": "dropdown",
  "props": {
    "id": "make",
    "name": "make",
    "label": "Make",
    "placeholder": "",
    "options": "${api.makes.data||[]}"
  }
}

This means that, anytime that api.makes.data changes (done by api action with scope = data), this component will be injected with its value on the options key. It also has a default value of empty array ...data||[]}.

If you want you can even nest templating also. Next example we will access to global namespace on name key. But we will access the prop dynamically from the field named myfield value

{
  "component": "input",
  "name": "destination",
  "props": {
    "name": "destination",
    "label": "Dynamic -> ${global.name.${fields.myfield.value||test}}"
  }
}

This will result in the following. Assume we have scope like

{
  "global": {
    "name": {
      "test": "test",
      "other": "other"
    }
  }
}

It will access global.name.test since we have the default value as test and there is no data in fields scope. The end result would be "Dynamic -> test"

But right after input on the form field named myfield, its scope will be populated

{
  "global": {
    "name": {
      "test": "test",
      "other": "other"
    }
  },
  "fields": {
    "myfield": {
      "value": "other"
      //...
    }
  }
}

In this case would access global.name.other and the final result would be "Dynamic -> other"

Templates

We talked about templates, but let's go a step further. THe definition of template, is a just a string that has a given prefix and suffix like ${...}.

Whatever comes inside the delimiters will be later extracted by the engine and mapped with the current scope in order to find a replacement value.

The only limitation is that the template must be a string representing an object path. That object path will be looked for in the scope like #{api.myapicall.response.data.value}.

Default values

You can set template default values with || like ${fields.foo.value||default-value}. This will lead to, if the scope has value in fields.foo set the value in template value, otherwise set the string default-value. You can also use as many values as you want as defaults. Only what is valid and has a value will go into the field, otherwise it will be undefined.

Template nesting

You can also nest multiple templates reaching extreme situations. For example

${fields.${gloval.fieldname||foo}.value||novalue}

This example will give you the following possible replacements:

  • If global.fieldname exists and has value bar for example, and form bar field contains value lets say value 2 - Output will be 2
  • If global.fieldname exists and has value bar for example, and form bar field does not contain value - Output will be non value
  • If global.fieldname does not exists , and form foo field does not contain value - Output will be non value
  • If global.fieldname does not exists , and form foo field contains value lets say value 3 - Output will be 3

VarOps

Templates are already a great power of form-engine, but we can go further allowing operations to be specified in the schema. Those operations are all under varOps (variable operations).

{
  "component": "input",
  "name": "password",
  "errorMessages": {
    "required": "Password is required",
    "value": "Error value must be varOps.concatenate(${fields.email.value||0},${fields.email2.value||0})"
  },
  "validations": {
    "ON_FIELD_CHANGE": {
      "required": true,
      "value": "varOps.concatenate(${fields.email.value||0},${fields.email2.value||0})"
    }
  },
  "props": {
    "variants": "default_border",
    "placeholder": "Please enter your password",
    "label": "Password"
  }
}

In the example the validation value comes from a varOps. This example uses the concatenate operations exposed by the engine.

Here this field (password) will register with templating to field email and email2. Meaning, each time they change this field schema will be recomputed with what changed to replace the needed values.

When this happens, lets say email has value foo and email2 value bar. The varOp concatenate will be called with the correct field replaced values

varOps.concatenate('foo', 'bar');

This will map to an operation function and the function return value will be replaced by the varOps like

{
  "validations": {
    "ON_FIELD_CHANGE": {
      "required": true,
      "value": "foo_bar"
    }
  }
}

Since we are already using templates to run our varOps and subscribe to changes, also the error message string subscribed to the operation result. In this example we would endue with the following messages.

{
  "errorMessages": {
    "required": "Password is required",
    "value": "Error value must be foo_bar"
  }
}

PS: Don't's forget that we still have the default values provided with templates and the rules are the same

Available VarOps

  • concatenate(arg1,arg2)
  • add(arg1,arg2)
  • subtract(arg1,arg2)

Direct fields binding

You can change multiple field values and properties with bindFields using the form ref in the react adapter, example:

{
  const ref = useRef < TFormRefActions > null;

  return (
    <>
      <Form id="form" ref={ref} schema={foo} />
    </>
  );
}

const handleFields = (input1: string, input2: number) =>
  ref.current?.bindFields({
    input1: {
      value: input1,
      props: {
        disabled: false,
      },
    },
    input2: {
      value: input2,
      props: {
        disabled: true,
      },
    },
  });

All schema fields that has the same name as the passed keys will have his values/properties changed and validations executed

Also, you can listen to the ON_FIELD_BINDED event to execute any handler on the target field.

State

This key will allow you to set up some initial state on the field.

hidden

Hidden prop on state, will turn your field visible or invisible

{
  "state": {
    "hidden": true
  }
}

This will be reflected on the field scope.

ignoreValue

Ignore component value prop on state, will control whether the property with the formatted value will be created.

{
  "state": {
    "ignoreValue": true
  }
}

Very useful when using group ownership, to limit who will actually send the value.

Rehydrate

DEPRECATED, you can accomplish it with templating (previous section)

It lets you rehydrate a given field

{
  "rehydrate": {
    "ON_FIELD_CHANGE": [
      {
        "validations": {
          "required": true
        },
        "fields": ["destination"]
      }
    ]
  },
  "component": "dropdown",
  "name": "originalField"
}

The api is pretty much like visibility conditions. The above example will rehydrate the destination field when field with the directive (originalField) meets the validations configured

Group

In form, we can correlate fields into a single field name. This is called the group functionality.

Say you have two checkboxes and want the selected value. You can use group for that

[
  {
    "name": "checkOne",
    "group": "checkedGroup",
    "component": "checkbox",
    "props": {
      //...
    }
  },
  {
    "name": "checkTwo",
    "group": "checkedGroup",
    "component": "checkbox",
    "props": {
      //...
    }
  }
]

This example will store the selected value of the checkbox in the checkedGroup and will then be sent to the client.

Form Step

In the form it is possible to use it as steps. With this it is possible to have more than one form in just one schema.

For example, if you implement something like:

{
  components: [
    { component: '', name: 'step1', children: [] },
    { component: '', name: 'step2', children: [] },
    { component: '', name: 'step3', children: [] },
  ];
}

You can control the go back and forth event from the onClick event of a button inside one of the forms using the form reference. Or Simply using the form reference in a button outside the form, for example:

{
  const ref = useRef < TFormRefActions > null;

  return (
    <Form id="form" ref={ref} onClick={() => ref.current?.stepForward()} />
  );
}

// --------------------- OR --------------------- //

{
  const ref = useRef < TFormRefActions > null;

  return (
    <>
      <Form id="form" ref={ref} />
      <button onClick={ref.current?.stepBack()} />
    </>
  );
}

However, it is possible to choose which step you want to go or go back, using the numerical index or simply the name of the step you want, for example:

() => ref.current?.stepForward(2);
// --------------- OR -------------- //
() => ref.current?.stepForward('step3');

() => ref.current?.stepBack(0);
// --------------- OR -------------- //
() => ref.current?.stepBack('step1');

Additionally, you can use a step method provided by form reference to easily set a desired step using the onFormMount event. For example:

<Form ref={ref} schema={schema} onFormMount={() => ref.current?.step(1)} />

Remembering that this method has a mandatory index parameter, which can either be the step index number in the array or simply its defined string name in the schema.

React Components

React <FormProvider />

React context that lets you provide configuration information to your application forms

Props

PropTypeDescription
mapperTMapperAllow you to map your own components to be used with the form
propsMappingTPropsMappingMap your component props names with the form functionalities

Example

The following example shows a provider that will provide forms with input and Dropdown component

import Input from 'Components/Input';
import Dropdown from 'Components/Dropdown';

const Mappings = {
  inputForm: { component: Input },
  dropdownForm: { component: Dropdown },
};

const propsMapping = {
  inputForm: {
    getValue: 'onChange',
    setValue: 'value',
  },
  dropdownForm: {
    getValue: 'onChangeCallback',
    setValue: 'data',
    setErrorMessage: 'errorMessageArray',
    setErrorState: 'isErrored',
    onBlur: 'onBlurCallback',
    onFocus: 'onFocusCallback',
    onKeyUp: 'onKeyUpCallback',
    onKeyDown: 'onKeyDownCallback',
  },
};

const App = () => {
  return <FormProvider mapper={Mappings} propsMapping={propsMapping} />;
};

You now can use in your form the mapped components with names inputForm and dropdownForm.

Also note the data in propsMapping. There you can map up to five form functionalities per component

KeyFunctionality
getValueThe name of your component prop that will give back the selected value.
setValueProp name that receives the value
setErrorMessageComponent prop name to receive an error message in case this field will have error and error message is configured
setErrorStateComponent prop name to receive a boolean indicating if the field has an error or not according to the configurations
onBlurProp name that is called when field is blured
onFocusProp name that is called when field is focused
onKeyUpProp name that is called when user releases a key on field
onKeyDownProp name that is called when user presses a key on field

You can also use default prop names for functionalities like:

import Input from 'Components/Input';
import Dropdown from 'Components/Dropdown';

const Mappings = {
  inputForm: { component: Input },
  dropdownForm: { component: Dropdown },
};

const propsMapping = {
  __default__: {
    getValue: 'onChangeCallback',
    setValue: 'data',
    setErrorMessage: 'errorMessageArray',
    onBlur: 'onBlurCallback',
    onFocus: 'onFocusCallback',
    onKeyUp: 'onKeyUpCallback',
    onKeyDown: 'onKeyDownCallback',
  },
};

const App = () => {
  return <FormProvider mapper={Mappings} propsMapping={propsMapping} />;
};

This will make form search for those names in all your components that do not have split mapping.

React <Form />

After configuring the provider, <Form /> components lets you render a form

Props

PropTypeDescription
disablebooleanDisable all form inputs. It will use the default htm disable attribute
groupstringForm group identifier. Used be able to group several forms and then get data with useGroupForm. One will be generated as default if omitted
idstringForm identified. One will be generated as default if omitted
hooksTHooksProvide functions to run on certain life-cycle events
iVarsObjectOne object with internal variables to be used in form with data binding
initialValuesObjectObject with form initial values that will map to a field.
SchemaTSchemaForm Schema
autoCompletestringHTML autocomplete
classNamestringAllow to style form
onSubmitcallback(HTMLFormElement,TFormValues)Will be called when there is a submit action in the form
onDatacallback(TFormValues,TComponent, TField)Will be called when any field data change. The arguments will let you know which field changed and the field configuration
onBlurcallback(TFormValues,TComponent, TField)Will be called when any field blured. The arguments will let you know which field blured and the field configuration
onFocuscallback(TFormValues,TComponent, TField)Will be called when any field focused change. The arguments will let you know which field focused and the field configuration
onFieldMountcallback(TFormValues,TComponent, TField)Will be called when some field mounted. Its called with the field that information that mounted.
onStepcallback(TFormValues)Called when a form step changed
onLogcallback(TLoggingEvent)Called on each log, if the logging is enabled
onScopeChangeonScopeChange?(scope: TScope, namespace: string, key: string): void;Called everything scope change with the changing information (namespace and key) and the changed scope
onClickonClick(TFormValues, TField)Callback function that runs on each component click
afterApiCallafterApiCall(values: TFormValues, component?: TComponent, field?: TField)Callback function that runs on each component after api call.
onFieldRehydrateonFieldRehydrate?(values: TFormValues, component: TComponent, field: TField): voidThis callback is called whenever some form field was rehydrated
renderLoadingrenderLoading?(): ReactElement;Component to render while the schema has not rendered
onFormMountonFormMount?(values: TFormValues): void;Called when the form finished mounted
formattedDataDefaultsObjectSome default data to fields when they are undefined
submitOnValidOnlybooleanBoolean indicating if form can be submitted even if it is invalid
renderFieldWrapperrenderFieldWrapper(component: TComponent, children: ReactElement[])Function that allows to insert a wrapper in place of a component or wrapping the component

Example

A simple example of rendering a basic form

<Form schema={schema} />

React useForm()

Exposed hook that allows you to connect to any form by the formId in any part of the application, as long as you are inside the form context.

Props

You can use the following arguments to tho hook

PropTypeDescription
idstringThe id of the form you want to connect to
idsarray of stringA range of ids of the each form you want to connect to
onValidcallbackCalled whenever form validation changes
onClickcallbackCalled whenever field has clicked
onDatacallbackCalled whenever data changes
onSubmitcallbackCalled whenever form is submitted

And it will provide you the following

PropTypeDescription
submitFormfunctionFunction that lets you call the submit on the form. After, the onSubmit callback will be called
formDatafunctionLets you get the most up-to-date form date

Example

In the following example useForm hooks are used to connect to multiple forms that are inside other components.

  const Comp = () => {
    const { submitForm: submitOne } = useForm({
      id: 'id1',
      onData: (data) => {},
      onSubmit: () => {},
    });
    const { submitForm: submitTwo } = useForm({
      id: 'id2',
      onData: (data) => {},
      onSubmit: () => {},
    });

    return (
      <>
        <button onClick={(() => submitOne())}>
        <button onClick={(() => submitTwo())}>
      </>
    );
  }

  const CompOne = () => {
    return (
      <Form id="id1" {...}/>
    );
  }

  const CompTwo = () => {
    return (
      <Form id="id2" {...} />
    );
  }

React useFormGroup()

Similar to useForm

Props

You can use the following arguments to tho hook

PropTypeDescription
idsarray of stringThe ids we want to listen to
groupstringA string to identify the form group we want to connect to
onValidcallbackCalled whenever form validation changes
onClickcallbackCalled whenever field has clicked
onDatacallbackCalled whenever data changes
onSubmitcallbackCalled whenever form is submitted

And it will provide you the following

PropTypeDescription
submitFormfunctionFunction that lets you call the submit on the form. After, the onSubmit callback will be called
formDatafunction({aggregate})Lets you get the most up-to-date form date in two ways. Aggregate the forms data in a single object or split by the several forms in the group or identified by the id

Example

In the following example useForm hooks are used to connect to multiple forms that are inside other components.

  useForm({
    onSubmit: () => {
      dispatch(
        formData({
          aggregate: true,
        }),
      );
    },
    id: 'main-form',
  });
  const { formData } = useFormGroup({
    onSubmit: (data) => {
      console.log('SUBMIT', data);
    },
    onData: (data) => {
      console.log('-> ', data);
    },
    group: 'logical',
  });

  return (
    <Form
      id="1"
      group="logical"
      schema={...}
    />
    <Form
      id="2"
      group="logical"
      schema={...}
    />
    <Form
      id="main-form"
      schema={...}
    />
  )

The above example will connect to main-form with useForm and to a form group (logical) with useFormGroup

React asFormField()

Allows you to add separate components from a standard form engine form schema. Transforming it into a field on a standard form.

Props

You can use the following arguments declare a component as a form field

PropTypeDescription
CompReact Function ComponentThe component to be used as form field.
propsMappingTComponentPropsMappingLink for the TComponentPropsMapping likely props mapper from default form.

And this will give you the following properties in addition to the native ones of your component

PropTypeDescription
validationsTSchemaValidationSetup validations to be used in the field.
masksTSchemaMasksAllows you to display the value of the masked component by events
clearFieldsTSchemaClearFieldsWill clear target fields in case they do not pass with the specified validations
apiTSchemaApiAllows you to make api calls using events emitted by the component.
errorMessagesTErrorMessagesField error messages in case any validation fails
filterTSchemaValidationFilters the component value based on a validation.
formattersTSchemaFormattersAllows you to format the value that the field will receive for each event issuance
visibilityConditionsTSchemaVisibilityConditionsAllows you to specify the conditions a given field will be visible what will run when this field meets the specified life-cycle
0.14.2

20 days ago

0.14.1

22 days ago

0.13.0

30 days ago

0.14.0

29 days ago

0.12.2

1 month ago

0.12.1

1 month ago

0.12.0

1 month ago

0.11.8

2 months ago

0.11.9

2 months ago

0.11.7

2 months ago

0.11.5

2 months ago

0.11.6

2 months ago

0.11.4

3 months ago

0.11.2

3 months ago

0.11.3

3 months ago

0.11.1

3 months ago

0.11.0

3 months ago

0.10.8

3 months ago

0.10.7

3 months ago

0.10.6

3 months ago

0.10.5

3 months ago

0.10.4

3 months ago

0.10.2

3 months ago

0.10.3

3 months ago

0.10.1

3 months ago

0.10.0

3 months ago

0.9.0

3 months ago

0.8.0

4 months ago

0.7.1

4 months ago

0.7.0

5 months ago

0.5.8

6 months ago

0.5.7

6 months ago

0.6.2

5 months ago

0.5.6

6 months ago

0.6.1

5 months ago

0.6.0

5 months ago

0.5.5

6 months ago

0.5.4

6 months ago

0.5.3

6 months ago

0.5.2

6 months ago

0.5.1

6 months ago

0.5.0

7 months ago

0.4.7

7 months ago

0.4.6

7 months ago

0.4.5

8 months ago

0.4.4

8 months ago

0.4.3

8 months ago

0.4.2

8 months ago

0.4.1

8 months ago

0.4.0

8 months ago

0.3.1

8 months ago

0.3.0

8 months ago

0.2.5

8 months ago

0.2.4

8 months ago

0.2.3

8 months ago

0.2.2

9 months ago

0.2.1

9 months ago

0.2.0

9 months ago

0.1.9

9 months ago

0.1.8

9 months ago

0.1.7

9 months ago

0.1.6

9 months ago

0.1.5

9 months ago

0.1.4

9 months ago

0.1.3

9 months ago

0.1.2

10 months ago

0.1.1

10 months ago

0.1.0

10 months ago