Handling JSON Validation Errors

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):

109 lines web/assets/js/RepLogApp.js
... 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:

109 lines web/assets/js/RepLogApp.js
... 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):

109 lines web/assets/js/RepLogApp.js
... 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:

109 lines web/assets/js/RepLogApp.js
... 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:

130 lines web/assets/js/RepLogApp.js
... 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:

130 lines web/assets/js/RepLogApp.js
... 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):

130 lines web/assets/js/RepLogApp.js
... 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:

130 lines web/assets/js/RepLogApp.js
... 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'):

130 lines web/assets/js/RepLogApp.js
... 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'):

130 lines web/assets/js/RepLogApp.js
... 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 return:

130 lines web/assets/js/RepLogApp.js
... 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:

130 lines web/assets/js/RepLogApp.js
... 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]):

130 lines web/assets/js/RepLogApp.js
... 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'):

130 lines web/assets/js/RepLogApp.js
... 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:

130 lines web/assets/js/RepLogApp.js
... 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!