<accordion></accordion>, or a custom attribute
<div accordion></div>, or a CSS class
<div class="accordion"></div> (and even as HTML comment).
In this tutorial we’ll go through the creation of a custom stepper directive that can be used as a reusable input component in your applications. We’ll cover the classic directive creation but also the input validation, and the use of the ngModelController, that will allow a seamless integration with any form, leveraging the existing AngularJS forms superpowers. The next part will cover the test suites with Jasmine and KarmaJS, and the publication and distribution of our widget with GitHub and bower.
For this example we’ll build a custom numeric input widget, named “rn-stepper”. We’ll use the last AngularJS 1.2 that brings some important fixes to the private scopes management (capital point for reusable components). The full widget code is available on github as a reusable component and you can see the final result here :
The first step is to create a naïve directive that build our markup, and renders correctly. We just declare the directive name, and template to use.
1 2 3 4 5 6 7 8 9 10 11 12 13
Now, to use our directive, its quite straightforward :
- declare our
revolunet.steppermodule as one of our app dependencies
<div rn-stepper></div>(attribute form) or simply
<rn-stepper></rn-stepper>(element form). to integrate the directive somewhere.
The attribute form is better if you want to support IE8 as it works out-of-the-box.
Add internal behaviour
Now we need to add behaviour and internal variables to our custom component. We’ll declare a “private scope” that will hold internal variables and functions, and add the
link function to our directive, which is responsible of initialising the component behaviour just after the markup has been inserted in the final page.
Here’s the updated directive code :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
We now have a functionnal component with an isolated code and template.
Communicate with the external world
Our component works great but it would be more useful if it could control a real public variable, known as a
ngModel in AngularJS.
Let’s add a databinding between our component internal
value and the outer world (our application).
We just need to update our scope declaration like this :
1 2 3
This will automagically bind our internal
value variable to the external one declared in the
ngModel attribute. The
= means “double data-binding” which means if ngModel is updated externally then the internal
value will be updated, and vice-versa.
Say my app expose a
rating variable, we could now bind it to our component simply like this :
Make our component form-friendly
We now have a dynamic input that can manipulate arbitrary ngModel data. We need to modify a bit the code to make it play nice with the AngularJS forms. For example, AngularJS forms and input generally expose a
$dirty state which can be useful in many situation. To make the forms aware of our model changes from inside our component, we need to make use of the
ngModelController.$setViewValue API methods, which are available as soon as you “require” a ngModel on your directive.
ngModelController.$render method is a method which you should override yourself in the directive and is responsible of updating the view; it will be called by the framework when the external ngModel changes. When the model changes, the framework executes the
$formatters pipeline which is responsible of eventually converting the
$modelValue raw value to a usable
For example, if your model is a real Date object, you’d want your input to display it as dd/mm/YY. The model-to-view conversion is made by the
$formatters pipeline and the view-to-model by the
$parsers pipeline. Once you get a ngModelController instance, you can easily insert new items in these pipelines.
ngModelController.$setViewValue method should always be called when you want update a model from your directive (view). It takes care of calling the eventual
$parsers pipeline. Then it applies the final value to the internal $modelValue, update the input $dirty state, update the optional parent form $dirty state and call any registered
$viewChangeListeners. Here’s the full code for this function.
As pointed by a comment from @ThomasBelin4 below, we don’t need anymore to have a scope
value variable, as we now have a reference to the original
ngModelController which holds a reference to the viewValue.
Here’s how we update the directive declaration :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
Add min/max attributes
Now our component is form-friendly, so let’s as some builtin validation rules.
We could add optional min/max attributes to our component, which will handle the form validation by himself when they are present. These attributes will be data-bound so they can be updated at any time by the application (some other inputs in a form may impact the min/max here).
ngModelController API gives us also a
$setValidity method that can inform the parent forms about our component validity, and automatically add some handy CSS classes related to validity to out form and inputs.
We just need to call
ngModelController.$setValidity('outOfBounds', false) to make our input, and thus parent forms invalids, and have
ng-invalid-out-of-bound CSS classes added to our forms and to our component.
Our stepper component is now full functionnal and integrates seamlessly in any form.
Prevent invalid input
Another nice-to-have feature would be to prevent the user from entering invalid data, which means disabling the buttons when the internal value reach the min/max limits. This could be achieved in two ways :
- BAD : manually in our link function, toggling our buttons states on each click.
- GOOD : automagically, using a builtin
ng-disableddirective in our template, that will disable the buttons under some conditions.
The second option is much more Angular-ish and there are several ways to achieve this so let’s see how we can do.
We can add
ng-disabled="isOverMin()" to our first button template and add a
scope.isOverMin function that returns a boolean indicating if we should disable or not the given button. same with
overMax that would check if the max has been reached or not.
Our template is now :
1 2 3
The next part will detail the tests suite and distribution subjects over github and bower.
Stay tuned :) and feel free to comment/ask below !