Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Handling JSON Validation Errors

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

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

Login Subscribe

Our first goal is to read the JSON validation errors and add them visually to the form. A moment ago, when I filled out the form with no rep number, the endpoint sent back an error structure that looked like this: with an errors key and a key-value array of errors below that.

Parsing the Error JSON

To get this data, we need to parse the JSON manually with var errorData = JSON.parse(jqXHR.responseText):

... lines 1 - 2
(function(window, $) {
... lines 4 - 24
$.extend(window.RepLogApp.prototype, {
... lines 26 - 61
handleNewFormSubmit: function(e) {
... lines 63 - 70
$.ajax({
... lines 72 - 78
error: function(jqXHR) {
var errorData = JSON.parse(jqXHR.responseText);
... line 81
}
});
},
... lines 85 - 88
});
... lines 90 - 107
})(window, jQuery);

That's the raw JSON that's sent back from the server.

To actually map the errorData onto our fields, let's create a new function below called _mapErrorsToForm with an errorData argument. To start, just log that:

... lines 1 - 2
(function(window, $) {
... lines 4 - 24
$.extend(window.RepLogApp.prototype, {
... lines 26 - 85
_mapErrorsToForm: function(errorData) {
console.log(errorData);
}
});
... lines 90 - 107
})(window, jQuery);

Above, to call this, we know we can't use this because we're in a callback. So add the classic var self = this;, and then call self._mapErrorsToForm(errorData.errors):

... lines 1 - 2
(function(window, $) {
... lines 4 - 24
$.extend(window.RepLogApp.prototype, {
... lines 26 - 61
handleNewFormSubmit: function(e) {
... lines 63 - 69
var self = this;
$.ajax({
... lines 72 - 78
error: function(jqXHR) {
var errorData = JSON.parse(jqXHR.responseText);
self._mapErrorsToForm(errorData.errors);
}
});
},
... lines 85 - 88
});
... lines 90 - 107
})(window, jQuery);

All the important stuff is under the errors key, so we'll pass just that.

Ok, refresh that! Leave the form empty, and submit! Hey, beautiful error data!

Mapping Data into HTML

So how can we use this data to make actual HTML changes to the form? There are generally two different approaches. First, the simple way: parse the data by hand and manually use jQuery to add the necessary elements and classes. This is quick to do, but doesn't scale when things get really complex. The second way is to use a client-side template. We'll do the simple way first, but then use a client-side template for a more complex problem later.

And actually, there's a third way: which is to use a full front-end framework like ReactJS. We'll save that for a future tutorial.

Creating a Selectors Map

In _mapErrorsToForm, let's look at the error data and use it to add an error span below that field. Obviously, we need to use jQuery to find our .js-new-rep-log-form form element.

But wait! Way up in our constructor, we're already referencing this selector:

... lines 1 - 2
(function(window, $) {
window.RepLogApp = function ($wrapper) {
... lines 5 - 17
this.$wrapper.on(
... line 19
'.js-new-rep-log-form',
... line 21
);
};
... lines 25 - 107
})(window, jQuery);

It's no big deal, but I would like to not duplicate that class name in multiple places. Instead, add an _selectors property to our object. Give it a newRepForm key that's set to its selector:

... lines 1 - 2
(function(window, $) {
... lines 4 - 24
$.extend(window.RepLogApp.prototype, {
_selectors: {
newRepForm: '.js-new-rep-log-form'
},
... lines 29 - 109
});
... lines 111 - 128
})(window, jQuery);

Now, reference that with this._selectors.newRepForm:

... lines 1 - 2
(function(window, $) {
window.RepLogApp = function ($wrapper) {
... lines 5 - 17
this.$wrapper.on(
... line 19
this._selectors.newRepForm,
... line 21
);
};
... lines 24 - 128
})(window, jQuery);

Below in our function, do the same: var $form = this.$wrapper.find(this._selectors.newRepForm):

... lines 1 - 2
(function(window, $) {
... lines 4 - 24
$.extend(window.RepLogApp.prototype, {
... lines 26 - 89
_mapErrorsToForm: function(errorData) {
... line 91
var $form = this.$wrapper.find(this._selectors.newRepForm);
... lines 93 - 108
}
});
... lines 111 - 128
})(window, jQuery);

Mapping the Data Manually

Now what? Simple: loop over every field see if that field's name is present in the errorData. And if it is, add an error message span element below the field. To find all the fields, use $form.find(':input') - that's jQuery magic to find all form elements. Then, .each() and pass it a callback function:

... lines 1 - 2
(function(window, $) {
... lines 4 - 24
$.extend(window.RepLogApp.prototype, {
... lines 26 - 89
_mapErrorsToForm: function(errorData) {
... line 91
var $form = this.$wrapper.find(this._selectors.newRepForm);
... lines 93 - 95
$form.find(':input').each(function() {
... lines 97 - 107
});
}
});
... lines 111 - 128
})(window, jQuery);

Inside, we know that this is actually the form element. So we can say var fieldName = $(this).attr('name'):

... lines 1 - 2
(function(window, $) {
... lines 4 - 24
$.extend(window.RepLogApp.prototype, {
... lines 26 - 89
_mapErrorsToForm: function(errorData) {
... lines 91 - 95
$form.find(':input').each(function() {
var fieldName = $(this).attr('name');
... lines 98 - 107
});
}
});
... lines 111 - 128
})(window, jQuery);

I'm also going to find the wrapper that's around the entire form field. What I mean is, each field is surrounded by a .form-group element. Since we're using Bootstrap, we also need to add a class to this. Find it with var $wrapper = $(this).closest('.form-group'):

... lines 1 - 2
(function(window, $) {
... lines 4 - 24
$.extend(window.RepLogApp.prototype, {
... lines 26 - 89
_mapErrorsToForm: function(errorData) {
... lines 91 - 95
$form.find(':input').each(function() {
var fieldName = $(this).attr('name');
var $wrapper = $(this).closest('.form-group');
... lines 99 - 107
});
}
});
... lines 111 - 128
})(window, jQuery);

Perfect!

Then, if there is not any data[fieldName], the field doesn't have an error. Just continue:

... lines 1 - 2
(function(window, $) {
... lines 4 - 24
$.extend(window.RepLogApp.prototype, {
... lines 26 - 89
_mapErrorsToForm: function(errorData) {
... lines 91 - 95
$form.find(':input').each(function() {
var fieldName = $(this).attr('name');
var $wrapper = $(this).closest('.form-group');
if (!errorData[fieldName]) {
// no error!
return;
}
... lines 103 - 107
});
}
});
... lines 111 - 128
})(window, jQuery);

If there is an error, we need to add some HTML to the page. The easy way to do that is by creating a new jQuery element. Set var $error to $() and then the HTML you want: a span with a js-field-error class and a help-block class:

... lines 1 - 2
(function(window, $) {
... lines 4 - 24
$.extend(window.RepLogApp.prototype, {
... lines 26 - 89
_mapErrorsToForm: function(errorData) {
... lines 91 - 95
$form.find(':input').each(function() {
var fieldName = $(this).attr('name');
var $wrapper = $(this).closest('.form-group');
if (!errorData[fieldName]) {
// no error!
return;
}
var $error = $('<span class="js-field-error help-block"></span>');
... lines 105 - 107
});
}
});
... lines 111 - 128
})(window, jQuery);

I left the span blank because it's cleaner to add the text on the next line: $error.html(errorsData[fieldName]):

... lines 1 - 2
(function(window, $) {
... lines 4 - 24
$.extend(window.RepLogApp.prototype, {
... lines 26 - 89
_mapErrorsToForm: function(errorData) {
... lines 91 - 95
$form.find(':input').each(function() {
var fieldName = $(this).attr('name');
var $wrapper = $(this).closest('.form-group');
if (!errorData[fieldName]) {
// no error!
return;
}
var $error = $('<span class="js-field-error help-block"></span>');
$error.html(errorData[fieldName]);
... lines 106 - 107
});
}
});
... lines 111 - 128
})(window, jQuery);

This jQuery object is now done! But it's not on the page yet. Add it with $wrapper.append($error). Also call $wrapper.addClass('has-error'):

... lines 1 - 2
(function(window, $) {
... lines 4 - 24
$.extend(window.RepLogApp.prototype, {
... lines 26 - 89
_mapErrorsToForm: function(errorData) {
... lines 91 - 95
$form.find(':input').each(function() {
var fieldName = $(this).attr('name');
var $wrapper = $(this).closest('.form-group');
if (!errorData[fieldName]) {
// no error!
return;
}
var $error = $('<span class="js-field-error help-block"></span>');
$error.html(errorData[fieldName]);
$wrapper.append($error);
$wrapper.addClass('has-error');
});
}
});
... lines 111 - 128
})(window, jQuery);

Yes! Let's try it! Refresh and submit! There it is!

The only problem is that, once I finally fill in the field, the error message stays! AND, I get a second error message! Man, we gotta get this thing cleaned up!

No problem: at the top, use $form.find() to find all the .js-field-error elements. And, remove those. Next, find all the form-group elements and remove the has-error class:

... lines 1 - 2
(function(window, $) {
... lines 4 - 24
$.extend(window.RepLogApp.prototype, {
... lines 26 - 89
_mapErrorsToForm: function(errorData) {
// reset things!
var $form = this.$wrapper.find(this._selectors.newRepForm);
$form.find('.js-field-error').remove();
$form.find('.form-group').removeClass('has-error');
$form.find(':input').each(function() {
... lines 97 - 107
});
}
});
... lines 111 - 128
})(window, jQuery);

Refresh now, and re-submit! Errors! Fill in one... beautiful!

And if we fill in both fields, the AJAX call is successful, but nothing updates. Time to tackle that.

Leave a comment!

0
Login or Register to join the conversation
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial uses an older version of Symfony... but since it's a JavaScript tutorial, the concepts are still ? valid!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.2.0",
        "symfony/symfony": "3.1.*", // v3.1.10
        "twig/twig": "2.10.*", // v2.10.0
        "doctrine/orm": "^2.5", // v2.7.1
        "doctrine/doctrine-bundle": "^1.6", // 1.10.3
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.3.2
        "symfony/swiftmailer-bundle": "^2.3", // v2.4.0
        "symfony/monolog-bundle": "^2.8", // 2.12.0
        "symfony/polyfill-apcu": "^1.0", // v1.2.0
        "sensio/distribution-bundle": "^5.0", // v5.0.22
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.16
        "incenteev/composer-parameter-handler": "^2.0", // v2.1.2
        "friendsofsymfony/user-bundle": "~2.0@dev", // dev-master
        "doctrine/doctrine-fixtures-bundle": "~2.3", // v2.4.1
        "doctrine/doctrine-migrations-bundle": "^1.2", // v1.2.1
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "friendsofsymfony/jsrouting-bundle": "^1.6" // 1.6.0
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.1.1
        "symfony/phpunit-bridge": "^3.0" // v3.1.6
    }
}
userVoice