Kate Herasimenak
Kate Herasimenak

Reputation: 275

How to prevent repeating code of form validation

I created the form for multiple inputs, where the specific input data shall be validated at the time of data entry and once again for all data just before the submission of the form to the backend.
The conditions to submit: all fields are mandatory and the data is valid.

My program works, but I don't like that I'm repeating the validation code in 2 places: in ErrorOutput and hadleSubmit.
In ErrorOutput I check the data and, if necessary, display an error message.
In handleSubmit I just check the data without displaying of error message and if all data is valid, I confirm submitting.

How can I improve my example to prevent the repetition of this code, but the data validation was also at the time of data entry and before submission?

import React from 'react'
import { render } from 'react-dom'

const ErrorOutput = props => {
  let name = props.name
  let inputValue = props.case
  let submit = props.submit
  // Data validation
  if (name === 'firstName') {
    if (!inputValue.match(/^[a-zA-Z]+$/) && inputValue.length > 0) {
        return <span>Letters only</span>
      } else if (submit && inputValue.length === 0) {
        return <span>Required</span>
      }
    return <span></span>
  }
  if (name === 'telNo') {
    if(!inputValue.match(/^[0-9]+$/) && inputValue.length > 0) {
        return <span>Numbers only</span>
      } else if (submit && inputValue.length === 0) {
        return <span>Required</span>
      }
    return <span></span>
  }
}

class App extends React.Component {
  constructor(props){
    super(props)

    this.state = {
      firstName: '',
      telNo: '',
      submit: false
    }
  }

  handleSubmit(e){
    e.preventDefault()
    let submit = true
    let error = true
    const { firstName, telNo } = this.state
    this.setState ({submit: submit})

    // Repeat the data validation before submission
    if (firstName === '' || !firstName.match(/^[a-zA-Z]+$/)) {
      error = true
    } else if (telNo === '' || !telNo.match(/^[0-9]+$/)) {
      error = true
    } else {
      error = false
    }

    // Submited if all data is valid
    if (!error) {
      // send data
      return alert('Success!')
    }
  }

  handleValidation(e) {    
    this.setState({
      [e.target.name]: e.target.value 
    })  
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit.bind(this)}>
        <div>
          <label>
            First name:
          </label>
          <input
            type='text'
            name ='firstName'
            value = {this.state.firstName}
            onChange = {this.handleValidation.bind(this)}
          />
          <ErrorOutput case={this.state.firstName} name={'firstName'} submit = {this.state.submit} />
        </div>
        <div>
          <label>
            Phone number:
          </label>
          <input
            type='tel'
            name ='telNo'
            value = {this.state.telNo}
            onChange = {this.handleValidation.bind(this)}
          />
          <ErrorOutput case={this.state.telNo} name={'telNo'} submit = {this.state.submit} />
        </div>
        <button>
          Submit
        </button> 
      </form>
    )
  }
}

render(
  <App />,
  document.getElementById('root')
)

Upvotes: 2

Views: 747

Answers (2)

Evgeny Timoshenko
Evgeny Timoshenko

Reputation: 3259

You could extract a FormItem component:

class FormItem extends React.Component {
  render() {
    return (
      <div>
        <label>
          {this.props.label}
        </label>
        <input
          {...this.props.input}
        />
        <ErrorOutput 
          case={this.props.input.value} 
          name={this.props.input.name} 
          submit={this.props.onSubmit} 
        />
      </div>
    );
  }
}

and use it in your App:

render() {
    return (
      <form onSubmit={this.handleSubmit.bind(this)}>
        <FormItem label='First name:' input={{
          type: 'text'
            name: 'firstName'
            value: this.state.firstName,
            onChange: this.handleValidation.bind(this)
          }} 
          onSubmit={this.state.submit}
        />
        <FormItem label='Phone number:' input={{
          type:'tel'
            name :'telNo'
            value : {this.state.telNo}
            onChange : {this.handleValidation.bind(this)}
          }} 
          onSubmit={this.state.submit}
        />
        <button>
          Submit
        </button> 
      </form>
    )
  }

