20

20

+

Challenges faced with form

  • Maintaining state of form
  • Field normalization (8234-2039-1022-3432)
  • Developing Dynamic forms
  • Component Reusability
  • Complex form validation logic
  • Code readability, refactoring, maintenance, debugging
  • Redundant use of event handlers such onClick(), onChange(), onBlur(), etc.

Redux Form

class Form extends Component {
  constructor(props) {
    super(props);
    this.state = {
      // local state of your form
    }

  }

  render() {

    return (
      // render your form elements here
    )
  }
}
import { Field } from 'redux-form';

<Field
  name="firstName"
  label="First Name"
  placeholder="John"
  component={TextInput}
  testingId="firstNameError"
  type="string"
/>
const MyOwnComponent = ({
  input,
  meta,
  label,
  placeholder,
  onClick,
  testingId,
}) => {
  return (
    <FormGroup>
      <Label>
        {label}
      </Label>

      {
        meta.asyncValidating &&
        <Spinner customClass="async-spinner" solid />
      }

      <Input
        placeholder={placeholder}
        valid={meta.valid}
        invalid={meta.error && meta.touched}
        onClick={onClick}
        {...input}
      />

      <FormFeedback type="invalid" data-testid={testingId}>
        {meta.error}
      </FormFeedback>
    </FormGroup>
  )
}
// meta prop is passed down by redux form
// to the component, which has information
// about state of the field that redux-form
// is tracking for you

{
  "visited": true,
  "touched": true,
  "active": false,
  "asyncValidating": false,
  "autofilled": false,
  "dirty": true,
  "error": "This email is already registered",
  "form": "Form",
  "invalid": true,
  "pristine": false,
  "submitting": false,
  "submitFailed": false,
  "valid": false
}
// instead of exporting component directly
// we export it after wrapping it with
// HOC provided by Redux Form

const ReduxFormWithValidateJs = reduxForm({
  form: 'Redux Form With ValidateJs',

  destroyOnUnmount: false,

  validate: validationFunction,

  asyncValidate: asyncValidationFunction,

  asyncBlurFields: ['email','userName'],
})(Form);

export default ReduxFormWithValidateJs;

Challenges solved by Redux Form

  • Maintaining state of form
  • Component Reusability
  • Redundant use of event handlers such
    as onClick(), onChange(), onBlur(), etc.
const validatedFormFieldObject = (value, isValid, errorMessage, touched = true) => {
    return {
        value,
        touched,
        isValid,
        errorMessage,
    }
}

export const isEmpty = (fieldName, fieldValue) => {
    let formattedFieldValue = fieldValue;
    if (formattedFieldValue.length === 0) {
        return validatedFormFieldObject(formattedFieldValue, false, `${fieldName} cannot be empty`);
    }
    if (fieldName === 'First name' || fieldName === 'Last name') {
        formattedFieldValue = fieldValue[0].toUpperCase() + fieldValue.slice(1);
    }

    return validatedFormFieldObject(formattedFieldValue, true, '');
}

const isAlphabetic = (value) => {
    const regexp = /^[A-Za-z ]+$/;

    return regexp.test(value);
}

export const isFirstNameValid = (firstName) => {
    const errorObject = isEmpty('First name', firstName);
    if (errorObject.isValid) {
        if (!isAlphabetic(firstName)) {
            return validatedFormFieldObject(firstName[0].toUpperCase() + firstName.slice(1), false, 'Numbers 0-9 and special characters such as _ ! @ # % ^ are not allowed');
        }
    }

    return errorObject;
}

export const isLastNameValid = (lastName, firstName) => {
    const errorObject = isEmpty('Last name', lastName);
    if (errorObject.isValid) {
        if (!isAlphabetic(lastName)) {
            return validatedFormFieldObject(lastName[0].toUpperCase() + lastName.slice(1), false, 'Numbers 0-9 and special characters such as _ ! @ # % ^ not allowed');
        }
        if (firstName.toLowerCase() === lastName.toLowerCase()) {
            return validatedFormFieldObject(lastName[0].toUpperCase() + lastName.slice(1), false, 'Last name cannot be same as first name');
        }
    }

    return errorObject;
}

export const isEmailValid = (email) => {
    const errorObject = isEmpty('Email', email);
    if (errorObject.isValid) {
        const regexp = /\S+@\S+\.\S+/;
        if (!regexp.test(email)) {
            return validatedFormFieldObject(email, false, 'Invalid email address');
        }
    }

    return errorObject;
}

