Making Custom Field Validations in Formulate
Formulate comes with two built-in field validations:
Mandatory
Regular Expression
If you have more custom needs, Formulate is extensible to allow for you to add your own types of field validations. To understand how to accomplish this, we’ll create a validation that sums the values in a list to ensure they total to a configurable value.
Disclaimer
Note that this article will only explain how to create validations with the plain JavaScript template that comes with Formulate. It will not explain how to create validations with the AngularJS template that also comes with Formulate, as the plain JavaScript template is the preferred option going forward. Most of the code is the same, but the client-side JavaScript is a bit different.
List Summation Validation
Suppose that you’re a math teacher and you’ve decided to use Formulate to administer a test to your students online. One way of going about that would be to create a form with validations so that students get instant feedback so they know whether or not their answers are correct.
To this end, you’ll be creating a field validation that checks a list of values to see whether or not they add up to a configured value. For example, suppose the configured value is “5”. The following list would add up to a value of 5, and so it would pass the validation: 1, 2, 2. The following list would add up to a value of 6, so it would not pass the validation: 1, 2, 3.
We’ll create this validation and we’ll call it the “list summation validation”.
Validation Components
There are three parts that go into creating a validation:
The Validation UI
The Server Side Validation
The Client Side Validation
The validation UI is what a user sees in the back office when they create an instance of your validation. This will show things common to all validations, such as the validation name, in addition to any configuration that your validation requires (e.g., the value that the list should total to).
The server side validation is the C# code that validates the field value on the server side after the form has been submitted. It’s a good idea to validate values on the server side to avoid anybody bypassing the validation on the client side (e.g., a clever student who considers themselves too busy to actually solve the questions).
The client side validation is the JavaScript that validates the field value on the site visitor’s browser. Client side validation is useful to provide instant feedback to let users know whether or not their values are correct (i.e., without having to wait for the form to post to the server and for the server to return whether or not the values passed all the validations).
The Validation UI
When building any validation with Formulate, the first thing you must do is create a class that implements the IValidationKind interface:
using formulate.app.Validations; using formulate.core.Types; using System; using System.Collections.Generic; public class ListSummationValidation : IValidationKind { public Guid Id => Guid.ParseExact("EF0C56C430934ACE954304AC6C2E6407", "N"); public string Name => "List Summation"; public string Directive => "list-summation-validation"; public object DeserializeConfiguration(string configuration, ValidationContext context) => null; public bool IsValueValid(IEnumerable<string> dataValues, IEnumerable<FileFieldSubmission> fileValues, object configuration) => true; }
This is just a placeholder implementation for now to get the concepts across. Here is the purpose of each component of this interface:
Id The ID is a GUID that identifies this validation type, which helps when serializing and deserializing (i.e., to identify the validation type by the GUID). It doesn’t matter so much what this GUID is so long as it is unique (i.e., it should not match any other Formulate GUID’s). If you don’t know how to generate a GUID in Visual Studio, you can generate one with https://www.guidgenerator.com/.
Name The name is what is shown to the user in the back office when they’re creating a validation and are presented with a list of each validation type.
Directive This is the AngularJS directive that will be used to display the UI in the back office when a user is editing a validation (e.g., this is where they edit the configuration values for the validation).
DeserializeConfiguration This method converts a string into some C# class that will be passed around as the configuration for this validation.
IsValueValid This is the method that checks if the supplied value meets the validation criteria. We’ll get to this later when we dive into the server side validation. For now, we’re just going to return true.
For the purposes of creating the validation UI, we’ll focus primarily on creating the AngularJS directive that is displayed in the back office. Here’s what we’ll be building:
First, we’ll need to create the JavaScript file for the AngularJS directive at ~/App_Plugins/Validations/list-summation-validation.js:
function directive() { return { restrict: "E", replace: true, templateUrl: "/App_Plugins/Validations/list-summation-validation.html", scope: { data: "=" } }; } angular.module("umbraco").directive("listSummationValidation", directive);
In order to ensure the JavaScript is loaded in the back office, we’ll need to create a package.manifest file at ~/App_Plugins/Validations/package.manifest:
{ "javascript": [ "~/App_Plugins/Validations/list-summation-validation.js" ] }
Finally, you’ll create the markup for the AngularJS directive at ~/App_Plugins/Validations/list-summation-validation.html:
<div class="control-group umb-control-group"> <div class="umb-el-wrap"> <label class="control-label"> Total Value </label> <div class="controls controls-row"> <input ng-model="data.totalValue" placeholder="Total Value" class="form-control input-block-level" /> </div> </div> <div class="umb-el-wrap"> <label class="control-label"> <localize key="formulate-labels_Error Message"> Error Message </localize> </label> <div class="controls controls-row"> <input ng-model="data.message" formulate-localize-attribute="placeholder" placeholder="formulate-placeholders_Error Message" class="form-control input-block-level" /> </div> </div> </div>
And with that, you’ve created the back office UI for your validation.
The Server Side Validation
Before we implement server side validation, we’ll have to deserialize the configuration from the back office validation UI. This deserialized configuration will give us the values we need in order to give some extra context to the validation process. This includes the validation message along with the total value that the list of values should sum to.
First, you’ll want to create a class to store your configuration:
public class ListSummationConfiguration { public int TotalValue { get; set; } public string Message { get; set; } }
Next, you’ll need to deserialize the configuration from a string to your class. Here’s one way you might go about that in ListSummationValidation.DeserializeConfiguration:
public class ListSummationValidation : IValidationKind { public object DeserializeConfiguration(string configuration, ValidationContext context) { var config = new ListSummationConfiguration(); var configData = JsonConvert.DeserializeObject<JObject>(configuration); var dynamicConfig = configData as dynamic; var properties = configData.Properties().Select(x => x.Name); var propertySet = new HashSet<string>(properties); if (propertySet.Contains("totalValue")) { config.TotalValue = int.Parse(dynamicConfig.totalValue.Value as string); } if (propertySet.Contains("message")) { config.Message = dynamicConfig.message.Value as string; } return config; } }
You’ll need to add a few extra namespaces for the above code to work:
using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System.Linq;
Now that you’ve got the configuration, you can start working on the server side validation. You can do that by implementing ListSummationValidation.IsValueValid:
public class ListSummationValidation : IValidationKind { public bool IsValueValid(IEnumerable<string> dataValues, IEnumerable<FileFieldSubmission> fileValues, object configuration) { var config = configuration as ListSummationConfiguration; var value = config.TotalValue; if (!dataValues.Any()) { return false; } foreach (var dataValue in dataValues) { if (string.IsNullOrWhiteSpace(dataValue)) { return false; } var parts = dataValue.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries); try { var numbers = parts.Select(x => int.Parse(x)); var sumTotal = numbers.Sum(); if (sumTotal != value) { return false; } } catch { return false; } } return true; } }
And that’s all there is to server side validation.
The Client Side Validation
The first thing you’ll need to do when implementing client-side validation is to modify the ~/Views/Partials/Formulate/Responsive.Plain JavaScript.cshtml file. This file formats the data in a way that the frontend JavaScript can understand. You’ll need to modify it to add support for your new type of validation. In particular, you’ll need to modify the code that constructs the data for validations to add your own validation data to it. That would look like this (some code removed for brevity):
// A function that returns a validation configuration. var getValidationConfig = new Func<ValidationDefinition, object>(x => { if (x.ValidationType == typeof(ValidationRegex)) { // ... } else if (x.ValidationType == typeof(ValidationMandatory)) { // ... } else if (x.ValidationType == typeof(ListSummationValidation)) { var config = x.Configuration as ListSummationConfiguration; return new { message = config.Message, totalValue = config.TotalValue }; } return new { }; });
You’ve now added the “message” and “totalValue” as the configuration that will be made available to the frontend JavaScript when using this type of validation. Next up, you’ll create the client-side validation JavaScript. Start by creating a file ~/App_Plugins/Validations/list-summation-validation.js and include it into your markup with a script tag:
<script src="~/App_Plugins/Validations/list-summation-validator.js"></script>
Note that this script tag should be included before you include the rest of Formulate’s JavaScript. You can also include this script using require statements if you write code using modular JavaScript. Finally, you’ll add some JavaScript to this file to implement your client-side validation:
function isValueValid(value, totalValue) { var items; try { items = JSON.parse("[" + value + "]"); if (!items || !items.length) { return false; } } catch(ex) { return false; } return totalValue === items.reduce(function (x, y) { return x + y; }); } function ListSummationValidator(configuration) { this.totalValue = configuration.totalValue; } ListSummationValidator.prototype.validateText = function (value) { var self = this; return new Promise(function (resolve) { resolve(isValueValid(value, self.totalValue)); }); }; function validateAsInvalid () { return new Promise(function (resolve) { resolve(false); }); } ListSummationValidator.prototype.validateBool = validateAsInvalid; ListSummationValidator.prototype.validateTextArray = validateAsInvalid; ListSummationValidator.prototype.validateFile = validateAsInvalid; var key = "formulate-plain-js-validators"; window[key] = window[key] || []; window[key].push({ key: "ListSummationValidation", validator: ListSummationValidator });
In this instance, you only care about validating text fields, so you are marking any other type of field (bool, text array, or file) as invalid. Because you have used a key of “ListSummationValidation” (the same as your C# class), the plain JavaScript Formulate template will use this code whenever it performs a validation on a field that has this type of validation selected (e.g., when the user attempts to submit the form).
One thing you might find curious is that the “validateText” function is returning a promise. This is in order to allow for validations to be asynchronous, which could come in handy, for example, when you need to make a network call (e.g., if you created a validation that hits a web service to check if an email address is known to be a good one).
If you want to support Internet Explorer, you may need to include a promise library (promises are built into other modern browsers). Formulate internally uses a promise library, but it doesn’t expose that library outside of Formulate, as you can see here. If you Google “JavaScript promise polyfill”, you will see several options for promise libraries you can make use of.
Final Result
Let’s take a look at what we accomplished: