If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeOur 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.
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!
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.
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); |
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.
"Houston: no signs of life"
Start the conversation!
// 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
}
}