This course is archived!

While the concepts of this course are still largely applicable, it's built using an older version of Symfony (4) and React (16).

Login to bookmark this video
Buy Access to Course
37.

Displaying Server Validation Errors

Share this awesome video!

|

Keep on Learning!

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

When server-side validation fails, the API returns a 400 status code with the details of the error in the response. And thanks to the change we just made, fetch() now throws an error, which we can handle!

Open RepLogApp: inside of handleAddRepLog, if the call to createRepLog fails, we need to grab the validation error messages and put them on the screen. And now that our Promise can fail, we can do this with a .catch(). Pass this an arrow function. For now, just console.log(error.response).

137 lines | assets/js/RepLog/RepLogApp.js
// ... lines 1 - 44
handleAddRepLog(item, reps) {
// ... lines 46 - 54
createRepLog(newRep)
.then(repLog => {
// ... lines 57 - 66
})
.catch(error => {
console.log(error.response);
})
// ... line 71
}
// ... lines 73 - 137

Let's see what that looks like: refresh, try dark matter again and... cool! We have the Response object!

Let's decode this just like we did before with success: error.response.json(). This returns another Promise, so add .then with an errorsData argument for the next arrow function. Log that... then let's go see what it looks like: dark matter, 10 times... perfect!

139 lines | assets/js/RepLog/RepLogApp.js
// ... lines 1 - 67
.catch(error => {
error.response.json().then(errorsData => {
console.log(errorsData);
})
})
// ... lines 73 - 139

It has an errors key, and a list of errors below where the key tells us which field this is for. So, how can we print this onto the screen? Well... it depends on how fancy you want to get. You could use the key of each error to find the field it belongs to, and render the error in that place. Or, you could print all of the errors on top of the form. Or, you could be even lazier like me and just print the first error above the form.

Adding the Error State

To do that, we need new state inside of RepLogApp to keep track of the current "rep log validation" error message. Add one called newRepLogValidationErrorMessage. set to empty quotes.

146 lines | assets/js/RepLog/RepLogApp.js
// ... lines 1 - 7
constructor(props) {
// ... lines 9 - 10
this.state = {
// ... lines 12 - 17
newRepLogValidationErrorMessage: ''
};
// ... lines 20 - 25
}
// ... lines 27 - 146

But wait, this is interesting. When we added client-side validation, we stored it in RepLogCreator and it handled all of that logic. But because RepLogApp is the only component that's aware of the server, this is state that it needs to handle. And, it's not really form validation logic: remember RepLogApp doesn't even know our app uses a form. Nope, it's really a business logic validation failure: something, somehow tried to save a rep log with invalid data.

Copy that name and go down to handleAddRepLog(). First, if we're successful, in case there was already a validation message on the screen, we need to set it back to empty quotes to remove it.

146 lines | assets/js/RepLog/RepLogApp.js
// ... lines 1 - 55
createRepLog(newRep)
.then(repLog => {
this.setState(prevState => {
// ... lines 59 - 60
return {
// ... lines 62 - 63
newRepLogValidationErrorMessage: '',
};
});
// ... lines 67 - 68
})
// ... lines 70 - 146

Down in catch(), add const errors = errorsData.errors. Then, to get just the first error... um... it's actually a bit tricky. Silly JavaScript! Use const firstError = errors[Object.keys(errors)[0]].

Wow! We need to do this because errors isn't an array, it's an object with keys. Use this in the setState() call: newRepLogValidationErrorMessage set to firstError.

146 lines | assets/js/RepLog/RepLogApp.js
// ... lines 1 - 69
.catch(error => {
error.response.json().then(errorsData => {
const errors = errorsData.errors;
const firstError = errors[Object.keys(errors)[0]];
this.setState({
newRepLogValidationErrorMessage: firstError
});
})
})
// ... lines 80 - 146

Passing & Using the Error State

Done! As you know, the new state is instantly passed down to the child as a prop. But we need to use this state in RepLogCreator: I want to put the message right above the form.

Ok! Time to pass some props around! Step 1: define the new prop type as a required string. Step 2: destructure this to a new variable. And step 3, pass to RepLogCreator. But wait! Let's change the name validationErrorMessage= then the variable.

112 lines | assets/js/RepLog/RepLogs.js
// ... lines 1 - 17
export default function RepLogs(props) {
const {
// ... lines 20 - 30
newRepLogValidationErrorMessage
} = props;
// ... lines 33 - 38
return (
// ... lines 40 - 87
<RepLogCreator
// ... line 89
validationErrorMessage={newRepLogValidationErrorMessage}
/>
// ... lines 92 - 94
);
}
// ... line 97
RepLogs.propTypes = {
// ... lines 99 - 109
newRepLogValidationErrorMessage: PropTypes.string.isRequired,
};

Why the name change? Well... if you think about it, even though we called this component RepLogCreator, there's nothing about the component that's specific to creating rep logs. We could easily reuse it later for editing existing rep logs... which is awesome!

All RepLogCreator cares about is that we're passing it the validation error message: it doesn't care that it's the result of creating a new rep log versus editing an existing one.

Anyways, let's go use this: add the prop type: validationErrorMessage as a string that's required. Then, destructure it. Oh, we don't have any props destructuring yet. No problem - use const {} = this.props and then add validationErrorMessage. I typed that a bit backwards so PhpStorm would auto-complete the variable name.

108 lines | assets/js/RepLog/RepLogCreator.js
// ... lines 1 - 3
export default class RepLogCreator extends Component {
// ... lines 5 - 53
render() {
// ... line 55
const { validationErrorMessage } = this.props;
// ... lines 57 - 100
}
}
// ... line 103
RepLogCreator.propTypes = {
// ... line 105
validationErrorMessage: PropTypes.string.isRequired,
};

Finally, just inside the form, use our trick: validationErrorMessage &&, some parenthesis and a div with className="alert alert-danger" and the message inside.

108 lines | assets/js/RepLog/RepLogCreator.js
// ... lines 1 - 53
render() {
// ... lines 55 - 57
return (
<form onSubmit={this.handleFormSubmit}>
{validationErrorMessage && (
<div className="alert alert-danger">
{validationErrorMessage}
</div>
)}
// ... lines 65 - 98
</form>
);
}
// ... lines 102 - 108

We're only printing the first message. But if you wanted to print a list instead, it's no big deal: just use the map() function like normal to render all of them.

Let's see if it works! Move over and make sure everything is refreshed. Select dark matter, 10, and... yes! We got it! Hmm, except, because we're not printing the error next to the field... it's not super obvious what I need to fix! If you're going to be lazy like us, you need to make sure the errors are descriptive.

Go into src/Form/Type/RepLogType.php. Most validation comes from our RepLog entity. But the form fields themselves can add some "sanity" validation. To customize the message, add an option: invalid_message set to "Please lift something that is understood by our scientists."

32 lines | src/Form/Type/RepLogType.php
// ... lines 1 - 13
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
// ... line 17
->add('item', ChoiceType::class, array(
// ... lines 19 - 20
'invalid_message' => 'Please lift something that is understood by our scientists.'
))
// ... line 23
}
// ... lines 25 - 32

Much easier to understand! Try that: refresh, choose dark matter and... got it!

Except... hmm: when you get a validation error, the "Lifting to the database" loading message is still there! It... shouldn't be. Let's fix that and learn about a super-useful language feature called "object rest spread".