Change Password - Part 6 - Avoid Blocking, and Wrap Up


In this video we are continuing on with our form error helper implementation. In previous videos we have learned how to display errors on the form fields that have problems, and also covered how to convert the JSON representation of a Symfony form error into an object that plays nicely with our system.

With these two pieces in place, we can now cover how to map the real form errors onto the respective form fields. There is an extra topic to cover here - which is running tasks in parallel to improve overall application performance. More on this as we go through.

Let's start by updating our profile.saga.js file to use the new formErrorHelper function that we have created:

// /src/sagas/profile.saga.js

// * snip *
import formErrorHelper from '../helpers/formErrorHelper';

export function *doChangePasswordFailed(action) {

  const errorData = action.payload.response;

  const currentPassword = yield call(formErrorHelper, errorData, 'children.current_password.errors');

  yield put(stopSubmit('change-password', {
    currentPassword,
  }));
}

export function *watchChangePasswordFailed() {
  yield *takeLatest(types.CHANGE_PASSWORD__FAILED, doChangePasswordFailed);
}

As we have covered throughout this course, with Sagas we don't actually invoke the function. Instead, we only 'describe' the function that we want to invoke (think: call), and the arguments we want to use when calling that function.

This is demonstrated by the line:

const currentPassword = yield call(formErrorHelper, errorData, 'children.current_password.errors');

We want to run / call the formErrorHelper function.

We have just created this function in previous videos, and to quickly recap, we have the function signature of:

const errorHelper = (errors, errorPath) => {
  // * etc *

Therefore we are calling the formErrorHelper function with two arguments. The errorData, and the path to the error(s) we care about for this form field. If any of this is new to you, please do watch the previous videos in the "change password" section of this course where this is covered in further detail.

There is an important point to note here: when this code runs, the code execution will be momentarily 'paused' whilst the real function call is executed. Then the result is passed back into the generator function, and we continue on to the next step. This isn't unique to this code in any way, but it is important to be aware of.

In this particular instance, this wouldn't be a problem. We have one call, and then we move on.

However, as we know, our "Change Password" form has three fields. We have the currentPassword, newPassword, and newPasswordRepeated.

We will need to call the formErrorHelper for each field.

That means we will call > block > call > block > call > block.

Again, even in this example, the chances are you would hardly notice this happen. Modern computers are fast, and these calls are really quite small. But they aren't free.

Let's add in the additional error checking calls so we can better see this problem:

// /src/sagas/profile.saga.js

// * snip *
import formErrorHelper from '../helpers/formErrorHelper';

export function *doChangePasswordFailed(action) {

  const errorData = action.payload.response;

  const currentPassword = yield call(formErrorHelper, errorData, 'children.current_password.errors');
  const newPassword = yield call(formErrorHelper, errorData, 'children.plainPassword.children.first.errors');
  const newPasswordRepeated = yield call(formErrorHelper, errorData, 'children.plainPassword.children.second.errors');

  yield put(stopSubmit('change-password', {
    currentPassword,
    newPassword,
    newPasswordRepeated,
  }));
}

export function *watchChangePasswordFailed() {
  yield *takeLatest(types.CHANGE_PASSWORD__FAILED, doChangePasswordFailed);
}

To quickly cover the potentially confusing object being passed in to stopSubmit:

{
  currentPassword,
  newPassword,
  newPasswordRepeated,
}

One really nice feature of JavaScript's ES6 is that if the key and the value are the same, you can omit the value. In other words, this is identical to:

{
  currentPassword: currentPassword,
  newPassword: newPassword,
  newPasswordRepeated: newPasswordRepeated,
}

We declared the three const variables above, so they would be the values. And then the key is the same as the variable / value name, so we can simply use the key as the key and the value. Looks a bit confusing at first, but is much cleaner once you know what it's doing, in my opinion. Ok, enough detour.

As mentioned, this current implementation would work, but could be improved.

Rather than go through this call > block > call > block pattern, we could instead run all three (or however many calls we have) in parallel. And doing so is surprisingly easy:

// /src/sagas/profile.saga.js

// * snip *
import formErrorHelper from '../helpers/formErrorHelper';

export function *doChangePasswordFailed(action) {

  const errorData = action.payload.response;

  const [currentPassword, newPassword, newPasswordRepeated] = [
    yield call(formErrorHelper, errorData, 'children.current_password.errors'),
    yield call(formErrorHelper, errorData, 'children.plainPassword.children.first.errors'),
    yield call(formErrorHelper, errorData, 'children.plainPassword.children.second.errors')
  ];

  yield put(stopSubmit('change-password', {
    currentPassword,
    newPassword,
    newPasswordRepeated,
  }));
}

Note that the variables currentPassword, newPassword, and newPasswordRepeated match up with the order used inside the array of yield'ed calls.

You do need to be careful here as if any of these call's happen to go wrong in an unexpected way, all of these calls would be rejected. In our case, our formErrorHelper simply returns undefined if unable to process the data, or the path, so we should be ok.

Making It Look Right

By now our form errors should be being returned, and also be displaying against the respective erroring form field.

However, our newPassword / newPasswordRepeated setup is a special case. These two fields should be tied together. When one errors out, the other should show an error also - even if just a Bootstrap styling error.

To address this we will make use of Redux Form's Fields component.

For a more in-depth look into this, please do watch the video. Here is the finished component we are going to use:

// /src/components/FormPasswordRepeatedFields.js

import React from 'react';
import classNames from 'classnames';

const FormPasswordRepeatedFields = (fields) => {
  const formGroup = classNames(
    'form-group',
    {'has-danger': fields.newPassword.meta.touched && fields.newPassword.meta.error}
  );

  const formControlCss = classNames(
    'form-control',
    {'form-control-danger': fields.newPassword.meta.touched && fields.newPassword.meta.error}
  );

  return (
    <div>
      <div className={formGroup}>
        <label className="form-control-label">New Password</label>
        <div className="input-row">
          <input {...fields.newPassword.input}
                 placeholder="New Password"
                 className={formControlCss}
                 type="password"/>
          {fields.newPassword.meta.touched && fields.newPassword.meta.error &&
          <span className="form-control-feedback">{fields.newPassword.meta.error}</span>}
        </div>
      </div>

      <div className={formGroup}>
        <label className="form-control-label">New Password Repeated</label>
        <div className="input-row">
          <input {...fields.newPasswordRepeated.input}
                 placeholder="New Password Repeated"
                 className={formControlCss}
                 type="password"/>
          {fields.newPassword.meta.touched && fields.newPassword.meta.error &&
          <span className="form-control-feedback">{fields.newPasswordRepeated.meta.error}</span>}
        </div>
      </div>
    </div>
  );
};

FormPasswordRepeatedFields.propTypes = {
  fields: React.PropTypes.object.isRequired
};

export default FormPasswordRepeatedFields;

There's quite a lot of styling going on here - which does somewhat cloud the more important parts.

As far as the styling goes, each of the used styles is from Bootstrap, form-control-feedback, form-control-danger, has-danger, etc. All of that can be found on the Bootstrap website. Regarding the use of classnames, please watch this video for more details.

There are some strange points to this component though.

In all of my testing, I've never seen newPasswordRepeated contain any errors.

We do want to show the relevant error CSS when the newPassword field has errors though. This is to tie the two together, for our end users, rather than for the computers benefit.

This is why we can use the formGroup, and formControlCss for both fields. If an error condition exists on newPassword then we also want the error CSS to display on the newPasswordRepeated field. I have included the error output for newPasswordRepeated on the form, but as mentioned, have never seen this actually display anything. Please let me know if you ever do.

To make use of this new component we must update the ChangePasswordForm file:

// src/components/ChangePasswordForm.js

import React from 'react';
import {Field, Fields, reduxForm} from 'redux-form';
import {Button} from 'reactstrap';
import FormField from './FormField';
import FormPasswordRepeatedFields from './FormPasswordRepeatedFields';

const ChangePasswordForm = (props) => {
  return (
    <form onSubmit={props.handleSubmit} className="form-change-password">

      <Field component={FormField}
             name="currentPassword"
             type="password"
             label="Current Password"
             placeholder="Current Password"
             required="required"
             className="form-control"
      />

      <Fields names={[ 'newPassword', 'newPasswordRepeated' ]} component={FormPasswordRepeatedFields}/>

Note here the inclusion of Fields in the import for redux-form.

And this concludes the implementation for changing our password.

You may be thinking - Chris, six videos covering how to change a password seems excessive.

I would normally agree, but along the way we have covered a ton of stuff that's useful not just for changing passwords, but across most any form you will use in your application.

What we have also done here is created things we can re-use. This password field form component can - and will - also be used in our registration form. What we've covered in terms of form styling is useful across all forms. The same again for handling form errors.

We now have most of the pieces in place for the foundations of our front end. We can login and log out, see our profile, update our password, but as yet we cannot register.

That's exactly what we will get onto in the final part of this series. And as we have laid all these foundations, this part will be easier than you might think :)

Code For This Course

Get the code for this course.

Episodes