Adding custom HTML attributes to your AngularJS web app
AngularJS is an excellent JavaScript web framework offering so-called "directives" to 'teach' HTML some new tricks. Examples of built-in AngularJS directives are:
- "ngView": defines the placeholder for rending views
- "ngModel": binds scope properties to "input", "select" and "text" elements
- "ngShow" / "ngDisabled": for showing or disabling an element based on the result of an expressions
Most directives are manifested as custom HTML attributes, but alternatively could be specified using element names, using the "class" attribute and HTML comments. I would suggest to always use directives through HTML attributess since:
- All directives (that I'm aware of) will support this.
- Custom elements (like "
") are not supported out of the box in Internet Explorer - None of the standard directives seem to support usage through HTML comments (which is kind of a pity, since it would be great for "ngRepeat")
- The HTML "class" attribute should not be abused for presentation logic
Each directive should have a "lower camel cased" name and by convention should start with a single word that specified a namespace. AngularJS uses the "ng" namespace and offers directives like "ngBind", "ngController" and "ngRepeat". However AngularJS isn't actually using this name in HTML (i.e. as an attribute name) but instead uses "snake cased" names with the possible special characters :
, -
or _
. Additionally when using the "-" character you can optionally prefix with either "x-" or "data-" to make it HTML validator compliant (i.e. will remove the HTML warnings in Eclipse). This means that the "ngModel" directive can be manifested either as "ng:model", "ng-model", "ng_model", "x-ng-model" or "data-ng-model". I would suggest using the "-" character as it appears common convention for AngularJS applications and allow prefixing using "x-" or "data- to prevent HTML warnings. In pre-1.0 versions of AngularJS the ":" notation was the only supported one. When you find such a notation in example code or articles it's a good indication that it might be out-dated since it's was written for a pre-1.0 version of AngularJS. In the rest of this article we will create a custom directive called "jdMask" step by step. This directive enables "masked" input on text fields and is more advanced than the "mask" directive of AngularUI. First we will declare the skeleton implementation for our directive:
angular.module('jdriven.directives', [])
.directive('jdMask', function() {
return {
require: 'ngModel',
link: function(scope, element, attrs, controller) {
//TODO: implementation logic will have to be written here
}
};
});
The above code defines a directive named "jdMask" which:
- is restricted for usage through HTML attributes (which is the default in AngularJS)
- requires the existence of "ngModel" directive on the same element
- will "link" features to an existing HTML element
The "link" function comes with 4 arguments:
- The relevant scope (the AngularJS implementation of a "view model"); a directive will at default use the same scope as its element.
- The element; which is already wrapped in a jqLite / jQuery collection
- The attrs object containing the name-value pairs of all the attributes declared on element. Instead of the actual HTML attribute names the names of directives will be normalized in its "lower camel case" name.
- The controller of this element; isn't actually the same a regular AngularJS controller since it's specific for this element. It's actually an instance of the "NgModelController" type (which is extensively documented in the AngularJS "API Reference") which supports retrieving and setting the view value, changing the validity the (form) field and registering parsers and renderers.
Before coding the logic of the directive we need to add <script src="_<js-file-url>_"></script>
elements for the following JavaScript dependencies:
- jQuery: prerequisite for the masked input jQuery add-on
- digitalBrush Masked Input Plugin for jQuery
No we can add the HTML code that:
- Initializes $scope['date'] with "31122012" (31-dec-2012)
- Binds the "date" element of $scope of a text field
- Attaches the masked input directive to the same text field
- Shows the actual value of the "date" model element in $scope
<div x-ng-init="date='31122012'">
<!-- text field for entering a date in "dd/mm/yyyy" format -->
<input type="text" x-ng-model="date" x-jd-mask="99/99/9999" />
Model value: { {date}}
</div>
Without any logic in the "link" function, the value of "31122012" will simply be shown as "31122012" inside the text field. To fix this we will need to (re)assign a $render function to the controller supplied to the "link" function. To render the model value using the "Masked Input Plugin" we will need to implement the $render function as follows:
controller.$render = function() {
var value = controller.$viewValue || '';
element.val(value);
element.mask(attrs.jdMask);
};
To render the value using a mask input it:
- First retrieves the actual view value; or use '' in case none is set
- Then the actual value (without the mask) is set for the element
- Finally the mask is set on the element (i.e. "99/99/9999") and the earlier set value is now be displayed using it.
To actually change the view value when text is entered (or deleted) from text field we will have to manually handle the "keyup" events for the element. Normally AngularJS automatically updates the model value for us, but in this case the "Masked Input Plugin" appears to block these automatic model updates. To restore the model updates we will need to bind to "keyup" and set the view value with the text entered in the mask (as returned by "element.mask()"). Since we "keyup" handler is invoked from outside AngularJS we will need to wrap it into a "scope.$apply" invocation.
element.bind('keyup', function() {
scope.$apply(function() {
controller.$setViewValue(element.mask());
});
});
Note that we didn't actually set the model view in the "keyup" handler but instead we set the so-called "view value". To transform the view value into the actual model value the controller (of type NgModelController) contains a "$parsers" array. To register your own parser simple push a new single-argument function responsible for returning a parsed view value. Additionally a parser function can also change the validity of a field to indicate errors in the user interface. In order to prevent invalid values to be set as a model value it's good practice to return a null value in case the value is invalid.
controller.$parsers.push(function(value) {
...
var validValue = ...;
controller.$setValidity('jdMask', validValue);
return validValue ? value : null;
});
The directive is now fully functional. In most cases a directive won't be need custom teardown logic since AngularJS will automatically unbind all events and remove any data attached to the element. In case you do need to perform additional teardown you can listen on the "$destroy" event of the scope by adding the following inside your "link" method:
scope.$on('$destroy', function() {
// advanced tear down logic goes here
});
So guess that all... make sure to check of the source-code on the directive on "plnkr.co".