@enfometa/em-forms

A simple, robust and flexible react forms validation library for both React Js and React Native. The great thing about the library is, it has no dependency on the UI. You can easily decouple your business logic from the UI and can reuse in both React Js and React Native apps.

Introduction

A simple, robust and flexible react forms validation library for both React Js and React Native. The great thing about the library is, it has no dependency on the UI. You can easily decouple your business logic from the UI and can reuse in both React Js and React Native apps.

Platforms

  • React Js
  • React Native

Installation

npm install @enfometa/em-forms

Usage

Create forms object

import { useEmForms, required, email } from "@enfometa/em-forms";

const forms = useEmForms({
  forms: [
    {
      name: "username",
      value: "",
      validators: [
        { name: "required", func: required, message: "Username is required" },
        { name: "email", func: email, message: "Invalid email address" },
      ],
    },
    {
      name: "password",
      value: "",
      validators: [{ name: "required", func: required, message: "Password is required" }],
    },
  ],
});

const login = () => {
  if (forms.validate()) {
    //do something
  }
};

UI implementation

import { EmFormGroup, EmFormErrorMessage, EmFormControl } from "@enfometa/em-forms";

<EmFormGroup emForms={forms}>
  <div>
    <EmFormControl formName="username">
      <input type="email" className="form-control" placeholder="Email" />
    </EmFormControl>

    <div className="error-message">
      <EmFormErrorMessage formName="username" validatorName="required" />
      <EmFormErrorMessage formName="username" validatorName="email" />
    </div>
  </div>
  <div>
    <EmFormControl formName="password">
      <input type="password" className="form-control" placeholder="password" />
    </EmFormControl>

    <div className="error-message">
      <EmFormErrorMessage formName="password" validatorName="required" />
    </div>
  </div>
  <button className="w-100 btn btn-primary btn-lg" type="button" onClick={login}>
    Login
  </button>
</EmFormGroup>;

Initializing EmFormsCore

EmFormsCore This is core of the em-forms. This module defines all the basic functionality for the forms validation. This object can be initialized in two ways. These both ways accepts the first argument as required forms object array.

  • Functional component: use useEmForms hook in functional components to initilize this object. In below example forms is an instance of the EmFormsCore. This hook will properly initialize object and define state handler.
import { useEmForms, required, email } from "@enfometa/em-forms";

const forms = useEmForms({
  forms: [
    {
      name: "username",
      value: "",
      validators: [
        { name: "required", func: required, message: "Username is required" },
        { name: "email", func: email, message: "Invalid email address" },
      ],
    },
    {
      name: "password",
      value: "",
      validators: [{ name: "required", func: required, message: "Password is required" }],
    },
  ],
  onChange: (formName, value) => {
    //on change
  },
});
  • Class component: use initEmForms function in class components to initilize this object. In below example this.forms is an instance of the EmFormsCore. This function will properly initialize object and define state handler.
import { initEmForms, required, email } from "@enfometa/em-forms";

this.forms = initEmForms(
  {
    forms: [
      {
        name: "username",
        value: "",
        validators: [
          { name: "required", func: required, message: "Username is required" },
          { name: "email", func: email, message: "Invalid email address" },
        ],
      },
      {
        name: "password",
        value: "",
        validators: [{ name: "required", func: required, message: "Password is required" }],
      },
    ],
  },
  this,
  "forms"
);

Note: For detials of EmFormsCore, please see section EmFormsCore Methods

Form

A form consists of a user-defined name, initial value (value), validators array, optional onChange event and optiona valueConverter function.

{
    name: "username",
    value: "",
    validators: [
        { name: "required", func: required, message: "Username is required" },
        { name: "email", func: email, message: "Invalid email address" },
    ],
    onChange: (value) => {
      //on change
    }
}

valueConverter :

This function is used to convert the value comming from input or any source before assigning to the form field. For example if you want to convert string to date:

{
    name: "username",
    value: "",
    validators: [
        { name: "required", func: required, message: "Username is required" },
        { name: "email", func: email, message: "Invalid email address" },
    ],
    valueConverter: (value) => {
      return typeOf(value) == 'string' : new Date(value) : value;
    }
}

Validators

Each form has an array of validators. So we can have multiple validators for a form as given below.

{
    name: "username",
    value: "",
    validators: [
        { name: "required", func: required, message: "Username is required" },
        { name: "email", func: email, message: "Invalid email address" },
    ],
}