export const isPhoneNumberValid = (phoneNumber) => {
    const errorObject = isEmpty('Phone Number', phoneNumber);
    if (errorObject.isValid) {
        if (phoneNumber.length !== 10) {
            return validatedFormFieldObject(phoneNumber, false, 'Phone number too short')
        }
    }

    return errorObject;
}

export const updateFormField = (value, state, fieldName) => {
    if (fieldName === 'phoneNumber') {
        if ((Number(value) && value.length <= 10 && value[0] !== ' ' && value[value.length-1] !== ' ') || value === '');
        else {
            return state;
        }
    }

    return validatedFormFieldObject(value, state.isValid, '', false);
}
const validationFunction = values => {
  const schema = {
    firstName: {
      presence: {
        message: '^Required',
      },
      format: {
        pattern: '[A-Za-z]+',
        flags: 'i',
        message: 'cannot have numbers 0-9 and
        special characters such as _ ! @ # % ^',
      },
    },

    lastName: {
      presence: {
        message: '^Required',
      },
      format: {
        pattern: '[A-Za-z]+',
        flags: 'i',
        message: 'cannot have numbers 0-9 and
        special characters such as _ ! @ # % ^',
      },
      equality: {
        attribute: 'firstName',
        message: 'cannot be same as first name',
        comparator(lastName, firstName) {
          if (firstName) {
            return lastName.toLowerCase() !==
              firstName.toLowerCase();
          }

          return false;
        },
      },
    },
const validatedFormFieldObject = (value, isValid, errorMessage, touched = true) => {
    return {
        value,
        touched,
        isValid,
        errorMessage,
    }
}

export const isEmpty = (fieldName, fieldValue) => {
    let formattedFieldValue = fieldValue;
    if (formattedFieldValue.length === 0) {
        return validatedFormFieldObject(formattedFieldValue, false, `${fieldName} cannot be empty`);
    }
    if (fieldName === 'First name' || fieldName === 'Last name') {
        formattedFieldValue = fieldValue[0].toUpperCase() + fieldValue.slice(1);
    }

    return validatedFormFieldObject(formattedFieldValue, true, '');
}

const isAlphabetic = (value) => {
    const regexp = /^[A-Za-z ]+$/;

    return regexp.test(value);
}

export const isFirstNameValid = (firstName) => {
    const errorObject = isEmpty('First name', firstName);
    if (errorObject.isValid) {
        if (!isAlphabetic(firstName)) {
            return validatedFormFieldObject(firstName[0].toUpperCase() + firstName.slice(1), false, 'Numbers 0-9 and special characters such as _ ! @ # % ^ are not allowed');
        }
    }

    return errorObject;
}

export const isLastNameValid = (lastName, firstName) => {
    const errorObject = isEmpty('Last name', lastName);
    if (errorObject.isValid) {
        if (!isAlphabetic(lastName)) {
            return validatedFormFieldObject(lastName[0].toUpperCase() + lastName.slice(1), false, 'Numbers 0-9 and special characters such as _ ! @ # % ^ not allowed');
        }
        if (firstName.toLowerCase() === lastName.toLowerCase()) {
            return validatedFormFieldObject(lastName[0].toUpperCase() + lastName.slice(1), false, 'Last name cannot be same as first name');
        }
    }

    return errorObject;
}

export const isEmailValid = (email) => {
    const errorObject = isEmpty('Email', email);
    if (errorObject.isValid) {
        const regexp = /\S+@\S+\.\S+/;
        if (!regexp.test(email)) {
            return validatedFormFieldObject(email, false, 'Invalid email address');
        }
    }

    return errorObject;
}

export const isPhoneNumberValid = (phoneNumber) => {
    const errorObject = isEmpty('Phone Number', phoneNumber);
    if (errorObject.isValid) {
        if (phoneNumber.length !== 10) {
            return validatedFormFieldObject(phoneNumber, false, 'Phone number too short')
        }
    }

    return errorObject;
}

export const updateFormField = (value, state, fieldName) => {
    if (fieldName === 'phoneNumber') {
        if ((Number(value) && value.length <= 10 && value[0] !== ' ' && value[value.length-1] !== ' ') || value === '');
        else {
            return state;
        }
    }

    return validatedFormFieldObject(value, state.isValid, '', false);
}
    email: {
      presence: {
        message: '^Required',
      },
      email: {
        message: '^Invalid format',
      },
    },

    phoneNumber: {
      presence: {
        message: '^Required',
      },
      length: {
        is: 11,
        message: 'too short',
      },
    },
  }

  const validationErrors = validate(values, schema);

  if (validationErrors) {
    return mapFormErrors(validationErrors);
  }

  return {};
}
// instead of exporting component directly
// we export it after wrapping it with
// HOC provided by Redux Form

