Ivanka Todorova
Ivanka Todorova

Reputation: 10219

Validating variables in the state used in render() to style/populate html

I have the following React Component, which holds a form with two inputs and a button.

export default class Login extends Component {
    constructor(props) {
        super(props);
        this.state = {
            email: null,
            password: null
        }
    }

    emailInputChanged = (e) => {
        this.setState({
            email: e.target.value.trim()
        });
    };
    passwordInputChanged = (e) => {
        this.setState({
            password: e.target.value.trim()
        });
    };

    loginButtonClicked = (e) => {
        e.preventDefault();
        if (!this.isFormValid()) {
            //Perform some API request to validate data
        }
    };

    signupButtonClicked = (e) => {
        e.preventDefault();
        this.props.history.push('/signup');
    };

    forgotPasswordButtonClicked = (e) => {
        e.preventDefault();
        this.props.history.push('/forgot-password');
    };


    isFormValid = () => {
        const {email, password} = this.state;
        if (email === null || password === null) {
            return false;
        }

        return isValidEmail(email) && password.length > 0;
    };

    render() {
        const {email, password} = this.state;
        return (
            <div id="login">
                <h1 className="title">Login</h1>
                <form action="">
                    <div className={(!isValidEmail(email) ? 'has-error' : '') + ' input-holder'}>
                        <Label htmlFor={'loginEmail'} hidden={email !== null && email.length > 0}>email</Label>
                        <input type="text" className="input" id="loginEmail" value={email !== null ? email : ''}
                               onChange={this.emailInputChanged}/>
                    </div>
                    <div className={(password !== null && password.length === 0 ? 'has-error' : '') + ' input-holder'}>
                        <Label htmlFor={'loginPassword'}
                               hidden={password !== null && password.length > 0}>password</Label>
                        <input type="password" className="input" id="loginPassword"
                               value={password !== null ? password : ''}
                               onChange={this.passwordInputChanged}/>
                    </div>
                    <button type="submit" className="btn btn-default" id="loginButton"
                            onClick={this.loginButtonClicked}>
                        login
                    </button>
                </form>
                <div className="utilities">
                    <a href={'/signup'} onClick={this.signupButtonClicked}>don't have an account?</a>
                    <a href={'/forgot-password'} onClick={this.forgotPasswordButtonClicked}>forgot your password?</a>
                </div>
            </div>
        )
    }
}

export function isValidEmail(email) {
    const expression = /\S+@\S+/;
    return expression.test(String(email).toLowerCase());
}

The values of the inputs are stored in the component's state. I have given them initial value of null and update them using setState() in the onChange event.

In the render() method I use the state to colour inputs with invalid values. Currently I am only checking the email value against a simple regex and the password to be at least 1 character in length.

The reason I have set the initial values of the state variables to null so I can check in the layout and the initial style on the input to not be marked as "has-errors". However I need to extend the check to:

this.state.email !== null && this.state.email.length === 0

in order to work, because null has no length property.

Is there a cleaner and "React-ish" way to achieve this: - initial state of the div holding the inputs has no class has-errors - less checks when setting the value attribute on the input (because React doesn't accept null as value of the attribute)


Edit: If the initial value of this.state.email and this.state.password are empty strings, I get has-error applied to the div holding the inputs. In my opinion this is bad UX, because the user hasn't done anything and he is already wrong.

The hidden attribute is used by a custom component I made, which acts like "placeholder" (gets erased if something is typed in the input).

Video below showing how my form looks like when this.state.email and this.state.password are empty strings as well how my <Label> component works:

enter image description here

Upvotes: 0

Views: 86

Answers (1)

jhujhul
jhujhul

Reputation: 1607

You could create a validate function which return an errors object to know which fields are in error, and use empty strings as initial values. I don't understand what you are trying to do with the hidden attribute.

Edit: add a touched property in the state, to know which field have been touched.

export default class Login extends Component {
  constructor(props) {
    super(props);
    this.state = {
      email: '',
      password: '',
      touched: {},
    };
  }

  emailInputChanged = e => {
    this.setState({
      email: e.target.value.trim(),
      touched: {
        ...this.state.touched,
        email: true,
      },
    });
  };

  passwordInputChanged = e => {
    this.setState({
      password: e.target.value.trim(),
      touched: {
        ...this.state.touched,
        password: true,
      },
    });
  };

  loginButtonClicked = e => {
    e.preventDefault();
    if (!this.isFormValid()) {
      //Perform some API request to validate data
    }
  };

  isFormValid = () => {
    const errors = this.validate();

    return Object.keys(errors).length === 0;
  };

  validate = () => {
    const errors = {};
    const { email, password } = this.state;

    if (!isValidEmail(email)) {
      errors.email = true;
    }

    if (password.length === 0) {
      errors.password = true;
    }

    return errors;
  };

  render() {
    const { email, password, touched } = this.state;
    const errors = this.validate();

    return (
      <div id="login">
        <h1 className="title">Login</h1>
        <form action="">
          <div
            className={
              (errors.email && touched.email ? 'has-error' : '') +
              ' input-holder'
            }
          >
            <Label htmlFor={'loginEmail'}>email</Label>
            <input
              type="text"
              className="input"
              id="loginEmail"
              value={email}
              onChange={this.emailInputChanged}
            />
          </div>
          <div
            className={
              (errors.password && touched.password ? 'has-error' : '') +
              ' input-holder'
            }
          >
            <Label htmlFor={'loginPassword'}>password</Label>
            <input
              type="password"
              className="input"
              id="loginPassword"
              value={password}
              onChange={this.passwordInputChanged}
            />
          </div>
          <button
            type="submit"
            className="btn btn-default"
            id="loginButton"
            onClick={this.loginButtonClicked}
          >
            login
          </button>
        </form>
      </div>
    );
  }
}

export function isValidEmail(email) {
  const expression = /\S+@\S+/;
  return expression.test(String(email).toLowerCase());
}

Upvotes: 1

Related Questions