$render
The other day I asked a team member to introduce ui-tinymce into our project. A half day later, he told me that it didn’t work with Angular rc3 while rc2 was ok. He found some guy reported the same issue and got a workaround from another thread which was not a decent fix.
The issue is all about $render function isn’t called when model is actually changed. No content can be set into tinymce.
What this library did is very common if you want to wrap a 3rd party library into Angular. $render is what you need to set the value back into the 3rd party widget given any change. But this function never gets called if using Angular rc3!
I got some time to look into this issue today. I dived into Angular’s source and found what it documents about $render.
It is empty by default and should be implemented by the user. That’s what the author of the ui-tinymce did so far. It is supposed to be working. To diagnose this issue, I decided to find where this function is called by Angular. It’s still in NgModelController a few lines below the defination of $render.
Check the bottom line, if the value changes, $render is called. I put some log right below where I added the comment. I also added some log when $render is called in ui-tinymce. The former works as expected but the latter never gets called. Obviously, the actual $render is not the one defined in ui-tinymce. I have to dig more into this. I logged the $render function, it was
I searched Angular’s source code and found the definition of this function
I added some log above it and witnessed that this definition is called after the counterpart in ui-tinymce. I then switched to rc2, verified the result is on the contrary.
It’s now clear that the root cause of the issue is that the link function in ui-tinymce is called earlier in rc3. There must be more than one directive defined on the element, the order of the link function is controlled by the priority property.
directive’s priorty
It occurs to me there’s some change log in Angular rc3’s changelog talking about the directive priority change.
Previously the compile/link fns executed in this order controlled via priority:
- CompilePriorityHigh, CompilePriorityMedium, CompilePriorityLow
- compile child nodes
- PreLinkPriorityHigh, PreLinkPriorityMedium, PreLinkPriorityLow
- link child nodes
- PostLinkPriorityHigh, PostLinkPriorityMedium, PostLinkPriorityLow
This was changed to:
- CompilePriorityHigh, CompilePriorityMedium, CompilePriorityLow
- compile child nodes
- PreLinkPriorityHigh, PreLinkPriorityMedium, PreLinkPriorityLow
- link child nodes
- PostLinkPriorityLow, PostLinkPriorityMedium , PostLinkPriorityHigh
Very few directives in practice rely on order of postLinking function (unlike on the order of compile functions), so in the rare case of this change affecting an existing directive, it might be necessary to convert it to a preLinking function or give it negative priority (look at the diff of this commit to see how an internal attribute interpolation directive was adjusted).
The first attempt is to find what is the priority of the ngModel directive. Searching through the source code, I didn’t find any. With a safe guess, it would be 0 by default. I added the priority of 10 in ui-tinymce, the whole thing gets back to work again, I can see from the log our customized link function is called after the native one.
The Mystery
I can call it a day if I want because the issue is fixed. But I still leave some questions unsolved:
- I can’t even name the directive by calling it “the native one”
- Actually rc3 didn’t change the priority related to this issue, both directives have default priority “0” in this case. Why the invoking order changes between rc2 and rc3?
directive under the hood
There’s some directive added by Angular implicitly during initialization, like input
, textarea
, form
. Despite the name, it’s actual an Angular directive. This is very important and may sound confusing when you first come to it. I think why Angular doesn’t pre-define those directives as ng-input
, ng-textarea
is to leave user no choice but always stick to them. Otherwise if you don’t want to use ng-input
, youcan switch to input
by simply use input
tag. That being said, elements like input
, textarea
are always directives.
All the native Angular directive name actually goes with a directive, the rule is defined as below.
I’d like to list them all because it helps me understand other stuff which I thought was magic before. During the compiling phase, Angular will go through the DOM and do the name-and-directive matching. Native directive will use beflow rule and customized one will use “same name matching”. rule. e.g. data-ng-input
will be normalized into input
and goes with inputDirecive
. data-ng-customized-div
will be normalized into customizedDiv
and go with customizedDiv
directive.
Let’s get to the inputDirecive
.
In the link
function, it will find the base link function it will use. We didn’t specify any type in the textarea tag, so it will use “text” as the default.
“text” goes with textInputType
textInputType
What is textInputType
? It’s a link function for all input elements. It adds
- some common functions on ngModel controller
- some event listener. It also addes validation. It also provide a default implementation for some
noop
function defined in ngModelController.$render
is one of it. But you can override it in your link, like ui-tinymce does. In our case, there’re actually 3 directives bound totextarea
, let’s take a close look at them. ‘textarea’ is the implicit directive which has textInputType as its link function.
If 2 directives on the same element both override the same function on ngModelController, how does Angular determine the order? well, Let’s look at this.
It’s the sort function byPriority
that makes the call and priority
plays an important role. rc3 and rc2 are totally different. In rc3, it will compare the name and index if priorty is the same.
In our case, because uiTinymce
is larger than textarea
in string comparison, in contrast, it has a lower priority than textarea
. According to the rule, lower priority directive is post-linked before higher one, so that’s why our $render is always overriden by the default one.
The original order is [textarea
, uiTinymce
, ngModel
]. In rc2, because lower priority directive will be post-linked after higher one, uiTinymce
is linked after textarea
. I’m going to take a look at how Angular gets these directives. But now, I’m going to call it a day.