const ReduxFormWithValidateJs = reduxForm({
  form: 'Redux Form With ValidateJs',

  destroyOnUnmount: false,

  validate: validationFunction,

  asyncValidate: asyncValidationFunction,

  asyncBlurFields: ['email','userName'],
})(Form);

export default ReduxFormWithValidateJs;

Challenges solved by Validate JS

  • Handling Complex form validation logic
  • Code has become more readable now, making it quite easy to maintain.
  • Refactoring and debugging will be easy as the code is now divided into segments.

Handling Complex Forms

const validateForm = values => {
  const schema = {
    firstName: {
      presence: {
        message: '^Required',
      },
      format: {
        pattern: '[A-Za-z]+',
        flags: 'i',
        message: 'cannot have numbers 0-9 and special
        characters such as _ ! @ # % ^',
      },
    },

    lastName: {
      presence: {
        message: '^Required',
      },
      format: {
        pattern: '[A-Za-z]+',
        flags: 'i',
        message: 'cannot have numbers 0-9 and special
        characters such as _ ! @ # % ^',
      },
      equality: {
        attribute: 'firstName',
        message: 'cannot be same as first name',
        comparator(lastName, firstName) {
          if (firstName) {
            return lastName.toLowerCase() !== firstName.toLowerCase();
          }

          return false;
        },
      },
    },

    dateOfBirth: {
      presence: {
        message: '^Required',
      },
      length: {
        is: 10,
        message: '^Invalid date',
      },
    },

    gender: {
      presence: {
        message: '^Required',
      },
    },

    email: {
      presence: {
        message: '^Required',
      },
      email: {
        message: '^Invalid format',
      },
    },

    confirmEmail: {
      presence: {
        message: '^Required',
      },
      email: {
        message: '^Invalid format',
      },
      equality: {
        attribute: 'email',
        message: '^Email doesn\'t match',
      },
    },

    phoneNumber: {
      presence: {
        message: '^Required',
      },
      length: {
        is: 11,
        message: 'too short',
      },
    },

    alternatePhoneNumber: {
      length: {
        is: 11,
        message: 'too short',
      },
      equality: {
        attribute: 'phoneNumber',
        message: '^Cannot be same as primary phone number',
        comparator(alternatePhone, primaryPhone) {
          return alternatePhone !== primaryPhone;
        },
      },
    },

    password: {
      presence: {
        message: '^Required',
      },
      length: {
        minimum: 6,
        message: '^Minimum length should 6',
      },
    },

    confirmPassword: {
      presence: {
        message: '^Required',
      },
      equality: {
        attribute: 'password',
        message: '^Password doesn\'t match',
      },
    },

    address: {
      presence: {
        message: '^Required',
      },
    },

    city: {
      presence: {
        message: '^Required',
      },
    },

    state: {
      presence: {
        message: '^Required',
      },
    },

    zipCode: {
      presence: {
        message: '^Required',
      },
      length: {
        is: 7,
        message: 'too short',
      },
    },
  }

  const validationErrors = validate(values, schema);

  if (validationErrors) {
    return mapFormErrors(validationErrors);
  }

  return {};
}

export default validateForm;
    alternatePhoneNumber: {
      length: {
        is: 11,
        message: 'too short',
      },
      equality: {
        attribute: 'phoneNumber',
        message: '^Cannot be same as primary phone number',
        comparator(alternatePhone, primaryPhone) {
          return alternatePhone !== primaryPhone;
        },
      },
    },

    password: {
      presence: {
        message: '^Required',
      },
      length: {
        minimum: 6,
        message: '^Minimum length should 6',
      },
    },

    confirmPassword: {
      presence: {
        message: '^Required',
      },
      equality: {
        attribute: 'password',
        message: '^Password doesn\'t match',
      },
    },

    address: {
      presence: {
        message: '^Required',
      },
    },

    city: {
      presence: {
        message: '^Required',
      },
    },

    state: {
      presence: {
        message: '^Required',
      },
    },

    zipCode: {
      presence: {
        message: '^Required',
      },
      length: {
        is: 7,
        message: 'too short',
      },
    },
  }

  const validationErrors = validate(values, schema);

  if (validationErrors) {
    return mapFormErrors(validationErrors);
  }

  return {};
}

export default validateForm;
{
  "email": "john@appleseed.com",
  "firstName": "John",
  "lastName": "Appleseed",
  "dateOfBirth": "26/09/1974",
  "gender": "male",
  "confirmEmail": "john@appleseed.com",
  "city": "Bengaluru",
  "state": "Karnataka",
  "zipCode": "123-456",
  "phoneNumber": "99999-99999",
  "password": "password",
  "confirmPassword": "password",
  "address": "#123 Block A",
  "alternatePhoneNumber": "88888-88888",
}
import validate from 'validate.js';