name: User-defined name to identify a validator
func: A validator function, which is called when validation is required. It is described in details in below section
message: An error message associated with the validator
param: An optional parameter for passing some custom information like in case of min length, we can pass length as given below

{
    name: "password",
    value: "",
    validators: [
        { name: "required", func: required, message: "Password is required" },
        { name: "minLength", func: minLength, message: "min Length required is 6", param: { minLength: 6 } },
    ],
}

Validator functions

em-forms has provided couple of built in validators to make developers life easy. Developers can define their own custom validators as well. Each validator function returns boolean and accept an object of type {form: EmFormControl, emForm: EmFormsObj, param: any}

List of built in validators

Below is the list of built in validators with usage and examples

required:

{ name: "required", func: required, message: "Password is required" }

required also accepts an optional parameter param: {acceptsWhiteSpace: true} to specify whether form should accept white spaces.

example: { name: "required", func: required, message: "Password is required", param: {acceptsWhiteSpace: true} }

number

{ name: "number", func: number, message: "Please provide a valid number" }

Validates whether the input is a valid number

pattern

{ name: "pattern", func: pattern, message: "Invalid number", param: { pattern: '^[0-9]+$'} }

The param object has a pattern key in which the desired regular expression is provided.

minLength

{ name: "minLength", func: minLength, message: "min Length required is 6", param: { minLength: 6 } }

maxLength

{ name: "maxLength", func: maxLength, message: "Max Length required is 6", param: { maxLength: 6 } }

The param object has a pattern key in which the desired regular expression is provided.

email

{ name: "email", func: email, message: "Invalid email" }

range

{ name: "age", func: range, message: "Age should be betweeb 18 and 50", param: {min: 18, max: 50} }

requiredIf

{ name: "requiredIf", func: requiredIf, message: "Password is required", param: { name: 'terms_conditions', value: true } }

requiredIf also accepts an optional parameter param: {acceptsWhiteSpace: true} to specify whether form should accept white spaces.

example: { name: "requiredIf", func: requiredIf, message: "Password is required", param: { name: 'terms_conditions', value: true, acceptsWhiteSpace: true } }

The param has two required keys name, name of the other form which value is equal to value. For example this form is requird when terms_conditions form is checked (true)

compare

{ name: "compare", func: compare, message: "Please confirm password", param: { compareTo: 'confrimPassword' } }

The param object has a compareTo key in which we provide other form name which value is to be compared. For example on the sign in page re type password will use this validator.

Custom validators

To define a custom validator we need to define a function which returns boolean and accepts below given parameters. These parameters are passed when validator function is executed.

formControl: Object of this form emForms: Object of the whole forms configuration param: Custom param object

Examples:

Compare validator can be defined in this way and can be used as given in above section "Validator functions"

function compare(formControl, emForms, param) {
  let isValid = true;
  let formCompareValue = emForms.getFormValue(param.compareTo);
  if (!(formControl.value === undefined || formControl.value === null || formControl.value === "")) {
    if (formCompareValue !== formControl.value) {
      isValid = false;
    }
  }
  return isValid;
}

A Max length validator can be defined as given below.

function maxLength(formControl, emForms, param) {
  let isValid = true;
  if (formControl.value !== undefined && formControl.value !== "" && formControl.value !== null) {
    if (param !== undefined && param.maxLength !== undefined) {
      let valLength = formControl.value.toString().length;
      if (valLength > param.maxLength) {
        isValid = false;
      }
    }
  }
  return isValid;
}

Similarly, you can define your own custom validators

Components

em-forms exposes a couple of important react components which can be used in both React Js and React Native.

EmFormControl

Wraps an input field on which we want to apply the forms.

Example:

<EmFormControl emForms={forms} formName="username">
  <input type="email" className="form-control" placeholder="Email" />
</EmFormControl>

Props

emForms : Object, the form object return by useEmForms or initEmForms
formName : String, name of the form
bindValue : Boolean (Default: true), it specifies whether value of the wrapped input field should be bound or not. If true auto bound from the forms, otherwise user is responsible for binding value
valuePropName: String (Default: "value"), It is the name of the attribute of the wrapped elemen to which value will be bount. For example for input type text "value" is passed which in case of checkbox it may be "checked"
onChangePropName: String (Default: "onChange"), this is the attribute name for the onChange event.
valueFunc: Function (Default (e) => { return e; }), this function specifies how to get the value from the event argument e on the onChange event. For example in case of React Js, we can get the value of the form in onChange event as e.target.value. So for React Js we will pas this as:

<EmFormControl emForms={forms} formName="username" valueFunc={(e) => { return e.target.value; })}>
  <input type="email" />
</EmFormControl>

valueConverter: This function specifies how to convert value before assinging/passing to the input control. It accepts a value and return converted value.

Note: Don't worry folks, we are here to make your life easy. We have provided a global configuration which will be discussed later to avoid passing these above props. (Thanks to enfometa)

EmFormError

This will render the children when the form has an error

<EmFormError emForms={forms} formName="username" validatorName="required">
  <span>Password is required</span>
</EmFormError>
<EmFormError emForms={forms} formName="username" validatorName="email">
  <span>Password is required</span>
</EmFormError>

Children of EmFormError should be a valid react js or react native element. You can get the error message from the forms object as well and shown along with the other custom elements like:

<EmFormError emForms={forms} formName="username" validatorName="required">
  <span>{froms.getFormErrorMessage("username", "required")}</span>
</EmFormError>
<EmFormError emForms={forms} formName="username" validatorName="email">
  <span>{froms.getFormErrorMessage("username", "email")}</span>
</EmFormError>

EmFormErrorMessage

This will render a string for the error message. So you need to wrap this control in valid react js or react native control like:

<div className="error-message">
  <EmFormErrorMessage emForms={forms} formName="username" validatorName="required" />
  <EmFormErrorMessage emForms={forms} formName="username" validatorName="email" />
</div>

This component only renders text messsage so if you are using this in react native it should be wrapped in component lik <Text>

<Text>
  <EmFormErrorMessage emForms={forms} formName="username" validatorName="required" />
  <EmFormErrorMessage emForms={forms} formName="username" validatorName="email" />
</Text>

EmFormGroup

Groups EmForms, EmFormControl, EmFormErrorMessage into a single froms group and pass down the emForms prop to all childrens. So instead of specifying emForms on each and every indivisual control, specify this prop on the EmFormGroup as given below.

<EmFormGroup emForms={forms}>
  <EmFormControl formName="username">
    <input type="email" />
  </EmFormControl>
  <div className="error-message">
    <EmFormErrorMessage formName="username" validatorName="required" />
  </div>
  <EmFormControl formName="password">
    <input type="password" />
  </EmFormControl>
  <div className="error-message">
    <EmFormErrorMessage formName="password" validatorName="required" />
  </div>
</EmFormGroup>

Global configuration

enfometa em-forms exposes a global configuration object to configure a few most commonly used functionality globally based on your application type. As em-forms is developed in such a way that it can work with both React Js & React Native. So, we have a few simple configuration user need to apply once to avoid repeating that code on each and every control as much as possible.

This global object has the following properties and signature with default values.

    valueFunc: (e: UIEvent) => void;
    bindValue: boolean = true;
    valuePropName: string = "value";
    onChangePropName: string = "onChange";
    valueConverter: (any) => any = null

These above has already been described in the section for EmFormControl Props

The control register defines each control and its nature to the em-forms once and you can go with neat and clean code for the rest of the application. For example, the first object in the array passed to the registerEmFormControls function, states that for control type: "input", should use configuration:

    valuePropName: "value",
    onChangePropName: "onChange",
    valueFunc: (e) => e.target.value,

Similary, for control type: "select"

    valuePropName: "value",
    onChangePropName: "onChange",
    valueFunc: (e) => e.target.value,

For checkbox, this is a little bit different, as usually developer want to bind the value with the checked property instead of the value. Also on the onChange event, value is extracted from the event argument as e.target.checked instead of e.target.value so it can be configured as:

{
    valuePropName: "checked",
    onChangePropName: "onChange",
    valueFunc: (e) => e.target.checked,
    controls: [
      {
        type: "input",
        props: {type: "checkbox"},
      },
    ],
  },

controls property can have multiple controls, for example as select and input has the same configuration, so we can combine this as single configuration object:

  {
    valuePropName: "value",
    onChangePropName: "onChange",
    valueFunc: (e) => e.target.value,
    controls: [{ type: "input" }, { type: "select" }],
  },

props further specifies an control by props, in case of checkbox we have further filtered by prop/attribute value.

This library is made flexible to configure custom components as well. So you can use this in the same way as for built in controls and components. Below configuraiton specifies a custom component:

{
    valuePropName: "selectedValue",
    onChangePropName: "onChange",
    valueFunc: (e) => e.target.value,
    controls: [{ type: "RadioGroupComponent" }],
  },

