• 沒有找到結果。

Peeking under the covers

在文檔中 AngularJS Directives (頁 40-47)

Let's first take a quick look at what Angular actually does with the functions it receives from the compile and link properties, and how they're used during the full compilation process. The first thing to know is that both returning a function from our directive factory (instead of using the definition object) and defining a function on the link parameter are really just shortcuts to setting the compile property to be a function that returns that same linking function. In other words, no matter how we define our link function, by the time Angular gets around to processing our directive, it will always call the compile function and take what it returns as the linking function(s).

For proof, here are few lines of code that make that initial call:

...

linkFn = directive.compile($compileNode, templateAttrs, childTranscludeFn);

if (isFunction(linkFn)) { addLinkFns(null, linkFn);

//Only attach to 'postLink' functions }else if (linkFn) {

addLinkFns(linkFn.pre, linkFn.post);

}

Compile versus Link

[ 30 ]

There are a couple of important things to note from this code before we move on.

First, compile is called really early on during this whole process. $compileNode represents the element we're manipulating, but it's not anywhere in the DOM just yet, instead it's much like what you would receive if you called jQuery(<div>). What this means is while we can manipulate it within the compile function (and if we need to conditionally add other directives or change the pre-parsed HTML, this is where we have to manipulate it), we can't attach any DOM listeners or plugins that need to process a fully-realized node just yet. Secondly, there are actually two types of linking functions, pre and post. When Angular is processing your directive, it will first execute any and all of the pre-linking functions on the directive element itself, then it will recurse down into any child nodes within the element and compile those, then walk back up the DOM tree and execute all of the post-linking functions. As a result, the whole compilation process proceeds through the following flow:

mainDirective - compile fn -preLinking fn

mainDirective - postLink fn

firstChildDirective - compile fn - preLinking fn

firstChildDirective - postLink fn

lastChildDirective - compile fn - preLinking fn - postLinking fn

As a general rule, the same DOM manipulation restrictions apply to both the compile function and the pre-linking function, since the element is still being updated by Angular and cannot be cloned or otherwise modified before creating the final instance that will be inserted into the document. The post-linking function, on the other hand, receives the final element and can be manipulated as you need. The majority of your directives will likely just use the post-linking function. The pre-linking function is only advantageous when you need to perform some individualized preparation on the scope or controller object before any child elements compile, so we'll review that use case in more detail in Chapter 6, Controllers–Better with Sharing. Compile, on the other hand, is ideal when you need to make use of the transclusion function or conditionally modify your element before it's compiled, so we'll review a couple use cases of that in this chapter.

Chapter 4

[ 31 ]

ng-repeat

To start our study into the primary use cases for the compile function, let's take a look at the definition object for the built-in directive ng-repeat. The definition object is as follows:

...

.directive('ngRepeat', function () { return {

priority: 1000,

transclude: 'element', terminal: true,

compile: function(element, attr, linker) { // Compile Function return function(scope, iterStartElement, attr){ // Linking Function

};

} } });

The first few properties should be familiar from the previous chapter, but let's quickly review how each is used here. A priority value of 1000 makes sure that this directive executes before any others on the element, and setting terminal to true ensures that Angular's automatic compilation doesn't continue on to any other directives on this element or its children. We want this because the ng-repeat compile function itself will actually be handling the compilation processing for the node tree of child elements. A transclude value of element is used to collect the HTML content of the original element, as we'll need that to be able to compile it later and as such we'll see that it is used here shortly within the compile function.

Compile

The compile function declared in the preceding section takes three properties. The first is the template element, meaning it contains all the HTML within our DOM node, but again, this doesn't necessarily represent the instance of that element that's actually going to be inserted into our final document. The second property is similar to the $attrs object that was passed to our linking function in Chapter 3, Deconstructing Directives. It contains normalized access to each attribute on the DOM element. Remember, however, that we don't have an initialized scope here, so any of the attribute values that need to be evaluated within a scope won't have a useful value yet.

Compile versus Link

[ 32 ]

For the purpose of our examination, however, the final parameter is by far the most valuable. This linker is the transcluded function, which Angular would normally use to attach a scope to this element, interpolate all of the values, and then insert the final object into the DOM. By grabbing this linking function, ng-repeat now has control over when to perform the actual linking, as well as the ability to repeatedly do so, which of course is exactly what we need.

Link

The linking function is what the compile function returns. Because, in the case of ng-repeat, we only need to get access to the transcluding function, and not actually do any DOM manipulation on the template element, we can just return

that immediately.

At the heart of the ng-repeat linking function is $scope.$watch, which serves to connect the data and data changes to the rest of our code. As a quick note of introduction, the $watch method's first argument can take two types of parameters.

The first type is a simple string that corresponds to a scoped variable and is evaluated against the current scope; this is the method you'll probably use most commonly. The second type is a full function which takes the current scope as its own first parameter and allows for more complex comparisons. In both cases, any time the return value changes or is first initialized, the function passed in to $watch as the second parameter is called. In our case here of ng-repeat, we're actually using the watcher function (first parameter) to perform all of the necessary changes, since it gets called every time Angular executes the $digest function. As a note, I wouldn't actually recommend this method unless you're confident it's necessary and you've extensively optimized your watcher function, as this can get called several times a second.

