2

I want to add a red border only if an input is empty. I couldn't find a way to "addClass" in React so I'm using state. Right now the code will add red border to all inputs, even if it has text.

State:

this.state = {
  inputBorderError: false,
};

HTML/JSX:

<label>Name</label>
<input className={
  this.state.inputBorderError ? 'form-input form-input-fail' : 'form-input'
} />

<label>Email</label>
<input className={
  this.state.inputBorderError ? 'form-input form-input-fail' : 'form-input'
} />

<label>Message</label>
<textarea className={
  this.state.inputBorderError ? 'form-input form-input-fail' : 'form-input'
} />

CSS:

.form-input-fail {
  border: 1px solid red;
}

JS:

   let inputFields = document.getElementsByClassName('form-input');

   for (var i = 0; i < inputFields.length; i++) {
      if (inputFields[i].value === '') {
        this.setState({
          inputBorderError: true,
        });
      }
    }

I see the error in my code as it's basically setting the state to true anytime it finds an empty input. I think I may be approaching this incorrectly as there's only one state. Is there a solution based on my state approach, or is there another solution?

Eric Nguyen
  • 926
  • 3
  • 15
  • 37
  • Hi Eric, try out the codesandbox in my solution, I think it should cover all the bases. Let me know if you have any questions. – Chris Ngo Jun 18 '19 at 21:22
  • @ChristopherNgo I'm looking into adding simple email validation. Have a thread on this, could use your expertise: https://stackoverflow.com/questions/56676342/react-adding-email-validation-to-empty-input-validation – Eric Nguyen Jun 19 '19 at 21:49

3 Answers3

4

Right now, you have single state-value that affects all inputs, you should consider having one for each input. Also, your inputs are not controlled, it will be harder to record and track their values for error-handling.

It is good practice to give each input tag a name property. Making it easier to dynamically update their corresponding state-value.

Try something like the following, start typing into each input, then remove your text: https://codesandbox.io/s/nervous-feynman-vfmh5

class App extends React.Component {
  state = {
    inputs: {
      name: "",
      email: "",
      message: ""
    },
    errors: {
      name: false,
      email: false,
      message: false
    }
  };

  handleOnChange = event => {
    this.setState({
      inputs: {
        ...this.state.inputs,
        [event.target.name]: event.target.value
      },
      errors: {
        ...this.state.errors,
        [event.target.name]: false
      }
    });
  };

  handleOnBlur = event => {
    const { inputs } = this.state;
    if (inputs[event.target.name].length === 0) {
      this.setState({
        errors: {
          ...this.state.errors,
          [event.target.name]: true
        }
      });
    }
  };

  handleOnSubmit = event => {
    event.preventDefault();
    const { inputs, errors } = this.state;
    //create new errors object
    let newErrorsObj = Object.entries(inputs)
      .filter(([key, value]) => {
        return value.length === 0;
      })
      .reduce((obj, [key, value]) => {
        if (value.length === 0) {
          obj[key] = true;
        } else {
          obj[key] = false;
        }
        return obj;
      }, {});

    if (Object.keys(newErrorsObj).length > 0) {
      this.setState({
        errors: newErrorsObj
      });
    }
  };

  render() {
    const { inputs, errors } = this.state;
    return (
      <div>
        <form onSubmit={this.handleOnSubmit}>
          <label>Name</label>
          <input
            className={
              errors.name ? "form-input form-input-fail" : "form-input"
            }
            name="name"
            value={inputs.name}
            onChange={this.handleOnChange}
            onBlur={this.handleOnBlur}
          />

          <label>Email</label>
          <input
            className={
              errors.email ? "form-input form-input-fail" : "form-input"
            }
            name="email"
            value={inputs.email}
            onChange={this.handleOnChange}
            onBlur={this.handleOnBlur}
          />

          <label>Message</label>
          <textarea
            className={
              errors.message ? "form-input form-input-fail" : "form-input"
            }
            name="message"
            value={inputs.message}
            onChange={this.handleOnChange}
            onBlur={this.handleOnBlur}
          />
          <button type="submit">Submit</button>
        </form>
      </div>
    );
  }
}
Chris Ngo
  • 15,460
  • 3
  • 23
  • 46
  • what does the three dots ... represent? – Eric Nguyen Jun 18 '19 at 21:57
  • 1
    it's called a spread operator. It basically copies the state to another object. – Akolade Adesanmi Jun 18 '19 at 22:11
  • @EricNguyen sorry about that. Yes, essentially it just means you want to take the existing key-value pairs from an existing object and use them in your new object. Let me know if you have any other questions :) – Chris Ngo Jun 18 '19 at 23:22
  • @ChristopherNgo why do you have "const { inputs, errors } = this.state;" in render function? – Eric Nguyen Jun 19 '19 at 16:46
  • @EricNguyen that is called object destructuring. You take a key from an object and use it to declare a new variable, assigning it the value from the original key-value pair. This is essentially equivalent to `const inputs = this.state.inputs` and `const errors = this.state.errors` – Chris Ngo Jun 19 '19 at 17:27
  • @ChristopherNgo I understand the destructuring element but initially wondering why you declared it in render method. I think it's so you don't have to reference 'this.sate' every-time you reference specific state. Thanks, you've helped me a lot! – Eric Nguyen Jun 19 '19 at 18:38
  • @EricNguyen that is exactly why I did that ;). You're very welcome! – Chris Ngo Jun 19 '19 at 19:00
0

You are correct that there is only one state.

What you need to do is store a separate error for each input. one way to do this is with a set or array on state like state = {errors: []} and then check

<label>Name</label>
<input className={
  this.state.errors.includes('name') ? 'form-input form-input-fail' : 'form-input'
} />

<label>Email</label>
<input className={
  this.state.errors.includes('email') ? 'form-input form-input-fail' : 'form-input'
} />
} />
aaronmgdr
  • 83
  • 1
  • 5
0

You should keep track of the input value in the state instead of checking for borderStyling state only.

Base on your code, you could refactor it to something like this:

// keep track of your input changes
this.state = {
  inputs: {
    email: '',
    name: '',
    comment: '',
  },
  errors: {
    email: false,
    name: false,
    comment: false,
  }
};

// event handler for input changes

handleChange = ({ target: { name, value } }) => {
    const inputChanges = {
       ...state.inputs,
       [name]: value
    }

    const inputErrors = {
       ...state.errors,
       [name]: value == ""
    }

    setState({
      inputs: inputChanges,
      errors: inputErrors,
    });
}

HTML/JSX

// the name attribut for your input

<label>Name</label>
<input name="name" onChange={handleChange} className={
  this.errors.name == "" ? 'form-input form-input-fail' : 'form-input'
} />

<label>Email</label>
<input name="email" onChange={handleChange} className={
  this.errors.email == "" ? 'form-input form-input-fail' : 'form-input'
} />

<label>Message</label>
<textarea name="comment" onChange={handleChange} className={
  this.errors.comment == "" ? 'form-input form-input-fail' : 'form-input'
} />

And if you are probably looking at implementing it with CSS and js, you can try this article matching-an-empty-input-box-using-css-and-js

But try and learn to make your component reusable and Dry, because that is the beginning of enjoying react app.

[Revised]

Akolade Adesanmi
  • 1,152
  • 11
  • 15