Multiple fields validation in AngularJS
Recently, I came across an interesing problem. String, representing a date, which has to be shown to the user as three dropdowns in the UI. Obviously, there has to be 2-way data binding between the string and the three dropdowns, which by the way, should also be validated.
Three dropdowns date problem
Let's break it down a bit.
Firstly, 2-way data binding requires data transformation on the fly. The string, our model value, needs to be transformed to three separate view values and vice versa.
Secondly, the validation. All three dropdowns should be validated as a whole. We are validating the combination of them rather than each of them separately. Here is what a valid combination should be like:
- All fields can be empty.
- All fields are required when at least one is filled.
- Certain combinations are not allowed. For example 30th of February is hardly ever a valid choice.
The elegant ng-way
How do we go about it? Let's start with what we know. First the model date.
$scope.modelDate = '1981/1/1';
Now the view. The view requires its own model - three values - for the dropdowns. There we go then:
$scope.viewDate = {
day: '1',
month: '1',
year: '1981'
};
Having the above in place, say:
<select ng-model="viewDate.day">...</select>
<select ng-model="viewDate.month">...</select>
<select ng-model="viewDate.year">...</select>
Now we are only missing the middle bit. We need three answers:
- How to validate the date?
- How to transfrom model value into view value?
- How to transform view value into model value?
As it turns out, the answer to all three is ngModel
controller. As it is explained in anguar developer guide about custom validation, it provides 2-way data transformers and allows for flagging up validation status. Perfect.
ngModel controller
Extremely useful in its own, ngModel
also allows other directives to feed on its controller, which in turn provides two simple and effective pipelines:
$modelValue
» $formatters[]
» $viewValue
» $render()
$modelValue
« $parsers[]
« $viewValue
« $setViewValue()
Any other directive sharing the ngModel
controller can add its own parsers, formatters and also define the $render()
method for displaying the view value. In our context, we want the dropdowns to manage displaying the viewDate
values on their own, because it's easier. We want however, to update viewDate
and that's what we are going to use $render()
for. In essence, we need two pipelines such as this:
modelDate
» middle-man-directive » viewDate
» dropdowns
modelDate
« middle-man-directive « viewDate
« dropdowns
We could call our middle-man-directive dateTypeMulti
and imagine using it like so:
<input type="hidden" ng-model="modelDate" date-type-multi="viewDate" />
The dataTypeMulti
directive could have a link method similar to the following:
function (scope, elem, attrs, ngModel) {
// Update viewDate, whenever there was a change to $viewValue.
// The dropdowns will pick it up themselves, as they have
// their own ngModel directives on them.
ngModel.$render = function () {
var value = scope.$eval(attrs.dateTypeMulti);
angular.extend(value, ngModel.$viewValue);
}
// Ensure we capture changes to viewDate and translate them
// into changes to $viewValue.
scope.$watch(attrs.dataTypeMulti, function (value) {
ngModel.$setViewValue(value);
}, true);
// Transform $modelValue into $viewValue on each change.
ngModel.$formatters.push(function (modelValue) {
// Boring stuff - convert modelDate (string)
// into viewDate (object).
});
// Transform $viewValue into $modelValue on each change.
ngModel.$parsers.unshift(function (viewValue) {
// More boring stuff - convert viewDate (object)
// into modelDate (string).
});
}
And that's it, conceptually. Feel free to have a look at full code example.
Have fun,
Jacek