I've been learning the AngularJs framework recently and have come across a case study that highlights how Isolate Scope works.
The app we're developing needs two individual sliders. The values represented by each of the sliders are related as they form two variables in a simple equation: the Loan-to-value ratio of a mortgage (LTV). The LTV is a percentage of the Loan Required figure over the Property's Value figure. There is a slider for both the Loan Required and Property Value.
Currently, mortgage lenders are reluctant to offer mortgages with an LTV over 95%. We've therefore chosen to introduce this feature into our sliders, so that they may not be dragged to a position in which the LTV ration is greater than 100%. If a slider is dragged beyond the 100% mark, the other slider must be moved accordingly so as to maintain the ratio at 100%. Challenge accepted.
This seemed like a good fit for a Directive. A Directive in AngularJs is a way of describing a portion of the page. Using a directive to construct your page is what's known as being declarative. Your intentions for the purpose of that piece of mark up is clear. A Directive would be something that you might use often, so as to provide yourself with a short hand way of getting the element on the page.
We intend to create a 'Linked Slider' Directive, that is capable of communicating with other slider directives, so that their values may stay in-sync. To do this we must make use of Isolate Scope. Scope is very important in AngularJs. It is the 'glue' that allows controllers and views to share information. The Angular Magic permits us to manipulate the same values in both the controller and the view - its the Scope that keeps the values consistent across both, thus delivering a huge amount of value for developers.
Scope is normally associated with a Controller and a View. Directives are declared on the View and share the same Scope as the Controller and View. Isolate Scope enables the Directive to maintain its own Scope. This is useful for us for two reasons. Firstly, it gives us the ability to 'wire up' our directive to our outer Scope without tightly coupling it - making it reusable elsewhere. Secondly, it allows us to have multiple instances of the same slider Directive.
You might enjoy the working example to get a feel for what I'm talking about. The code for the directive is shown below, followed with discussion..
Starting from line 1, the directive is declared on the app global variable, and named as 'linkedSlider'. It is restricted for us as an attribute (A), and then has its Scope defined.
The Scope property is the Isolate Scope I was banging on about. There is a convention at play here. The keys of the scope property become attributes that can be used to configure the Directive when it is declared in the mark up. The '=', '@' and '&' values of these keys have special meaning.
'=' means the value is a two-way binding property. This means that its value can be changed by the Directive and by the Controller. This is why we use the '=' symbol to describe both the Value and Link properties. The Value property is the value that the slider is going to maintain. The Link property is the value that the slider is going to have to update in light of changes to its own Value when the LTV is pushed over 100%. It is this arrangement that lets us declare the property once and but be able to use it as many sliders as we like.
'@' is a straight forward attribute. It does not stay up to date in the Directive if it is changed in the Controller's scope. For this reason attributes are normally populated with string literals or numeric values. In our case, we specify all the necessary properties of the slider (max, min & step).
'&' represents an expression to be evaluated. It could be a pointer to a function on the Scope, or anything that can be executed. I have used this type of scope property to contain the test to be checked each time the slider is moved, to determine whether or not the other slider needs to be adjusted.
The Link function that follows is what binds the directive to the controller and kicks everything off. You will notice still the reliance on jQuery to create an instance of the jQuery UI slider. Nothing unusual there. As you can see I pass in the necessary configuration from the Isolate Scope of the instance of the Directive. When the slide event occurs, I have to call the special function scope.$apply() for the value change to be recognised in the Controller Scope. I then evaluate the Scope's condition property (remember that we defined it as an expression with '&') to find out if the new value would cause the LTV to exceed 100%. If so, I use the scope.$apply function again to update the Link property value in the Controller Scope.
As we have just seen, we have given our Directive the ability to change the value of it's Link property. This Link property will be a Value property in another instance of the same Directive, so we need to be able to listen for changes. scope.$watch is just the thing we need to be able to react to changes to our Value in the Controller Scope. When a change occurs, we use the jQuery UI slider API to update our slider's value, this also takes care of dragging the slider to the correct position.
There still remains two unexplained features for the moment. I do not understand why a setTimeout is required. Without it, the attribute ('@') values are not yet populated at the time of initialising the JQUI Slider. Also, I am also required to apply a scope.$watch to the Link value. Again, without this the Link property value will not stay up to date, even though there is no actual function content. A small price to pay for what is otherwise an elegant (enough) solution.