Home > Software engineering >  Radio validation in multistep Reactjs (StepZilla)
Radio validation in multistep Reactjs (StepZilla)

Time:10-04

I'm attempting to add validation for radio buttons and check boxes within the StepZilla wizard. Essentially not allowing the user to proceed unless a choice has been selected. The way I have it now the user is allowed to skip the first step without first selecting either yes/no on the first radio question and I can't figure out why. I've added choice logic to the functions before rendering, but can't figure out where the issue is occurring.

I'm fairly certain it has something to do with the choiceVal: (data.choice != null) line I added within _validatedata. My thinking is that the validation is such that the choice should not be null, but maybe that's incorrect?

'use strict';


import React, { Component } from 'react';

export default class Step1 extends Component {
  constructor(props) {
    super(props);

    this.state = {
      email: props.getStore().email,
      name: props.getStore().name,
      choice: props.getStore().choice
    };

    this._validateOnDemand = true; // this flag enables onBlur validation as user fills forms

    this.validationCheck = this.validationCheck.bind(this);
    this.isValidated = this.isValidated.bind(this);
  }

  componentDidMount() {}

  componentWillUnmount() {}

  isValidated() {
    const userInput = this._grabUserInput(); // grab user entered vals
    const validateNewInput = this._validateData(userInput); // run the new input against the validator
    let isDataValid = false;

    // if full validation passes then save to store and pass as valid
    if (Object.keys(validateNewInput).every((k) => { return validateNewInput[k] === true })) {
        if (this.props.getStore().email != userInput.email || this.props.getStore().name != userInput.name || this.props.getStore().choice != userInput.choice) { // only update store of something changed
          this.props.updateStore({
            ...userInput,
            savedToCloud: false // use this to notify step4 that some changes took place and prompt the user to save again
          });  // Update store here (this is just an example, in reality you will do it via redux or flux)
        }

        isDataValid = true;
    }
    else {
        // if anything fails then update the UI validation state but NOT the UI Data State
        this.setState(Object.assign(userInput, validateNewInput, this._validationErrors(validateNewInput)));
    }

    return isDataValid;
  }

  validationCheck() {
    if (!this._validateOnDemand)
      return;

    const userInput = this._grabUserInput(); // grab user entered vals
    const validateNewInput = this._validateData(userInput); // run the new input against the validator

    this.setState(Object.assign(userInput, validateNewInput, this._validationErrors(validateNewInput)));
  }

