Stratui Validation Framework
Travelers • Scott Sauyet • February 8, 2010
Validation Framework
====================
### Scott Sauyet ###
An overview of how to use the new validation framework.
The Problem
===========
From older Vehicle Panel code:
> errorHandler.clear("year-validation");
> ui.removeError($year);
>
> if (!ui.validateModelYear(year, isAntique) ||
year === "") {
> ui.addError($year);
> errorHandler.addError({
> Description: "Year is invalid, please correct."
> }, "year-validation");
> return false;
> }
Note how much validation can be mixed in with what we'd prefer is plain business
logic.
The Solution
============
Move as much of the validation work out to stand-alone components and
simple-to-use APIs.
The validation framework involves the following new Javascript files:
* `fieldChangeHandler.js`
* `questionManager.js`
* `errorHandler.js`
* `validators.js`
* `renderers.js`
as well as changes to `pageManager.js`. Most of your interactions will be
with the `FieldChangeHandler`.
Loading the Questions
=====================
To set things up, you need to tell the `QuestionManager` about the questions
used for your panel. It's pretty simple to do in your `.ascx` page:
> <% var contents = Model.GetMarkupForPanel("driver"); %>
>
> <script type="text/javascript">
> (function() {
> questionConfigs = <%=contents[
PanelHelper.RULE_QUESTION_CONFIGS]%>;
> QuestionManager.load(questionConfigs);
> }());
> </script>
Later when we discuss the internals of the framework, we'll talk about what
happens during this call.
FieldChangeHandler
==================
The object you will interact with most is a FieldChangeHandler. A
FieldChangeHandler
* Listens for changes on your fields and
* Runs any registered initializer functions to update the field
* Runs any built-in or customized validations
* Show you validation error messages if validation fails, or
* Updates the policy if validation succeeds
* Calls functions registered to listen to valid and invalid changes
* Runs the behavior rules
* Runs a custom clean-up function if desired
* Runs client-side validation on all fields (e.g. on Continue)
* Handles server-side validations errors
* Allows you to manually add and remove validation messages if needed
Constructing a FieldChangeHandler
=================================
You will probably need one FieldChangeHandler per view. The code to create one
looks like this:
> var handler = new FieldChangeHandler({
> containerName: "driverPanelContainer",
> behaviorRulesKey: "DriverPanel"
> //, other optional parameters here
> });
We'll discuss the optional parameters later. For now, it should be simple
enough that `containerName` is the id of some container holding all your
controls and that `behaviorRulesKey` is the string supplied to the behavior
rules system to limit the subset of rules that will be run.
Telling Validation Framework About Fields
=========================================
The FieldChangeHandler supplies a `makeId` function that connects any
templatized ids with the original question. This is from the `driverPanel.js`:
> var $tds = $templateCells.clone();
> $("select, input, button", $tds).each(function () {
> this.id = handler.makeId(this.id, { drvId: drvId });
> });
This is just a thin wrapper around `QuestionManager`'s `makeId`. `makeId` takes
a templatized id, and the data needed to fill in that template, and returns a
filled-in id. That is no more than is already done by `ui.fillTemplate`, but
this function also stores that data for subsequent retrieval, which is essential
if you have templatized ids. On the to-do list is another function that simply
identifies a field's id with a particular question, probably
`registerId(dataBindingId, newId, data).`
Running Code Before Validation
==============================
If you want a function to run before the validation is checked on your field,
you can do this:
> var capitalizeField = function (ctrl, templateId, data) {
> var $ctrl = $(ctrl);
> $ctrl.val($(ctrl).val().toUpperCase());
> }
>
> handler.addControlInitializer(acordConstants.LicensePermitNumber, capitalizeField);
Passed to your function are `ctrl`, which is the control being changed,
`templateId`, which is the (templated if appropriate) id for the question, and
`data`, which is a magical object discussed next.
Since this runs before validation, it lets you work as above and actually change
the value of the control. There is only one real restrictions on what you can
do in here: Do not trigger a change event on the control in question, or call
a function which might do so. That leads to an infinite loop.
Magical `data` Parameter
=========================
Many functions you write to work with the validation framework are passsed a
`data` parameter. This is the object we passed in to `makeId` earlier.
You can count on it having at least those values used to fill in the templates
if your ids were templatized. But you can also use this if you wish to store
additional metadata about a particular field that's known when the field is
created, but doesn't have a good home in the application.
Adding Custom Validation
========================
There are built-in validations derived from the questions for things like
minimum and maximum field length, minimum and maximum integer or decimal
values, regex formats, data types and required fields. If you need additional
validation, you can do it like this:
> var validYear = function(val, ctrl) {
> var status = !!/\d\d\d\d/.test(val),
> vehId = ui.parseControlId(ctrl).vehId || "";
> status = status && ui.validateModelYear(val, Vehicles.isAntique({ vehId: vehId }));
> return status ? { status: true} : {
> status: false,
> msg: "Year must be numeric value greater than 1900"
> };
> };
>
> handler.addValidator(acordConstants.modelYearTemplate, validYear);
Custom Validator Function
=========================
A validator function receives two parameters, `val`, `ctrl`. (It should
probably also receive a magical `data` parameter as well; that's on the to-do
list.) The result should be an object that has a boolean `status` property. If
the validation succeeds, that's all needed. If it fails, the object should also
have a `msg` property with a string message about the reason for validation
failure. This is a typical idiom:
> // find if the value is valid, storing as `isValid`.
> return isValid
> ? {status: true}
> : {status: false, msg: "Reason for failure"};
Note that at the moment your custom message is **not** what shows up in the top
message. There is some built-in default behavior for most fields. But if you
want to override that, you can add an `overrideErrorMessage` property with value
of `true`.
Alternate Validation Object
===========================
You don't have to supply a simple function to the validation framework. You can
use an object instead with a `validate` function structured as discussed. This
allows you to supply several other parameters to the process:
> handler.addValidator(templateId, {
> validate: function(val, ctrl) { /* return result as above */ },
> dontRunOnChangeEvent: true,
> checkBlanks: true
> });
The `validate` function is required. The optional properties are:
* `dontRunOnChangeEvent`: (boolean) run only on events like continue, not
whenever the field changes. Default: `false`.
* `checkBlanks`: (boolean) run this validation against even blank fields.
Default: `false`.
Listening to Change Events
==========================
We can use the FieldChangeHandler to listen to changes to our fields and run
custom functions when they are changed. We can connect to this with three very
similar calls:
> var myFunc = function(ctrl, data) { /* ... */ },
> id = // some templatized control id
>
> handler.addValidListener(id, myFunc); // OR
> handler.addInvalidListener(id, myFunc); // OR
> handler.addListener(id, myFunc);
In the first case, your function will be called when the changed field value has
properly passed validation. In the second it will only be called if the value
fails validation. And in the third it will be called regardless of whether
the value validates.
Example Listener
================
var mfg = acordConstants.manufacturerCdTemplate;
handler.addValidListener(mfg, function (ctrl, data) {
var $year = ui.getControlById(acordConstants.modelYearTemplate, data),
year = $year.val(),
manufacturer = $(ctrl).val();
loadModels(data.vehId, year, manufacturer);
});
The parameters supplied are the actual control being used and the magical data
parameter supplied earlier. Any return value from your function is ignored.
Presumably, most of the time you would want to connect to the `addValidListener`
function, but the `addInvalidListener` and the `addListener` ones are there if
needed. Note that these are fired **after** the field has been updated.
Additional Features
===================
There are several additional features of the FieldChangeHandler that you might
want to use:
* `addValidationMessage`
* `addMessage`
* `removeMessage`
* `validateField`
* `validateAll`
* `ignoreOnContinue`
* `remove`
* `removeValidator`
Manually Adding and Removing Messages
=====================================
* This adds a validation message to `myCtrl`:
> handler.addValidationMessage(myCtrl.id,
> "Does not compute");
* This adds a general error message (probably to the top message area):
> handler.addMessage("The XYZ Service is down",
> "xyzService");
* This removes that message:
> handler.removeMessage("xyzService");
Manually Triggering Validation
==============================
* This triggers a validation of the field `myCtrl`:
> // returns boolean
> handler.validateField(myCtrl, false, true);
(The additional parameters are `update` and `dontRender`; the first is
probably implementation cruft which should be refactored out. The second, if
set to true, will only return the boolean value and not render any errors.)
* This validates all the fields in your container:
> handler.validateAll(); // return boolean
Both of these return `true` if everything validates without issue, `false` if
there are any validation errors.
Stopping Certain Validations
============================
* This tells the framework to ignore the validation of a particular field in the
result of `validateAll` (which is what is used by `continue`:
> handler.ignoreOnContinue(myTemplateId);
* This tells the framework that the field is to be removed, and all related
validation messages should therefore be cleared:
> handler.remove(myCtrl, true);
(The second parameter, if `true`, removes the question's id entirely from the
system. If `false`, or not supplied it only removes the validation messages.)
* This entirely removes a particular validator, if that was an object with a `name` property:
> handler.removeValidator("myValidatorName");
Rendering Errors
================
All this validation is fine, but we haven't discussed how to show the user that
there are errors. The validation framework will handle showing this for you,
but you need to tell it what sort of rendering you want. This is handled by
one more of the constructor parameters to the FieldChangeHandler:
> var handler = new FieldChangeHandler({
> // ...
> renderers: [
> Renderers.Field(containerName),
> Renderers.Row(containerName),
> Renderers.TopMessage("error_driver")
> ],
> // ...
> });
Error Renderers
===============
There are existing Renderers (which really should be renamed to ErrorRenderers)
to handle common error-handling requirements:
* `Field`: highlights the actual field that is in error, by adding and removing
the CSS class `stratui-validation-warning`.
* `Row`: highlights the row in the table that contains the field in error, by
adding and removing the CSS class `errorRow`.
* `Label`: highlights the label matching the field in error, by adding and
removing the CSS class `invalid`.
* `TopMessage`: add and removes error messages at the top of the page.
Note that the CSS classes are inconsistent. That's also on the to-do list.
Creating your own Renderer
==========================
There is no magic to these Renderers. You can create your own, but the API is a
little more complex than for the Validators or the FieldChangeHandler's public
methods:
> var MyNewRenderer = {
> addError: function (error, validation,
> converter, overrideErrorMessage) //...
> clearErrors: function (ctrl, validation,
> converter) // ...
> };
Note that Renderers.Field, Renderers.Row, etc. are simply functions that return
objects with this API.
We won't discuss these parameters. If you need to write one, we'll discuss it
at another time.
Behind the Scenes
=================
That is all you need to know about the FieldChangeHandler. Most of the rest of
the framework is hidden behind the scenes. We just discussed the Renderers.
The Validators are just built-in versions of the Validators you can write as
needed. The other two components involved are the ErrorHandlers and the
QuestionManager. We'll discuss those next, but we also need to mention that
outside your own code, the other entry-point to the validation framework is the
PageManager which calls validation framework functions during:
* `unload`
* `rate`
* `issue`
* `refer`
ErrorHandler
============
The ErrorHandler is fairly simple. It's responsible for routing the messages
to the various Renderers. It's unlikely you'll need to write your own, but if
you do, you can supply it as an `errorHandler` property to the FieldChangeHandler
constructor. This is the public API of ErrorHandler:
> handle: function(error, validation, converter,
> overrideErrorMessage) {
> clear: function(ctrl, validation, converter);
> addMessage: function(msg, key);
> removeMessage: function(key);
> handleValidationErrors: function(errors, converter);
> handleExternalErrors: function(errors);
The default ErrorHandler is constructed using the containerName and your
Renderers.
Introduction to the QuestionManager
===================================
In some ways, the QuestionManager is the heart of the system. It knows about
the actual questions that were used to generate your HTML, your behavior rules,
and much else. You probably will not need to interact with it directly very
often, though. The QuestionManager maintains:
* the actual questions, in (nearly) complete form
* the mapping of questions to pages
* the built-in-validators for each question
* the mapping of full ids to the relevant question
* the data used to generate a full id from a template
* translator functions to convert ui formats to and from ACORD formats (for
example, "2010-02-08" <--> "02/08/2010".)
Usage of QuestionManager
========================
We discussed QuestionManger's `load` function earlier, and FieldChangeHandler's
`makeId` function is a thin wrapper around the one in QuestionManager. Although
that is likely your only direct use, you should know that FieldChangeHandler
behind the scenes is calling QuestionManager to do much of its work. It's also
one of the few classes really completely documented with the JSDoc Toolkit format,
which would be a nice segue to our third topic for the day. But before that,
we have the STRATUI Panel, which is the only other code making direct use of
QuestionManager.
Before we get to that though...
A Work in Progress
==================
It should be clear that this validation framework is still a work in progress.
Just yesterday we finished the part that handles one set of server errors. Today
we added the Label renderer.
There are still likely to be bugs, missing features, and API warts. If you see
something wrong, please speak up.