The actual internals of the $watcher function are more complicated than it is worthwhile for us to dive into for the goals of this chapter, so we'll operate off this simplified model for the purpose of our discussion.

compile: function (element, attrs, linker) {

return function ($scope, $element, $attrs) { // "post" Linking function

$scope.$watch(function ($internalScope) {

Chapter 4

[ 33 ]

$element.html('');// Clear the element's current HTML var values = … // Read in array to iterate over values.forEach(function (data, index) {

$internalScope.element = data; // Attach this element's data properties to the 'element' property on the scope so we can use it within the template

linker($internalScope, function (clone) {

$element.append(clone); // Take the interpolated HTML and append it to our main $element

}); // end of linker clone function }); // end of forEach

}); // end of $watch

}; // end of linking function } // end of compile function

Obviously, we're setting aside some necessary caching and sorting methods, however for our purposes this represents the key process. We begin by reading in the current values for our array, iterating over each element to assign it to our scope, and then performing magic.

Well, almost magic. We're finally going to use that linker function we've been working up to for this whole section. Remember that we fetched this from the compile property, which means it's imbued with all of the requisite knowledge of our directive template (if there is one), the internal HTML within our element node, and is all but salivating at the chance to show off its skills at scope binding and interpolation. And since it's never wise to disappoint an anxious function, we're finally going to oblige it by sending in our newly crafted scope, along with a secondary function to handle the result.

Within the linker function, the scope gets bound to the compiled, but not yet interpolated, element HTML, and a new "fully transformed" element is generated.

That element gets passed to our secondary function as the clone parameter, whereby we use a little standard jQuery DOM manipulation to insert our clone element into the DOM at the end of our original element.

In a (somewhat large) nutshell, that's how ng-repeat makes use of the compile property to transform an element DOM structure, not just once, but continuously as the data changes. Now let's continue and check out ng-switch, another built-in directive that makes use of the compile property, though in a slightly different way.

Compile versus Link

[ 34 ]

ng-switch

Since we've already covered attaching directives extensively, we're not going to examine the HTML of an ng-switch directive in detail. However, in the interest of having a reference point while going through the JavaScript, here's a simple example shown in the following code snippet:

<div ng-switch="currentSport">

<p ng-switch-when="baseball">Home run!</p>

<p ng-switch-when="football">Touchdown!</p>

<p ng-switch-default>Goal!</p>

</div>

For an unexpected twist in the story, let's take a look at the ng-switch definition object:

{

restrict: 'EA', require: 'ngSwitch', controller: function () { this.cases = {};

},

link: function(scope, element, attr, ctrl) {

var watchExpr = attr.ngSwitch, // Read in the data property we want to monitor

selectedLinker, selectedElement, selectedScope;

scope.$watch(watchExpr, function (value) {

if (selectedElement) { // remove any prior HTML within $element selectedScope.$destroy();

selectedElement.remove();

selectedElement = selectedScope = null;

}

if ((selectedLinker = ctrl.cases['!' + value] || ctrl.

cases['?'])) {

selectedScope = scope.$new();

selectedLinker(selectedScope, function(caseElement) { selectedElement = caseElement;

element.append(caseElement);

});

} });

}

Chapter 4

[ 35 ]

I warned you there was a twist, and you've likely already seen it... there's no compile property here. That's right; our next foray into this study of compilation doesn't even include a compile property on the main directive. That's because ng-switch is actually a collection of three different directives, all of which work together, and it's on these other two directives that compile makes its shining appearance.

The two supplemental directives, ng-switch-when and ng-switch-default, work in nearly the same way, and function just as you might expect if you have any experience with switch statements in other languages. ng-switch-when serves to match against a specific case, and display its associated element if a match is made. On the other hand, ng-switch-default comes into play if no matches can be found. Because they're so similar, we'll only look in depth at the code of ng-switch-when, but I'll point out the one place they differ just so you can have a fuller understanding of it. Without further ado, let's have a look at the following ng-switch-when directive:

{

transclude: 'element', priority: 500,

require: '^ngSwitch',

compile: function(element, attrs, linker) { return function(scope, element, attr, ctrl) {

// For ng-switch-default, the linker function is simply attached to the '?' key within ctrl.cases

ctrl.cases['!' + attrs.ngSwitchWhen] = linker;

};

} }

In the same manner as we just saw with ng-repeat, we're once again setting the transclude property to element in order to request an instance of the linking function. This time, however, we've also grabbed an instance of the ngSwitch controller. We'll talk more about controllers in Chapter 6, Controllers–Better with Sharing, but for now just remember that when we request a controller with require, we're fetching the same instance that's in use on the ng-switch directive itself, and thus is shared with ng-switch and all of the other ng-switch-[when/default]

directives as well.

Unlike ng-repeat, we're not going to use the linker function immediately, but instead we use the subordinate directives (when/default) to collect all of the possible linker functions, and thus their respective DOM elements, and then store them on the primary ng-switch controller for use there. Once again, we use $watch to connect the data to our code, though in this case we're using the more common string method. This time, instead of grabbing a specific data element to update our scope, we're grabbing the transclude function that's attached to a particular template, allowing us to update the element HTML automatically to reflect changes in the data.

Compile versus Link

[ 36 ]

在文檔中 AngularJS Directives (頁 40-47)

相關文件