Refactoring bnRepeatSwitch To Use A Multi-Priority Directive In AngularJS
Yesterday, I demonstrated that a single directive could be bound to multiple priorities in the same compile and linking phase in AngularJS. While the coolness of this may not be obvious, I immediately thought of my previous post on the ngRepeat-optimized "switch" directive. It used two directives in order to execute on both "sides" of the native ngRepeat directive; but, given yesterday's discovery, I wanted to refactor bnRepeatSwitch to use a single directive with multiple priorities.
I'll keep this blog post short since there's not really any new content - just some remixing of existing ideas. But, the basic idea behind the bnRepeatSwitch directive is that I wanted to pre-compile switch-case statements, that would be used in the context of an ngRepeat directive, such that they only had to be compiled once and not N-times (once for each ngRepeat clone). In order to do this, I need to access two different parts of the compilation and linking phase of the ngRepeat element:
- Pre-ngRepeat - pre-compile and strip-out "case" statements.
- ngRepeat executes.
- Post-ngRepeat - clone, link, and inject appropriate "case" statement.
In my previous attempt, I did this using two different directives:
- bnRepeatSwitch - executes pre-ngRepeat.
- bnRepeatSwitchOn - executes post-ngRepeat.
But, in the following code, you'll see that I have replaced the ngRepeatSwitchOn directive with an alternate version of the bnSwitchRepeat directive that executes at a lower priority. As such the following code now uses a single directive - bnSwitchRepeat - which is compiled and linked at two different priorities on the same element:
The ngRepeat directive sticks out as the most obvious use-case for this kind of duality because of its 1-to-N nature: 1 compile phase, N linking phases. Thinking about the ngRepeat holistically, then, there are things that I'll want to do once, and then subsequent things that I'll want to do N-times. But, I'm sure there are many other use-cases. I mean, heck, the AngularJS source code uses this to implement some of its directives.
Want to use code from this post? Check out the license.
Hm...in your comments you seem to imply that the higher a directive's priority the sooner it gets compiled **and** linked.
I just wanted to point out that the order* of each phase is as follows:
1. Compile (in descending priority order).
2. Instantiate controller (in descending priority order).
3. Pre-link (in descending priority order).
4. Post-link (in **ascending** priority order).
I.e. a higher priority means the directive will compile/controller/pre-link first, but post-link last.
* Using templates affects the order, because some asynchronous operations are involved into resolving templates.
Ah, good point! Though, I believe the caveat might be that I think the order was a breaking-change in AngualrJS 1.2. But, you are completely right.
And, to be honest, I don't think I've ever put much thought in to the order of the linking on a single element. I wonder what kind of use-cases would cause a conflict there.... hmmm.
I have no idea about pre 1.2.x, so you might be right.
Not sure if any use-case would cause a conflict (probably none).
The main points imo are two:
The order doesn't seem to play a significant role and everything works fine although the link functions execute in the opposite order than you expected.
But, it is OK, because the second directive sets up a watch, so will pick up changes even if they happen later.
Basically, you can probably achieve the same effect with just one directive executing at priority 1001, since you can use the compile/preLink functions to do stuff before ngRepeat and the postLink function to do stuff after ngRepeat.
(I say "probably" because I haven't tried it out, but I am pretty sure it is possible.)
You raise an interesting question. I don't actually understand the difference between the "link" function and the "postLink" function. I have only ever used "link. I just hopped over to the $compile() documentation:
### Pre-linking function
Executed before the child elements are linked. Not safe to do DOM transformation since the compiler linking function will fail to locate the correct elements for linking.
### Post-linking function
Executed after the child elements are linked. Note that child elements that contain templateUrl directives will not have been compiled and linked since they are waiting for their template to load asynchronously and their own compilation and linking has been suspended until that occurs. It is safe to do DOM transformation in the post-linking function on elements that are not waiting for their async templates to be resolved.
This doesn't necessarily clarify it. It refers to "child elements". So, does that include same-element directives at "lower priories". I'll have to do some more digging.
Thanks for bringing up such an interesting question!
The description of the `priority` property sheds some more light
> Directives with greater numerical priority are compiled first. Pre-link functions are also run in priority order, but post-link functions are run in reverse order.
(This is what I was referring to basically.)
What this means is that if ng-repeat has priority 1000 and your-directive has priority 1001 (both being on the same element), the order of execution will be as follows:
1. your-directive compile
2. ng-repeat compile
3. your-directive pre-linking
4. ng-repeat pre-linking
5. ng-repeat post-linking
6. your-directive post-linking
(Here is a fiddle that demonstrates this: http://jsfiddle.net/ExpertSystem/h0jjcr6g/1/)
I did a little bit more digging and just for point of clarity, I wanted to demonstrate (to myself) that the "post link" function and the "generic" link function are one-in-the-same things:
So, when we refer to Post and Pre link timing, the only "new" functionality we get with the breakdown is the "pre" portion. The "post" link function is just the standard link function (which links after its children have linked).
That said, going back to the compile and linking order, I still think that it won't work for ng-repeat and my Switch experiment. Remember, ngRepeat doesn't link "N-times". It only links once. It just so happens that within that one linking function, it clones itself and links the clone to a new child scope. And, since the priority of the ngRepeat is 1,000, no directives with higher priority (ex, 1,001) will link on the cloned element.
As such, I think that you still need to bind the bnRepeatSwitch at two different priorities - one that compiles before ngRepeat; and, one that links at a *lower priority* so that it will link during the transclusion.
Originally I didn't pay much attention to the fact that you need one directive to act on the template and one to act on the cloned instances.
I realize my suggestion won't work (because my proposed directive's postLink function will act on the original element (at that time replaced by ngRepeat with a comment)) and not on the cloned instances.
Sorry for causing a little confusion. Hopefully it was the constuctive type of confusion :D
That said, there is a part on the comment of the latter `bnRepeatSwitch`, that is kind of inaccurate/misleading (imo):
> NOTE: This directive has to execute with the priority 999 so that it is linked AFTER the ngRepeat directive is linked (at 1,000).
Normaly, a priority 999 would post-link **before** a priority 1000.
The real reason that a priority 999 is important here, is how `transclude: 'element'` works, which results in attaching directives with lower priority (e.g. < 1000 for ngRepeat) to the cloned instances and not the original element
As a final note, it **is** indeed possible to provide the same functionality using just one directive (relying on `ngRepeat`'s `transclude: 'element'` behaviour), but we need to change the implementation a bit. More specifically, we need to drop the use of controller (which acts as nothing more than a "medium" to pass some data from the compile phase to the linking phase).
We can define a `bnRepeatSwitch` directive at priority 999, which processes the templates during compiling and (after `ngRepeat` has transcluded the whole template element) and links the templates during the post-linking phase of each instance.
Here is the POC fiddle: http://jsfiddle.net/ExpertSystem/ovy60r25/5/