You can configure this object in App.js or Index.js file as given below:

For React Js

//App.js
import { emFormsGlobalConfig } from "@enfometa/em-forms";

emFormsGlobalConfig.registerEmFormControls([
  {
    valuePropName: "value",
    onChangePropName: "onChange",
    valueFunc: (e) => e.target.value,
    controls: [{ type: "input" }],
  },
  {
    valuePropName: "value",
    onChangePropName: "onChange",
    valueFunc: (e) => e.target.value,
    controls: [{ type: "select" }],
  },
  {
    valuePropName: "checked",
    onChangePropName: "onChange",
    valueFunc: (e) => e.target.checked,
    controls: [
      {
        type: "input",
        props: { type: "checkbox" },
      },
    ],
  },
  {
    valuePropName: "selectedValue",
    onChangePropName: "onChange",
    valueFunc: (e) => e.target.value,
    controls: [{ type: "RadioGroupComponent" }],
    valueConverter : (value) => parseInt(value)
  },
]);

For React Native

import { emFormsGlobalConfig } from "@enfometa/em-forms";
//if you are using 'onChange'
emFormsGlobalConfig.emFormValueFunc = (e) => e.nativeEvent.text;

//if you are using 'onChangeText'
emFormsGlobalConfig.emFormValueFunc = (e) => e;

In short this configuration tells the library how to extract value from the onChange event handler

EmFormsCore methods

Below is the list of public methods of the EmFormsCore object returned by the initializer

  • addForm(formObject: EmFormControl): void
    Adds a new form dynamically
  • isValid(): boolean
    Returns form status as true or false, it does not fire UI triggers
  • isValidForm(formName: string): boolean
    Checks an indivisual form
  • isValidFormValidator(formName: string, validatorName: string): boolean
    Checks an indivisual form validator, as form can have multiple validators
  • getFormErrors(formName: string): FormError[]
    Returns form error object array if has errors
  • getFormError(formName: string, validatorName: string): FormError | null
    Returns form error object for a validator
  • getFormErrorMessage(formName: string, validatorName: string = null): string | null
    Returns form error message in string
  • getErrors(): FormError[]
    Get all errors, if you want to show a summary of all errors this method is helpful
  • validate(): boolean
    Validates all forms and returns true or false. It triggers UI events
  • validateForm(formName: string): boolean
    It validates indivisual form
  • resetForm(formName: string, value: any): void
    Resets indivisual form with the provided value or set default value
  • reset(values?: ResetConfig[] | null, excludeForms?: ResetConfig | null): void
    Reset all forms with the values configuration if not provided, set to defaults, excludes excludeForms. By default if values is not provided, all the values are set to defaults when object was initialized.
  • setFormValue(formName: string, value: any): void
    Sets a form value
  • setFormTouch(formName: string, touched: boolean): void
    Sets a form touch, if form touch is set to true, error message becomes visible. Touch identifies whether form has ever focused
  • setTouch(touched: boolean): void
    set all forms touch to true or false
  • getForm(formName: string): EmFormControl
    Gets a form object by form name
  • getFormValue(formName: string): any
    Gets a form value
  • getFormTouch(formName: string): boolean
    Gets a form touch true or false
  • toModel(): any
    Converts em-forms configuration to a model like given above example section will return object like this. For example, when you want to send json object to server, you will use this method to convert em-forms object to json.
    {
      username: "enfometa",
      password: "abc123"
    }
    
  • toArray(): KeyValue[] Converts em-forms to array of key value objects, for example:
    [
     {username : "enfometa"},
     {password : "abc123"}
    ]
    
  • setValuesFromModel(obj: any, setDefaults: boolean = true): void;
    This will set values from model. like above json values can be set to the em-forms object. For example when you get an object from server in json, you can use this method to set values from json object to em-forms. setDefaults parameter specifies whether these values should set to defaults or not.
  • setValues(values: KeyValue[], setDefaults: boolean = true): void;
    Accepts an array for KeyValue and sets to the em-froms object. setDefaults parameter specifies whether these values should set to defaults or not.
  • setModel(model: any, allowAddProps: boolean = false): void
    This method will set and associate a model with the em-forms object. When a form is updated, the property of that model object is updated, based on allowAddProps parameter. This parameter speceifes whether new properties shoulb be added if does not exist in the model object.

Declarations

Below is the declarations for the em-froms modules