this is where libraries like react-final-form and redux-form become handy.

UPD

ErrorOutput component should not validate anything, it is not a responsibility of a component. Instead, you could validate your values on inputs blur event and before submit:

class App extends React.Component {
  constructor(props){
    super(props)

    this.state = {
      firstName: '',
      telNo: '',
      submit: false,
      errors: {},
      invalid: false,
    }
  }

  handleSubmit(e){
    e.preventDefault()
    if (this.validate()) {
      // handle error
    } else {
      // submit
    }
  }

  validate = () => {
    const { firstName, telNo } = this.state
    const errors = {}
    let invalid = false;
    if (firstName === '' || !firstName.match(/^[a-zA-Z]+$/)) {
      errors.firstName = 'first name is required'
      invalid = true;
    } else if (telNo === '' || !telNo.match(/^[0-9]+$/)) {
      telNo.telNo = 'telNo is required'
      invalid = true;
    }
    this.setState({
      invalid,
      errors,
    })
    return invalid;
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit.bind(this)}>
        <FormItem label='First name:' input={{
            type: 'text',
            name: 'firstName',
            value: this.state.firstName,
            onChange: e => this.setState({ firstName: e.target.value }),
            onBlur: () => this.validate(),
          }} 
        />
        <FormItem label='Phone number:' input={{
            type: 'tel',
            name: 'telNo',
            value: this.state.telNo,
            onChange: e => this.setState({ telNo: e.target.value }),
            onBlur: () => this.validate(),
          }} 
        />
        <button>
          Submit
        </button> 
      </form>
    )
  }
}

and FormItem and ErrorOutput:

const ErrorOutput = ({ error }) => <span>{error}</span>

class FormItem extends React.Component {
  render() {
    return (
      <div>
        <label>
          {this.props.label}
        </label>
        <input
          {...this.props.input}
        />
        {this.props.error && <ErrorOutput error={this.props.error} />}
      </div>
    );
  }
}

Upvotes: 2

N. Solomon
N. Solomon

Reputation: 140

const ErrorOutput = ({ errorText }) => <span>{errorText}</span>;

class App extends React.Component {
constructor(props) {
    super(props);

    this.state = {
    firstName: "",
    telNo: "",
    submit: false,
    errors: {} //Add errors object to the state.
    };
}

handleSubmit(e) {
    e.preventDefault();
    const errors = this.validateData();

    if (Object.keys(errors).length === 0) {
       alert("Success");
    }
    //else errors exist
    this.setState({ errors });
}

validateData = () => {
    let errors = {};
    const { firstName, telNo } = this.state; // read the values to validate

    if (firstName.length === 0) {
    errors.firstName = "Required";
    } else if (firstName.length > 0 && !firstName.match(/^[a-zA-Z]+$/)) {
    errors.firstName = "Letters only";
    }

    if (telNo.length === 0) {
    errors.telNo = "Required";
    } else if (telNo.length > 0 && !telNo.match(/^[0-9]+$/)) {
    errors.telNo = "Numbers only";
    }

    return errors;
};

handleValidation(e) {
    this.setState({
    [e.target.name]: e.target.value
    });
}

render() {
    const { errors } = this.state; // read errors from the state
    return (
    <form onSubmit={this.handleSubmit.bind(this)}>
        <div>
        <label>First name:</label>
        <input
            type="text"
            name="firstName"
            value={this.state.firstName}
            onChange={this.handleValidation.bind(this)}
        />
        {errors.firstName && <ErrorOutput errorText={errors.firstName} />}
        </div>
        <div>
        <label>Phone number:</label>
        <input
            type="tel"
            name="telNo"
            value={this.state.telNo}
            onChange={this.handleValidation.bind(this)}
        />
        {errors.telNo && <ErrorOutput errorText={errors.telNo} />}
        </div>
        <button>Submit</button>
    </form>
    );
}
}

render(<App />, document.getElementById("root"));

Upvotes: 2

Related Questions