const validationFunction = values => {
  const schema = {
    firstName: {
      presence: {
        message: '^Required',
      },

      format: {
        pattern: '[A-Za-z]+',
        message: 'cannot have numbers 0-9 and special characters',
      },

      equality: {
        attribute: 'lastName',
        message: 'cannot be same as last name',
      },
    },

    confirmEmail: {
      presence: {
        message: '^Required',
      },

      email: {
        message: '^Invalid format',
      },

      equality: {
        attribute: 'alternateEmail',
        message: '^Email doesn\'t match',
        comparator(alternateEmail, email) {
          if (alternateEmail) {
            return alternateEmail.toLowerCase()
            !== email.toLowerCase();
          }

          return false;
        },
      },
    },

    password: {
      presence: {
        message: '^Required',
      },

      length: {
        minimum: 6,
        // is: 6,
        message: '^Minimum length should 6',
      },
    },
  }

  const validationErrors = validate(values, schema);

  if (validationErrors) {
    return mappedFormErrors(validationErrors);
  }

  return {};
}

Validators Provided By Validate JS

  • Date(Format), Datetime (Too Early, Too Late), Email (Format).
  • Length (Invalid, Too Short, Too Long, Wrong).
  • Equality, Format, Exclusion/Inclusion.
  • Type (Array, Integer, Number, String, Date, Boolean).
  • URL (Message, Schemes, Allow Local, Allow Data Url).
0
 Advanced issue found
 
{
  "firstName": "Required",
  "lastName": "Required",
  "dateOfBirth": "Required",
  "gender": "Required",
  "confirmEmail": "Email doesn't match",
  "alternatePhoneNumber": "Can't be same as primary number",
  "password": "Minimum length should 6",
  "confirmPassword": "Password doesn't match",
  "address": "Required",
  "city": "Required",
  "state": "Required",
  "zipCode": "Required"
}

Form errors generated

by Validate JS

Writing Your Own

Custom Validators

Writing Your Own

Custom Validators

0
 Advanced issue found
 
validate.validators.myValidator = (value) => {

  // test the value against
  // your condition and..
  if (conditionFails) {
    return 'Error message from your own validator'
  }
}
import validate from 'validate.js';

const validationFunction = values => {
  const schema = {
    firstName: {
      presence: {
        message: '^Required',
      },

      format: {
        pattern: 'Regex',
        message: 'Error Message',
      },
      
      myValidator: {},
    },
  }

  const validationErrors = validate(values, schema);

  if (validationErrors) {
    return mappedFormErrors(validationErrors);
  }

  return {};
}

Async (Server-Side)

Validation

validate.validators.emailValidator = async (email) => {
  return new validate.Promise(async (resolve) => {

    // an API which checks if entered
    // email is already registered
    const isEmailRegistered = await emailApi(email);

    if (isEmailRegistered)
      resolve('^This email is already registered');
    else
      resolve();
  });
};

validate.validators.phoneNumberValidator =
  async (phoneNumber) => {
  // follow same paradigm as asbove
  // with the API for phone number
};
const asyncValidationFunction = async (values) => {
  const schema = {
    email: {
      emailValidator: !!values.email,
    },
    phoneNumber: {
      phoneNumberValidator: !!values.phoneNumber,
    },
  };

  const validationErrors = await validate
    .async(values, schema)
    .then(() => '', err => err);

  if (validationErrors) {
    throw mappedFormErrors(validationErrors);
  }
};
// instead of exporting component directly
// we export it after wrapping it with
// HOC provided by Redux Form

const ReduxFormWithValidateJs = reduxForm({
  form: 'Redux Form With ValidateJs',

  destroyOnUnmount: false,

  validate: validationFunction,

  asyncValidate: asyncValidationFunction,

  asyncBlurFields: ['email','userName'],
})(Form);

export default ReduxFormWithValidateJs;

Pros of Validate JS

  • Unit tested with 100% code coverage.
  • No external dependencies required at all.
  • As light as 5.05KB, minified and gzipped
  • Provides a declarative way of validating javascript objects
  • Not tightly coupled to any specific language or framework.
  • Works with any ECMAScript 5.1 runtime which means it works in both the browser and in node.js.
  • All modern browsers are supported (IE9+, Firefox 3+, Opera 10.5+, Safari 4+, Chrome).

Thank You Very Much! </>

vinaysharma14

Validate JS With Redux Form

By Vinay Sharma

Validate JS With Redux Form

  • 555