Background

One of the main ideas that pulled me towards learning full stack JavaScript was the idea that common code could be used to validate data at both the client and the server. Validation is always needed at the server to prevent rogue requests from sending in invalid data. It is optional at the client but it is a good idea because it provides a reduction in post-backs to the server that are going to prove invalid and, if the client can work it out, why not do it there? It’s a nicer user experience.

This is what I have explored for this post.

Objective

The primary goal here, is to see whether (and how much) code can be shared for use at both the server and in the browser client. I’m doing this by creating a mini-web dialog setup that allows entry of data that must be validated.

It’s going to show a form that has 3 fields, Name, Email Address and Age. These are going to be validated as follows:

Name Cannot be left blank

Email Address Cannot be left blank Must be the correct format for an email address Is not on a blacklist held on the server (i.e. via a remote call to a server resource)

Age Cannot be left blank Is a numeric value between 13 and 150 (inclusive)



Implementation

It has taken a while to look into this and arrive at an initial solution that I’m happy with. It has one big flaw, in my view, but I will go into that later and address the flaw in my next post.

I’ve created the following page flow in this implementation:

When the NodeJS app is executed, it uses ExpressJS to deliver an MVC implementation at the server. It uses HandlebarsJS to build the page views. When visiting the root URL (“/”) of the application, it serves an intro page that has a link to the form. Clicking this link, takes you to the “/form” URL which presents the form. If you submit valid data in the form, it takes you to a data display page that shows the data you entered.

Entering invalid data will cause error messages on the form to be displayed and you are required to edit the form data again.

The files for this site are in the following file/folder structure:

“/server.js” is the web application that serves pages defined in the “/views” folder. Which pages are served, are defined in the two routes in the “form-route.js” and “home-route.js” files in the “src/server-only” folder. The “jquery.min.js” under “public/scripts” and “styles.css” under “public/styles” are static files to deliver jquery functionality and style information to my pages. I will not go into detail with these, they are standard Express, Handlebars and linked files fare.

I will detail the form view and the client-side and server-side validation and, in particular, the common code shared between these two areas in the “form-validation.js” file under the “src/common/” folder.

Implementation – The Form

Here is the page template file:

<html> <head> <title>{{{Title}}}</title> <link rel="stylesheet" type="text/css" href="/public/styles/styles.css"> <script type="text/javascript" src="/public/scripts/jquery.min.js"></script> </head> <body> <div class="container"> <div class="header">Fsjs Exploration</div> {{{ body }}} <div class="footer">Common validation code between browser and server</div> </div> </body> </html>

All pages in this example site have a consistent page template defined in “views/layouts/main.hbs”. This uses a “Title” value and a “body” value to render the more specific page elements. The form page is served by this route method:

router.get('/', function (req, res) { var vm = Fsjs.DataServices.defaultPageData('', 'root@localhost.org', 12); vm.Title = "Form-app - form page" res.render('form', vm); });

This simply builds the view model and renders the form page to the response using a call to the following defaultPageData function:

Fsjs.DataServices.defaultPageData = function(name, emailAddress, age){ return {name: name, emailAddress: emailAddress, age: age, nameFieldVisibility: 'visibility: hidden;', nameFieldFeedbackText: '', emailAddressFieldVisibility: 'visibility: hidden;', emailAddressFieldFeedbackText: '', ageFieldVisibility: 'visibility: hidden;', ageFieldFeedbackText: '' }; }

Notice that this view model also contains display details for the text and visibility of the error feedback elements. The following form is delivered to the body of the page template:

<form action="/form" method="post"> <p>Please enter your details:</p> <p> <label>Name (required):</label> <input name="nameField" type="text" id="nameField" class="inputTextBox" value="{{name}}"> <span id="nameFieldFeedback" class="errorFeedback" style="{{nameFieldFeedbackVisibility}}">{{nameFieldFeedbackText}}</span> </p> <p> <label>Email Address (required, correct format and not on server-held blacklist [test with 'root@localhost.org']):</label> <input name="emailAddressField" type="text" id="emailAddressField" class="inputTextBox" value="{{emailAddress}}"> <span id="emailAddressFieldFeedback" class="errorFeedback" style="{{emailAddressFieldFeedbackVisibility}}">{{emailAddressFieldFeedbackText}}</span> </p> <p> <label>Age (a number between 13 and 150):</label> <input name="ageField" type="text" id="ageField" class="inputTextBox" value="{{age}}"> <span id="ageFieldFeedback" class="errorFeedback" style="{{ageFieldFeedbackVisibility}}">{{ageFieldFeedbackText}}</span> </p> <p class="submit"> <input id="formSubmitButton" type="submit" value="Submit" /> </p> </form> <script type="text/javascript" src="/public/common/form-validator.js"></script> <script type="text/javascript" src="/public/scripts/form.js"></script> <script> $(function(){ $('#formSubmitButton').click(function(event){ if (!validateForm()) event.preventDefault(); }); }); </script>

The form has the 3 input elements and error feedback controls, the style and display data of which is controlled from values in the view model.

The script elements from line 23 onwards bring in the common code and the browser specific code for handling the validation at the client and they are used by invoking the “validateForm” function on the click of the submit button.

Implementation – Data Validation

The common code shared between the server and the client carries out the actual data validation. This file contains the following:

(function(exports){ Fsjs = exports.Fsjs || {}; Fsjs.FormValidation = Fsjs.FormValidation || {}; Fsjs.FormValidation.EmailBlacklistChecker = function(){} Fsjs.FormValidation.EmailBlacklistChecker.prototype.validateEmail = function(emailAddress) { throw 'validateEmail not implemented'; } var validateEmail = function(email) { var re = /^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/; return re.test(email); } var validityResponse = function(valid, errCode, errMessage){ if (valid) { return { isValid: true, errorCode: 0, errorMessage: '' }; } return { isValid: valid, errorCode: errCode, errorMessage: errMessage }; } Fsjs.FormValidation.validateName = function(name){ // Name cannot be blank. if (name.trim().length == 0) { return validityResponse(false, -1, 'Name cannot be blank.' ); } return validityResponse(true); } Fsjs.FormValidation.validateEmailAddress = function(emailAddress, emailBlacklistChecker){ // Email Address cannot be blank and must be the correct format. if (emailAddress.length == 0){ return validityResponse(false, -1, 'Email Address cannot be blank.' ); } if (!validateEmail(emailAddress)) { return validityResponse(false, -1, 'This is not a valid Email Address.' ); } if (typeof emailBlacklistChecker !== 'undefined') { if (!(emailBlacklistChecker instanceof Fsjs.FormValidation.EmailBlacklistChecker)) { throw 'emailBlacklistChecker is not an instance of EmailBlacklistChecker'; } if (!emailBlacklistChecker.validateEmail(emailAddress)){ return validityResponse(false, -1, 'This Email Address is blacklisted.' ); } } return validityResponse(true); } Fsjs.FormValidation.validateAge = function(age){ // Age must be a number between 13 and 150. var ageIntegerValue = parseInt(age, 10); if (isNaN(ageIntegerValue) || ageIntegerValue < 13 || ageIntegerValue > 150) { return validityResponse(false, -1, 'Age must be a number between 13 and 150.' ); } return validityResponse(true); } exports.Fsjs = Fsjs; })(typeof exports === 'undefined' ? this['form-validator'] = {} : exports)

Lines 1 and 60 wrap this whole object into an automatically invoked function. When included server-side, the instance of “Fsjs” will be available in the exports object. When linked into a browser page, this code is available via this[‘form-validator’].Fsjs for use in the browser.

Lines 3 and 4 ensure that the export objects are initially created or re-used if there is already one in existence. Lines 9 to 12 have a standard function for validating the format of and email address.

Lines 6 and 7 provide the “interface-esque” definition of an object that can be provided to implement the validation of the email address against a remote validation procedure.

Lines 14 to 20 provide a simple mechanism for ensuring all items return a consistently formatted response object.

Lines 22 to 28 provide a function used to ensure that the given name value is not blank. Lines 30 to 47 provide a function used to validate that the email address is not blank and is of the correct format. If given an implementation of an EmailBlacklistChecker object, it will use it to validate the email address is not blacklisted.

Lines 49 to 57 provide a function used to ensure that the age value is a number and between 13 and 150.

Line 59 exports our Fsjs object, ensuring it is available for use in other code areas.

Implementation – Client Form Validation

Fsjs = this['form-validator'].Fsjs || {}; var ClientEmailBlacklistChecker = function(){ this.validateEmail = function(emailAddress){ var call = $.ajax({ url: "/form/emailAddressIsBlacklisted", data: { "emailAddress": emailAddress }, cache: false, async: false, type: "GET", dataType: "json" }); return !$.parseJSON(call.responseText); } } ClientEmailBlacklistChecker.prototype = Object.create(Fsjs.FormValidation.EmailBlacklistChecker.prototype); function showError(name, message){ $(name) .text(message) .css("visibility", ""); } function hideError(name){ $(name).css("visibility", "hidden"); } var validateForm = function(){ var valid = true; // Name cannot be blank. var nameValidity = Fsjs.FormValidation.validateName($('#nameField').val()); if (!nameValidity.isValid) { showError('#nameFieldFeedback', nameValidity.errorMessage); valid = false; } else { hideError('#nameFieldFeedback'); } // Email Address cannot be blank and must be the correct format. var emailAddressValidity = Fsjs.FormValidation.validateEmailAddress($('#emailAddressField').val(), new ClientEmailBlacklistChecker()); if (!emailAddressValidity.isValid) { showError('#emailAddressFieldFeedback', nameValidity.errorMessage); valid = false; } else { hideError('#emailAddressFieldFeedback'); } // Age must be a number between 13 and 150. var ageValidity = Fsjs.FormValidation.validateAge($('#ageField').val()); if (!ageValidity.isValid) { showError('#ageFieldFeedback', nameValidity.errorMessage); valid = false; } else { hideError('#ageFieldFeedback'); } return valid; };

The form validation occurs when the validateForm method is executed upon the submit button being clicked on the form. Line 30 is the start of this function and it calls the various validation methods and hides or shows error displays on the page accordingly.

Lines 3 to 18 create an object that implements the client-side EmailBlacklistChecker. The actual email blacklist data is only present on the server so, at the client, the email blacklist is checked via an ajax call to a service on the server. [Incidentally, this block of code contains the flaw in this code that I have previously mentioned. The flaw is, specifically, the presence of line 11. It looks innocuous but has big implications that I will go into later on.]

Implementation – Server Form Validation

Server form validation occurs when the form data is posted to the following service method of the Express MVC route:

router.post('/', urlencodedParser, function(req, res){ var responsePageData = Fsjs.DataServices.defaultPageData( req.body.nameField, req.body.emailAddressField, req.body.ageField); if (!Fsjs.FormValidation.validateForm(req.body, responsePageData)) { responsePageData.Title = "Form-app - form page" res.render('form', responsePageData); } else { responsePageData.Title = "Form-app - success page" res.render('valid-posted-form', responsePageData); } });

This method calls the validateForm method in the “server-form-validation.js” (below). If the data is valid, the “valid-posted-page” form is rendered with the valid form data. If it is invalid, the form is shown again. The responsePageData object will have been loaded with the form values and error feedback display control data required to present the problems to the user.

The “server-form-validation.js” file is as follows:

Fsjs = require('../common/form-validator').Fsjs || {}; Fsjs.DataServices = require('./data-services').Fsjs.DataServices || {}; Fsjs.FormValidation = Fsjs.FormValidation || {}; var ServerEmailBlacklistChecker = function(){ this.validateEmail = function(emailAddress){ return Fsjs.DataServices.getEmailBlacklist().indexOf(emailAddress) == -1; } } ServerEmailBlacklistChecker.prototype = Object.create(Fsjs.FormValidation.EmailBlacklistChecker.prototype); Fsjs.FormValidation.validateForm = function(body, responseObject){ var valid = true, nameValidity, emailAddressValidity, ageValidity; // Name cannot be blank. nameValidity = Fsjs.FormValidation.validateName(body.nameField); if (!nameValidity.isValid) { responseObject.nameFieldFeedbackText = nameValidity.errorMessage; responseObject.nameFieldFeedbackVisibility = ''; valid = false; } else { responseObject.nameFieldFeedbackVisibility = 'visibility: hidden;'; } // Email Address cannot be blank and must be the correct format. emailAddressValidity = Fsjs.FormValidation.validateEmailAddress(body.emailAddressField, new ServerEmailBlacklistChecker()); if (!emailAddressValidity.isValid) { responseObject.emailAddressFieldFeedbackText = emailAddressValidity.errorMessage; responseObject.emailAddressFieldFeedbackVisibility = ''; valid = false; } else { responseObject.emailAddressFieldFeedbackVisibility = 'visibility: hidden;'; } // Age must be a number between 13 and 150. ageValidity = Fsjs.FormValidation.validateAge(body.ageField); if (!ageValidity.isValid) { responseObject.ageFieldFeedbackText = ageValidity.errorMessage; responseObject.ageFieldFeedbackVisibility = ''; valid = false; } else { responseObject.ageFieldFeedbackVisibility = 'visibility: hidden;'; } return valid; }; exports.Fsjs = Fsjs;

Lines 1 to 3 ensure that required objects are used if they exist or are created if they are not.

Lines 5 to 10 implement the server-side EmailBlacklistChecker object. On the server, it is simply a case of determining whether the email address is on the server-held blacklist.

Line 12 starts the function that validates the data and sets up the error feedback display fields accordingly.

To Conclude

What this code achieves is a form validation mechanism, using code common to both the client and the server, that will validate the data at the client and the server in the same way. If you turn JavaScript off at your browser, the same validation and response occurs, but the server is called more often. If you go outside of the browser completely and try to post bad data to the post service method, you still get the form returned and nothing is done with the bad data on the server.

Unit Testing

I have only created unit tests that address “form-validator.js” that contains the common code. I’ve limited my tests to this as this was my primary area of focus. I would also, ideally, be testing the code in “form.js” and “server-form-validation.js” but will explore testing of browser code at a later time.

and Finally, the Flaw…

The “async: false,” on line 11 of form.js is a problem. Ajax calls are, by their nature, asynchronous. You are supposed to make the ajax call and then get on with other things and being responsive to the user of the page until a later time when the call has completed and the page processes the result of that call.

Adding “async: false” to an ajax call tells the browser to make the ajax call and do nothing else until that call has completed and the response is known. In this scenario, we are only doing one ajax call and it returns immediately so there is no real issue. But what if we were doing 10 or 20 ajax calls all waiting one-after-the-other until they complete? What if your site is so busy that a single call takes 10 seconds or more for the response to come back? Your page will look like it has stopped responding. Much longer than 10 seconds and the browser will start saying that the JavaScript on the page is not responding, and ask your user if they want to kill the page and re-load it.

I’ve done the exploration on working around this flaw and will go straight into that post.