interface EmFormConfigControl {
    type: string;
    props: any;
  }
  interface EmFormConfig {
    valueFunc: (e: UIEvent) => void;
    bindValue: boolean;
    valuePropName: string;
    onChangePropName: string;
    controls: EmFormConfigControl[];
    valueConverter: (value : any) => any
  }

  interface EmFormsGlobalConfig {
    emFormConfig: EmFormConfig;
    registerEmFormControls(configArray: EmFormConfig[]): void;
    getEmFormControlsRegister(): EmFormConfig[];
    setEmFormGlobalConfig(defaultConfig: EmFormConfig): void;
  }

  export const emFormsGlobalConfig: EmFormsGlobalConfig;

  interface Validator {
    name: string;
    func: (form: EmFormControl, emForms: EmFormsObj, param: any) => boolean;
    message: string;
    param?: any;
  }

  interface EmFormControl {
    name: string;
    value: any;
    validators: Validator[];
    onChange?: (value: any) => void;
    valueConverter: (value : any) => any
  }

  interface KeyValue {
    name: string;
    value: any;
  }

  interface EmFormsObj {
    forms: EmFormControl[];
    onChange?: (formName: string, value: any) => void;
    handleStateUpdate: () => void;
    config?: EmFormsConfig;
  }

  interface FormError {
    validatorName: string;
    message: string;
  }

  interface ResetConfig {
    name: string;
    value: any;
  }

  interface EmFormsTriggersCofig {
    touch: boolean;
    change: boolean;
  }

  interface EmFormsConfig {
    errorMessageTriggers: EmFormsTriggersCofig;
  }

  interface EmFormGroupProps {
    emForms: EmFormsObj;
  }

  interface EmFormProps extends EmFormGroupProps {
    formName: string;
    bindValue: boolean;
    valuePropName: string;
    onChangePropName: string;
    valueFunc: () => any;
    valueConverter: (value : any) => any
  }

  interface EmFormErrorProps extends EmFormGroupProps {
    formName: string;
    validatorName: string;
  }

  interface EmFormsCore {
    new (emForms: EmFormsObj);
    addForm(formControl: EmFormControl): void;
    isValid(): boolean;
    isValidForm(formName: string): boolean;
    isValidFormValidator(formName: string, validatorName: string): boolean;
    getFormErrors(formName: string): FormError[];
    getFormError(formName: string, validatorName: string): FormError | null;
    getFormErrorMessage(formName: string, validatorName: string): string | null;
    getErrors(): FormError[];
    validate(): boolean;
    validateForm(formName: string): boolean;
    resetForm(formName: string, value: any): void;
    reset(values?: ResetConfig[] | null, excludeForms?: ResetConfig | null): void;
    setFormValue(formName: string, value: any): void;
    setFormTouch(formName: string, touched: boolean): void;
    setTouch(touched: boolean): void;
    getForm(formName: string): EmFormControl;
    getFormValue(formName: string): any;
    getFormTouch(formName: string): boolean;
    toModel(): any;
    setValuesFromModel(obj: any, setDefaults: boolean): void;
    setValues(values: KeyValue[], setDefaults: boolean): void;
    setModel(model: any, allowAddProps: boolean): void;
  }

  export function useEmForms(emForms: EmFormsObj): EmFormsCore;
  export function initEmForms(emForms: EmFormsObj, component: React.Component, stateKey: string): EmFormsCore;

  export function EmFormGroup(props: EmFormGroupProps): React.FC;
  export function EmFormControl(props: EmFormProps): React.FC;
  export function EmFormError(props: EmFormErrorProps): React.FC;
  export function EmFormErrorMessage(props: EmFormErrorProps): React.FC;

  export function required(formControl: EmFormControl, emForms: EmFormsObj, param: any): boolean;
  export function maxLength(formControl: EmFormControl, emForms: EmFormsObj, param: any): boolean;
  export function minLength(formControl: EmFormControl, emForms: EmFormsObj, param: any): boolean;
  export function pattern(formControl: EmFormControl, emForms: EmFormsObj, param: any): boolean;
  export function email(formControl: EmFormControl, emForms: EmFormsObj, param: any): boolean;
  export function requiredIf(formControl: EmFormControl, emForms: EmFormsObj, param: any): boolean;
  export function compare(formControl: EmFormControl, emForms: EmFormsObj, param: any): boolean;
  export function range(formControl: EmFormControl, emForms: EmFormsObj, param: any): boolean;
  export function number(formControl: EmFormControl, emForms: EmFormsObj, param: any): boolean;