Most likely, you'll find that ngModel is the most common controller you'll require within your code. Any time you want to create a custom input, or even just bind an input plugin that requires specific formatting, ngModel provides the methods you'll need to coordinate communication between the plugin and your data model.
Controllers – Better with Sharing
[ 54 ]
With that in mind, let's walk through the process of creating an input for time values that utilizes the handy timepicker jQuery plugin provided by Jon Thornton. Our goal is to be able to turn a regular text field input into a timepicker that displays its value in the format HH:mm but stores it in our data property in milliseconds.
For this example, our HTML is once again pretty simple, as shown here:
<input type="text" ng-model="timeOfDay" time-picker />
Obviously, we'll also need to include the timepicker plugin within our main page so that it can be attached to our input, so if you're recreating the code on your own, be sure to do that before continuing on. For our directive, let's start with the basic definition object, and since we know we need data-binding functionality, we'll require ngModel from the start so that we can utilize its methods.
.directive('timePicker', function () {
var today = new Date(new Date().toDateString());
return {
require : '?ngModel',
link : function ($scope, $element, $attrs, ngModel) { }
} });
Note that since we need to be working with the actual instance of the element, almost all our code is going to sit inside the linking function, and we don't need to worry about the compilation process at all. We've also initialized a today variable that holds a Date object set to this morning at midnight. Creating the variable as part of the factory function allows us to just have one today variable that's shared across all instances of our time-picker directive. Be careful, however, as even though this allows us to minimize memory usage, it does mean that if our app is left open overnight, our directive will start providing inaccurate results. If you're planning on using this directive in a live application you'll want to create a secondary function that updates this value once tomorrow comes. Let's move forward now and grab our controller:
link : function ($scope, $element, $attrs, ngModel) { ngModel = ngModel || {
"$setViewValue" : angular.noop }
}
Chapter 6
[ 51 ]
You may have noticed this pattern before, as a part of the form and ngModel
controllers that we looked at previously. Remember that we've made our controller requirement optional, so that if someone wants to use our directive just to attach a timepicker, but doesn't need the data-binding offered by ngModel, our directive won't throw an error when it doesn't find the requested controller. We could just use conditional statements to verify that ngModel is defined each time we need to use it, however the developers at Angular use this pattern, and I recommend it, in order to help keep the directive code a little cleaner. All we're doing here is saying that if ngModel is defined and has a true value, use that. If not, define it as an object with all of the requisite method names set to a no-op function (angular.
noop is a convenience method provided for exactly this purpose). Now when we call ngModel.$setViewValue later in our directive, if there's no ngModel directive attached to our node, our code will continue along without an issue.
Speaking of ngModel.$setViewValue, let's take a look at how we'll attach our timepicker and where we might need that very function. If you've been pining away for a jQuery plugin while reading this book, now is the time for a brief moment of relief:
link : function ($scope, $element, $attrs, ngModel) { …
var initialized = false;
setTimeout(function () {
initialized = $element.timepicker() .on('changeTime', function (ev, ui) {
var sec = $element.timepicker('getSecondsFromMidnight') ngModel.$setViewValue(sec * 1000);
});
});
}
Undoubtedly, your first question is going to be about setTimeout, particularly one with no actual timeout. Because we're in the linking function, our $element is fully instantiated, so this sort of trickery shouldn't be necessary. And you're right, it isn't necessary. It is, however, a practice I recommend, for two primary reasons. First, on occasion, particularly if your directive or another on your element is applying a template, Angular and jQuery will both try to apply themselves at the same time and you run into a race condition. While this is rare, and usually means that your plugin isn't actually working on the $element itself, but trying to clone it or nest something inside, it still can cause a few headaches and this helps guard against that.
Controllers – Better with Sharing
[ 56 ]
Secondly, and more importantly, is that when you begin to develop larger
applications and have hundreds or even thousands of different directive instances all manipulating and binding to their own elements in various ways, any plugin that requires DOM manipulation tends to slow things down. And often, especially for input type plugins that are hidden until the user directly interacts with them, these plugins can wait a few milliseconds to initialize themselves without harming the user experience. Wrapping our initialization process within setTimeout tells the JavaScript interpreter to process this after it's done with the current task, so the compilation process doesn't get delayed by our jQuery plugin attachment. Again, this isn't a necessity, but it is a practice I recommend you consider as you begin to develop larger applications with Angular.
Now that we've discussed that, let's take a look at how we're using $setViewValue. Because we've grabbed a shared instance of our ngModel controller, we can call the controller's $setViewValue function from our own directive, which helps us connect our plugin to the data model. Remember that this is used to take the display value, perform any necessary parsing, and then store it in the data property. The timepicker plugin emits a changeTime event anytime the user updates the time value displayed in our input, so we use that to know when we need to change our internal value.
Within our event handler, all we have to do is get the number of seconds since midnight, which the plugin provides a convenience method for, then multiply it by a thousand and pass that into $setViewValue. Once we're done, our data will travel through the following process:
The ngModel controller takes in the new value and updates the data
Once we have our timepicker initialized and listening for changes in the view, our next step is to define the $render method, which is responsible for converting a data value to the appropriate display or view value. This will be called any time the data value changes from a source outside our directive, including when it's first initialized and can be defined as follows:
link : function ($scope, $element, $attrs, ngModel) { …
ngModel.$render = function (val) { if (!initialized) {
Chapter 6
[ 51 ]
//If $render gets called before our timepicker plugin is ready, just return
return;
};
$element.timepicker('setTime', new Date(today.getTime() + val));
} }
Again, note that we're actually redefining the $render method of our shared
ngModelController function, so when the ngModel directive observes a data change and tells the controller to execute $render, it's our function that gets called. All we have to do is know how to transform the data-model value into a value our plugin expects. In this case, the timepicker plugin provides a method for setting the time displayed by passing in a Date object with the specified time. Because our values are stored in milliseconds since midnight, when we need to render a value, we simply take the time from today, add on our new value, and create a Date object with that value. Again, when we're finished, our data will flow back into the view via the following process:
Timepicker updates element and displays
2:58PM Our $render function
adds val to today’s time and passes it to ‘setTime’
ngModel Directive observes change and
calls $render with val = 53907764
Summary
As you have hopefully seen, controllers provide a powerful mechanism for connecting two or more otherwise disconnected directives. This means that each directive can provide its own functionality, such as the simple timepicker plugin, but also extend its functionality if another directive is present. This targeted communication helps us make calls and interact with other pieces of an application directly without the need for the general broadcast approach provided by messaging. For the next chapter, we are moving on to a different topic entirely, transclusion, which allows you to utilize the existing content of an element when applying your directive.
Transclusion
Up to this point, we've primarily focused on one side of directives, which is how they can affect the elements to which they are attached by replacing the HTML via a template, binding additional plugins, or some combination of the two. In this chapter, we're going to look at the opposite side and see how transclusion allows for the original element's content to impact the behavior of the directive. While this methodology is certainly less common, it still is a powerful tool to be aware of and one you should call to mind when you find yourself tempted to create multiple directives that only vary from one another in content. With that, let's dive in and check out some examples.
That's not a word...
True. Looking up transclude in the dictionary won't help you understand what's really happening here (a fact lamented in the comment section on Angular's documentation site). I believe, however, that a brief dive into why they created this word in the first place will actually help greatly in your understanding of what its real purpose is, so bear with me a moment while we step aside from the focused realm of JavaScript and breathe deeply in the wider world of computer science.
If you've ever had the privilege, or arduous task, of creating a templating syntax, along with the parser required to bring that syntax to life, you're likely familiar with the concept of inclusion. Within a template, various snippets of code will often be repeated. In HTML, this is commonly seen with headers, footers, a Twitter widget, and so forth. And in the continuous quest for Don't-Repeat-Yourself code, we usually build 'include' commands into our templating definitions that allow you to write your snippet of code once and then drop it into other parts of your template wherever you want.
Transclusion
[ 60 ]
That part of templating, pretty much everyone agrees upon. There remains a question, however, which is: if you're including a snippet that itself has dynamic variables and needs to be parsed, what scope do you use when parsing it? Some widgets, such as the Twitter widget mentioned in the preceding paragraph, benefit from being parsed all by themselves in an isolated scope, and then just having the compiled result inserted at the include tag. Other widgets, such as a customized blog post header or dynamic list display, however, need to be parsed within the original scope of the include tag, not outside of it. Most mature templating syntaxes have ways of performing both types of inclusion, but it's a problem that each syntax designer must answer in their own way.
...it is a solution
For Angular that answer is transclusion. My unofficial interpretation of the word is translated-inclusion. What transclusion does is offer a way to create a widget with an isolate scope, which we as good modular developers always do, but then tunnel back out into the parent scope to parse the original content. This, of course, is significantly clearer in an example, so let's check one out. Take a look at the following HTML and note what's different from our previous examples:
<div>
<input type="text" ng-model="name" />
<select ng-model="movie">
<option value="Man Of Steel">Superman</option>
<option value="A New Hope">Star Wars</option>
</select>
<input type="number" ng-model="friendCount" />
</div>
<div movie-info="movie">
<p>Hi, I'm {{name}}, and I'm going to see {{movie}} with {{friendCount}} friends</p>
</div>
Ok, so what do you notice? Hopefully, one of the main things you saw is that our directive, movie-info, isn't just an empty node this time. It has child nodes. Before we get too much farther into how exactly that impacts our development, however, let's take a look at the directive definition as well:
directive('movieInfo', function () { return {
template : '<div class="movie-info">' + '<h1 class="movie-title">{{name}}</h1>' +
'<img class="movie-poster" ng-src="posters/{{name}}.jpg" />' + '<div ng-transclude></div>' +
'</div>',
Chapter 7
Now I should ask again, what do you notice? Perhaps most notably, we have a scoping issue. On the one hand, we're adhering to our principle of modularity and isolating our scope. On the other hand, however, we've ignorantly introduced a naming conflict. Both scopes use the name property, and even though our scope is isolated, the origin element content we've pulled in is now inside our directive element, so surely it's going to be parsed against the directive scope, which isn't the name property we want.
Enter transclusion, stage left. Remember, we said that transclusion stands for translated-inclusion, which means that first we parse it and then we include it. In the official Angular documentation, they explain this by saying that the transcluded scope and the isolate scope are siblings. The transcluded scope inherits from the parent scope per normal, and the isolate scope, though still a child of the parent scope, is otherwise disconnected.
That undoubtedly sounds fascinating, but perhaps a bit obtuse. Let's take a look then, at what our HTML will look like after everything is parsed and compiled, taking note of the highlighted values:
<div>
<input type="text" ng-model="name" /><!-- value: "Alex" -->
<select ng-model="movie">
<option value="Superman">Man of Steel</option><!-- selected -->
<option value="Star Wars">A New Hope</option>
</select>
<input type="number" ng-model="friendCount" /><!-- value: 3 -->
</div>
<div movie-info="movie">
<div class="movie-info">
<!-- these two lines parse against the directive scope -->
<h1 class="movie-title">Superman</h1>
<img class="movie-poster" src="posters/Superman.jpg" />
<div ng-transclude>
Transclusion
[ 62 ]
As this hopefully makes clearer, we've effectively created a tunnel to the parent scope, hidden from the directive's scope and yet fully accessible to the transcluded portion nested inside. And, of course, all these data values are dynamic, so if the user selects a different movie, all the instances of Superman will change to Star Wars, whether they're bound to the movie property of the parent scope or the name property of the directive scope. Likewise, changing the parent scope's name property will only affect the value within our transcluded element, the directive scope will remain ignorant of its existence entirely.