   _validateData(data) {
    return  {
      nameVal: /^[a-zA-Z] [a-zA-Z] $/.test(data.name), // required: anything besides N/A
      emailVal: /^[a-zA-Z0-9.!#$%&'* /=?^_`{|}~-] @[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/.test(data.email),
      choiceVal: (data.choice != null) // required: regex w3c uses in html5
    }
  }

  _validationErrors(val) {
    const errMsgs = {
      nameValMsg: val.nameVal ? '' : 'Please enter a full name',
      emailValMsg: val.emailVal ? '' : 'A valid email is required',
      choiceValMsg: val.choiceVal ? '' : 'A choice is required'
    }
    return errMsgs;
  }

  _grabUserInput() {
    return {
      name: this.refs.name.value,
      email: this.refs.email.value,
      choice: this.refs.choice.value
    };
  }

  render() {
    // explicit class assigning based on validation
    let notValidClasses = {};

    if (typeof this.state.nameVal == 'undefined' || this.state.nameVal) {
      notValidClasses.nameCls = 'no-error col-md-8';
    }
    else {
       notValidClasses.nameCls = 'has-error col-md-8';
       notValidClasses.nameValGrpCls = 'val-err-tooltip';
    }

    if (typeof this.state.emailVal == 'undefined' || this.state.emailVal) {
        notValidClasses.emailCls = 'no-error col-md-8';
    }
    else {
       notValidClasses.emailCls = 'has-error col-md-8';
       notValidClasses.emailValGrpCls = 'val-err-tooltip';
    }

    if (typeof this.state.choiceVal == 'undefined' || this.state.choiceVal) {
      notValidClasses.choiceCls = 'no-error col-md-8';
    }
    else {
     notValidClasses.choiceCls = 'has-error col-md-8';
     notValidClasses.choiceValGrpCls = 'val-err-tooltip';
     }

Split the return below so it's easier to view.

    return (
      <div className="step step1">
        <div className="row">
          <form id="Form" className="form-horizontal">
            <div className="form-group">
              <label className="col-md-12 control-label">
                <h1>Step 1</h1>
              </label>
            </div>
         
            <div className="row content">
              <div className="col-md-12">
              <legend>Are you happy?</legend>
                <div className={notValidClasses.choiceCls}>
                  <input 
                    ref="choice"
                    type="radio" 
                    id="yes" 
                    name="choice" 
                    value="yes" 
                    onblur={this.validationCheck}
                    defaultValue={this.state.choice}
                    required />
                    <label form="yes">Yes</label>
                  <input 
                    ref="choice"
                    type="radio" 
                    id="no" 
                    name="choice" 
                    value="no"
                    onblur={this.validationCheck}
                    defaultValue={this.state.choic} />
                    <label form="no">No</label>
                </div>
                <div className={notValidClasses.choiceValGrpCls}>{this.state.choiceValMsg}</div>
              </div>
            </div>
            
            <div className="form-group col-md-12 content form-block-holder">
                <label className="control-label col-md-4">
                  Name
                </label>
                <div className={notValidClasses.nameCls}>
                  <input
                    ref="name"
                    autoComplete="off"
                    className="form-control"
                    required
                    defaultValue={this.state.name}
                    onBlur={this.validationCheck} />
                  <div className={notValidClasses.nameValGrpCls}>{this.state.nameValMsg}</div>
                </div>
              </div>
              <div className="form-group col-md-12 content form-block-holder">
                <label className="control-label col-md-4">
                  Email
                </label>
                <div className={notValidClasses.emailCls}>
                  <input
                    ref="email"
                    autoComplete="off"
                    type="email"
                    placeholder="[email protected]"
                    className="form-control"
                    required
                    defaultValue={this.state.email}
                    onBlur={this.validationCheck} />
                  <div className={notValidClasses.emailValGrpCls}>{this.state.emailValMsg}</div>
                </div>
              </div>
          </form>
        </div>
      </div>
    )
  }
}

CodePudding user response:

My guess would be the default store value props.getStore().choice is null. You might need to change the store such that this is defaulted to false or you could default it when you set the initial component state:

    this.state = {
      email: props.getStore().email,
      name: props.getStore().name,
      choice: props.getStore().choice ?? false
    };

It could well be true of email and name as well but whilst its not causing a problem (because the regex would return false over null) its also bad practice. They should be empty string explicitly to begin with.

CodePudding user response:

Just wanted to update saying that I got the validation to work with these additions, everything in between //**// is new.

      this.state = {
      email: props.getStore().email,
      name: props.getStore().name,
      choice: props.getStore().selectedOption
    };

//**// this.onValueChange = this.onValueChange.bind(this); //**//
       this._validateOnDemand = true; // this flag enables onBlur validation as user fills forms

    this.validationCheck = this.validationCheck.bind(this);
    this.isValidated = this.isValidated.bind(this);
  }

  //**//
  onValueChange(event) {
    this.setState({
      selectedOption: event.target.value
    });
  }
  //**//

[...]

  _grabUserInput() {
    return {
      name: this.refs.name.value,
      email: this.refs.email.value,
//**// choice: this.state.selectedOption //**//
    };
  }

After return

 <input 
                    ref="choice"
                    type="radio" 
                    value="no"
          //**//    checked={this.state.selectedOption === "no"}
                    onChange={this.onValueChange}   //**//
                    onBlur={this.validationCheck} 
                    />
  • Related