Skip to content

Instantly share code, notes, and snippets.

@shovon
Created August 21, 2019 01:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save shovon/4ac7487b2346684d8831155bd7b53330 to your computer and use it in GitHub Desktop.
Save shovon/4ac7487b2346684d8831155bd7b53330 to your computer and use it in GitHub Desktop.

Usage

import React from 'react';
import { useValidation, useFormFeedback } from './use-validation';

type InputItemProps = {
  name,
  value
} & FieldProps<string>

const InputItem: FC.React<FieldProps> = ({
  name,
  validation,
  value,
  showError,
  onChange
}) => {
  const {
    onChange: handleInputChange,
    onBlur,

    // TODO: use.
    shouldShowError,
    errorMessages
  } = useFormFeedback<string>({ validation, value, showError });

  const labelStyle = {};
  const inputStyle = {};
  if (shouldShowError) {
    Object.assign(labelStyle, { color: 'red' });
    Object.assign(inputStyle, { borderColor: 'red' });
  }
  return (
    <div>
      <label style={labelStyle} for={name}>First Name</label>
      <input
        style={inputStyle}
        id={name}
        name={name}
        onChange={e => {
          onChange(e.target.value);
          handleInputChange();
        }} />
    </div>
  )
};

const required = (name: string) =>
  requiredCustomMessage(`${name} is required`);

const requiredCustomMessage = (message: string) =>
  (value: string) => (value).trim().length > 0 ? null : [ message ];

function useFormValidation() {
  const config = {
    firstName: { validation: required('First name') },
    lastName: { validation: required('Last name') },
    email: { validation: required('Email') },
  };
  const initialState = {
    firstName: { value: '' },
    lastName: { value: '' },
    email: { value: '' }
  };
  return useValidation(config, initialState);
}

function Form() {
  const { getFormProps, validate } = useValidation()
  return (
    <form onSubmit={e => {
      if (!validate()) {
        e.preventDefault();
      }
    }}>
      <InputItem name='first_name' {...getFormProps('firstName')} />
      <InputItem name='last_name' {...getFormProps('lastName')} />
      <InputItem name='email' {...getFormProps('email')} />
      <input type='submit' name='submit' value='Submit' />
    </form>
  )
}
import { useReducer, useState } from 'react';
/**
* The error of a single field.
*/
export type FieldError = string[] | null
/**
* The function that serves to validate a value.
*/
type ValidationFunction<T> = (value: T) => FieldError
/**
*
*/
type ValidationFunctionWithState<T, K extends T[keyof T]> = (value: K, state: State<T>) => FieldError
/**
* The configuration object used to represent a single field in a form.
*/
type ConfigField<T, P extends T[keyof T]> = {
validation: ValidationFunctionWithState<T, P>
}
/**
* The configuration object for the form validation.
*/
export type Config<T> = {
fields: {
[P in keyof T]: ConfigField<T, T[P]>
}
}
/**
* The field within a single state.
*/
type StateField<T> = {
value: T
}
// Not sure if this is even necessary.
export type State<T> = {
forceShowErrors: boolean
fields: {
[P in keyof T]: StateField<T[P]>
}
}
/**
* The field's props.
*/
export type FieldProps<T> = {
value: T
validation: ValidationFunction<T>
onChange: (v: T) => void
showError: boolean
}
/**
* The object returned by the `useValidation` hook.
*/
type ValidationHook<T> = {
/**
* Gets the fields' props.
* @param key The string key to get the form from.
*/
getFieldProps<K extends keyof T>(key: K): FieldProps<T[K]>
/**
* Triggers a validation.
*/
validate(): void
}
/**
* The payload inside the change action.
*/
type ChangeActionPayload<T, K extends keyof T> = {
key: K
value: T[K]
}
/**
* The change action.
*/
type ChangeAction<T> = {
type: 'CHANGE'
payload: ChangeActionPayload<T, keyof T>
}
/**
* The action to force the showing of errors.
*/
type ForceShowError = {
type: 'FORCE_SHOW_ERROR'
value: boolean
}
/**
* All the possible actions that we can take, on a form.
*/
type FormStateActions<T> = ChangeAction<T> | ForceShowError
/**
* The React reducer function for the form state.
* @param state The current state
* @param action The action to dispatch.
*/
function formStateReducer<T>(state: State<T>, action: FormStateActions<T>): State<T> {
switch (action.type) {
case 'CHANGE': {
const { payload } = action;
return { ...state, fields: { ...state.fields, [payload.key]: { value: payload.value } } }
}
case 'FORCE_SHOW_ERROR': {
return { ...state, forceShowErrors: true }
}
}
console.log(state);
return state;
}
/**
* The function type to represent the form's state reducer.
*/
type FormStateReducer<T> = React.Reducer<State<T>, FormStateActions<T>>;
/**
* Creates the form validation hook.
* @param config The form validation configuration
* @param initialState The initial state
*/
export function useValidation<T>(config: Config<T>, initialState: State<T>): ValidationHook<T> {
const [ state, dispatch ] = useReducer<FormStateReducer<T>>(formStateReducer, initialState);
let subsequentState = { ...state };
return {
getFieldProps: key => {
return ({
showError: subsequentState.forceShowErrors,
value: subsequentState.fields[key].value,
validation: value => {
return config.fields[key].validation(value, subsequentState);
},
onChange: value => {
dispatch({ type: 'CHANGE', payload: { key, value } })
}
})
},
validate: () => {
throw new Error('Not yet implemented');
}
}
}
/**
* The props for the form feedback hook.
*/
export type FormFeedbackProps<T> = {
validation: ValidationFunction<T>
value: T
showError: boolean
}
/**
* A hook for a form's feedback information.
* @param config The props needed for the form feedback.
*/
export function useFormFeedback<T>({ validation, value, showError }: FormFeedbackProps<T>) {
const [ didBlur, setDidBlur ] = useState({ value: false });
const [ didChange, setDidChange ] = useState(false);
const errorMessages = validation(value);
const shouldShowError = (
(didBlur.value && didChange) || showError) &&
(errorMessages !== null && errorMessages.length > 0
);
return {
onChange: () => {
setDidChange(true);
},
onBlur: () => {
setDidBlur({ value: true });
},
shouldShowError,
errorMessages
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment