fix($compile): fix (reverse) directive postLink fn execution order

previously the compile/link fns executed in this order controlled via priority:

- CompilePriorityHigh, CompilePriorityMedium, CompilePriorityLow
- PreLinkPriorityHigh, PreLinkPriorityMedium, PreLinkPriorityLow
- link children
- PostLinkPriorityHigh, PostLinkPriorityMedium, PostLinkPriorityLow

This was changed to:

- CompilePriorityHigh, CompilePriorityMedium, CompilePriorityLow
- PreLinkPriorityHigh, PreLinkPriorityMedium, PreLinkPriorityLow
- link children
- PostLinkPriorityLow, PostLinkPriorityMedium , PostLinkPriorityHigh

Using this order the child transclusion directive that gets replaced
onto the current element get executed correctly (see issue #3558),
and more generally, the order of execution of post linking function
makes more sense. The incorrect order was an oversight that has
gone unnoticed for many suns and moons.

(FYI: postLink functions are the default linking functions)

BREAKING CHANGE: the order of postLink fn is now mirror opposite of
the order in which corresponding preLinking and compile functions
execute.

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).

Closes #3558
This commit is contained in:
Igor Minar 2013-10-01 07:55:47 -07:00
parent fe2145016c
commit 31f190d4d5
3 changed files with 123 additions and 30 deletions

View file

@ -1095,7 +1095,7 @@ function $CompileProvider($provide) {
childLinkFn && childLinkFn(scope, linkNode.childNodes, undefined, boundTranscludeFn);
// POSTLINKING
for(i = 0, ii = postLinkFns.length; i < ii; i++) {
for(i = postLinkFns.length - 1; i >= 0; i--) {
try {
linkFn = postLinkFns[i];
linkFn(scope, $element, attrs,
@ -1328,7 +1328,7 @@ function $CompileProvider($provide) {
}
directives.push({
priority: 100,
priority: -100,
compile: valueFn(function attrInterpolateLinkFn(scope, element, attr) {
var $$observers = (attr.$$observers || (attr.$$observers = {}));
@ -1346,6 +1346,7 @@ function $CompileProvider($provide) {
// register any observers
if (!interpolateFn) return;
// TODO(i): this should likely be attr.$set(name, iterpolateFn(scope) so that we reset the actual attr value
attr[name] = interpolateFn(scope);
($$observers[name] || ($$observers[name] = [])).$$inter = true;
(attr.$$observers && attr.$$observers[name].$$scope || scope).

View file

@ -958,7 +958,7 @@ var VALID_CLASS = 'ng-valid',
</file>
* </example>
*
*
*
*/
var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse',
function($scope, $exceptionHandler, $attr, $element, $parse) {

View file

@ -96,19 +96,25 @@ describe('$compile', function() {
directive('div', function(log) {
return {
restrict: 'ECA',
link: log.fn('1')
link: {
pre: log.fn('pre1'),
post: log.fn('post1')
}
};
});
directive('div', function(log) {
return {
restrict: 'ECA',
link: log.fn('2')
link: {
pre: log.fn('pre2'),
post: log.fn('post2')
}
};
});
});
inject(function($compile, $rootScope, log) {
element = $compile('<div></div>')($rootScope);
expect(log).toEqual('1; 2');
expect(log).toEqual('pre1; pre2; post2; post1');
});
});
});
@ -206,7 +212,7 @@ describe('$compile', function() {
'<span greet="angular" log="L" x-high-log="H" data-medium-log="M"></span>')
($rootScope);
expect(element.text()).toEqual('Hello angular');
expect(log).toEqual('H; M; L');
expect(log).toEqual('L; M; H');
}));
@ -387,7 +393,7 @@ describe('$compile', function() {
element = $compile(
'<span log="L" x-high-log="H" data-medium-log="M"></span>')
($rootScope);
expect(log).toEqual('H; M; L');
expect(log).toEqual('L; M; H');
}));
});
@ -511,8 +517,7 @@ describe('$compile', function() {
($rootScope);
$rootScope.$digest();
expect(element.text()).toEqual('Replace!');
// HIGH goes after MEDIUM since it executes as part of replaced template
expect(log).toEqual('MEDIUM; HIGH; LOG');
expect(log).toEqual('LOG; HIGH; MEDIUM');
}));
@ -521,7 +526,7 @@ describe('$compile', function() {
($rootScope);
$rootScope.$digest();
expect(element.text()).toEqual('Append!');
expect(log).toEqual('HIGH; LOG; MEDIUM');
expect(log).toEqual('LOG; HIGH; MEDIUM');
}));
@ -1079,7 +1084,7 @@ describe('$compile', function() {
expect(log).toEqual(
'first-C; FLUSH; second-C; last-C; third-C; ' +
'first-PreL; second-PreL; last-PreL; third-PreL; ' +
'third-PostL; first-PostL; second-PostL; last-PostL');
'third-PostL; last-PostL; second-PostL; first-PostL');
var span = element.find('span');
expect(span.attr('first')).toEqual('');
@ -1104,7 +1109,7 @@ describe('$compile', function() {
expect(log).toEqual(
'iFirst-C; FLUSH; iSecond-C; iThird-C; iLast-C; ' +
'iFirst-PreL; iSecond-PreL; iThird-PreL; iLast-PreL; ' +
'iFirst-PostL; iSecond-PostL; iThird-PostL; iLast-PostL');
'iLast-PostL; iThird-PostL; iSecond-PostL; iFirst-PostL');
var div = element.find('div');
expect(div.attr('i-first')).toEqual('');
@ -1130,7 +1135,7 @@ describe('$compile', function() {
expect(log).toEqual(
'first-C; FLUSH; second-C; last-C; third-C; ' +
'first-PreL; second-PreL; last-PreL; third-PreL; ' +
'third-PostL; first-PostL; second-PostL; last-PostL');
'third-PostL; last-PostL; second-PostL; first-PostL');
var span = element.find('span');
expect(span.attr('first')).toEqual('');
@ -1156,7 +1161,7 @@ describe('$compile', function() {
expect(log).toEqual(
'iFirst-C; FLUSH; iSecond-C; iThird-C; iLast-C; ' +
'iFirst-PreL; iSecond-PreL; iThird-PreL; iLast-PreL; ' +
'iFirst-PostL; iSecond-PostL; iThird-PostL; iLast-PostL');
'iLast-PostL; iThird-PostL; iSecond-PostL; iFirst-PostL');
var div = element.find('div');
expect(div.attr('i-first')).toEqual('');
@ -1344,10 +1349,10 @@ describe('$compile', function() {
scope: true,
restrict: 'CA',
compile: function() {
return function (scope, element) {
return {pre: function (scope, element) {
log(scope.$id);
expect(element.data('$scope')).toBe(scope);
};
}};
}
};
});
@ -1409,9 +1414,9 @@ describe('$compile', function() {
directive('log', function(log) {
return {
restrict: 'CA',
link: function(scope) {
link: {pre: function(scope) {
log('log-' + scope.$id + '-' + scope.$parent.$id);
}
}}
};
});
}));
@ -1419,7 +1424,7 @@ describe('$compile', function() {
it('should allow creation of new scopes', inject(function($rootScope, $compile, log) {
element = $compile('<div><span scope><a log></a></span></div>')($rootScope);
expect(log).toEqual('LOG; log-002-001; 002');
expect(log).toEqual('002; log-002-001; LOG');
expect(element.find('span').hasClass('ng-scope')).toBe(true);
}));
@ -1427,7 +1432,7 @@ describe('$compile', function() {
it('should allow creation of new isolated scopes for directives', inject(
function($rootScope, $compile, log) {
element = $compile('<div><span iscope><a log></a></span></div>')($rootScope);
expect(log).toEqual('LOG; log-002-001; 002');
expect(log).toEqual('log-002-001; LOG; 002');
$rootScope.name = 'abc';
expect(iscope.$parent).toBe($rootScope);
expect(iscope.name).toBeUndefined();
@ -1439,7 +1444,7 @@ describe('$compile', function() {
$httpBackend.expect('GET', 'tscope.html').respond('<a log>{{name}}; scopeId: {{$id}}</a>');
element = $compile('<div><span tscope></span></div>')($rootScope);
$httpBackend.flush();
expect(log).toEqual('LOG; log-002-001; 002');
expect(log).toEqual('log-002-001; LOG; 002');
$rootScope.name = 'Jozo';
$rootScope.$apply();
expect(element.text()).toBe('Jozo; scopeId: 002');
@ -1453,7 +1458,7 @@ describe('$compile', function() {
respond('<p><a log>{{name}}; scopeId: {{$id}}</a></p>');
element = $compile('<div><span trscope></span></div>')($rootScope);
$httpBackend.flush();
expect(log).toEqual('LOG; log-002-001; 002');
expect(log).toEqual('log-002-001; LOG; 002');
$rootScope.name = 'Jozo';
$rootScope.$apply();
expect(element.text()).toBe('Jozo; scopeId: 002');
@ -1467,7 +1472,7 @@ describe('$compile', function() {
respond('<p><a log>{{name}}; scopeId: {{$id}} |</a></p>');
element = $compile('<div><span ng-repeat="i in [1,2,3]" trscope></span></div>')($rootScope);
$httpBackend.flush();
expect(log).toEqual('LOG; log-003-002; 003; LOG; log-005-004; 005; LOG; log-007-006; 007');
expect(log).toEqual('log-003-002; LOG; 003; log-005-004; LOG; 005; log-007-006; LOG; 007');
$rootScope.name = 'Jozo';
$rootScope.$apply();
expect(element.text()).toBe('Jozo; scopeId: 003 |Jozo; scopeId: 005 |Jozo; scopeId: 007 |');
@ -1481,7 +1486,7 @@ describe('$compile', function() {
$httpBackend.expect('GET', 'tiscope.html').respond('<a log></a>');
element = $compile('<div><span tiscope></span></div>')($rootScope);
$httpBackend.flush();
expect(log).toEqual('LOG; log-002-001; 002');
expect(log).toEqual('log-002-001; LOG; 002');
$rootScope.name = 'abc';
expect(iscope.$parent).toBe($rootScope);
expect(iscope.name).toBeUndefined();
@ -1501,7 +1506,7 @@ describe('$compile', function() {
'</b>' +
'</div>'
)($rootScope);
expect(log).toEqual('LOG; log-003-002; 003; LOG; log-002-001; 002; LOG; log-004-001; 004');
expect(log).toEqual('002; 003; log-003-002; LOG; log-002-001; LOG; 004; log-004-001; LOG');
})
);
@ -1571,7 +1576,38 @@ describe('$compile', function() {
$rootScope.$digest();
expect(element.text()).toEqual('text: angular');
expect(element.attr('name')).toEqual('attr: angular');
}));
})
);
it('should process attribute interpolation at the beginning of the post-linking phase', function() {
module(function() {
directive('attrLog', function(log) {
return {
compile: function($element, $attrs) {
log('compile=' + $attrs.myName);
return {
pre: function($scope, $element, $attrs) {
log('preLink=' + $attrs.myName);
},
post: function($scope, $element) {
log('postLink=' + $attrs.myName);
}
}
}
}
})
});
inject(function($rootScope, $compile, log) {
element = $compile('<div attr-log my-name="{{name}}"></div>')($rootScope);
$rootScope.name = 'angular';
$rootScope.$apply();
log('digest=' + element.attr('my-name'));
expect(log).toEqual('compile={{name}}; preLink={{name}}; postLink=; digest=angular');
});
});
describe('SCE values', function() {
it('should resolve compile and link both attribute and text bindings', inject(
@ -1753,7 +1789,7 @@ describe('$compile', function() {
it('should compile from top to bottom but link from bottom up', inject(
function($compile, $rootScope, log) {
element = $compile('<a b><c></c></a>')($rootScope);
expect(log).toEqual('tA; tB; tC; preA; preB; preC; postC; postA; postB');
expect(log).toEqual('tA; tB; tC; preA; preB; preC; postC; postB; postA');
}
));
@ -2230,7 +2266,7 @@ describe('$compile', function() {
});
inject(function(log, $compile, $rootScope) {
element = $compile('<div main dep other></div>')($rootScope);
expect(log).toEqual('main; dep:main; false');
expect(log).toEqual('false; dep:main; main');
});
});
@ -2639,7 +2675,7 @@ describe('$compile', function() {
element = $compile('<div><div high-log trans="text" log>{{$parent.$id}}-{{$id}};</div></div>')
($rootScope);
$rootScope.$apply();
expect(log).toEqual('compile: <!-- trans: text -->; HIGH; link; LOG; LOG');
expect(log).toEqual('compile: <!-- trans: text -->; link; LOG; LOG; HIGH');
expect(element.text()).toEqual('001-002;001-003;');
});
});
@ -2907,6 +2943,62 @@ describe('$compile', function() {
});
it('should make the result of a transclusion available to the parent *replace* directive in post-linking phase (template)',
function() {
module(function() {
directive('replacedTrans', function(log) {
return {
transclude: true,
replace: true,
template: '<div ng-transclude></div>',
link: {
pre: function($scope, $element) {
log('pre(' + $element.text() + ')');
},
post: function($scope, $element) {
log('post(' + $element.text() + ')');
}
}
};
});
});
inject(function(log, $rootScope, $compile) {
element = $compile('<div replaced-trans><span>unicorn!</span></div>')($rootScope);
$rootScope.$apply();
expect(log).toEqual('pre(); post(unicorn!)');
});
});
it('should make the result of a transclusion available to the parent *replace* directive in post-linking phase (templateUrl)',
function() {
module(function() {
directive('replacedTrans', function(log) {
return {
transclude: true,
replace: true,
templateUrl: 'trans.html',
link: {
pre: function($scope, $element) {
log('pre(' + $element.text() + ')');
},
post: function($scope, $element) {
log('post(' + $element.text() + ')');
}
}
};
});
});
inject(function(log, $rootScope, $compile, $templateCache) {
$templateCache.put('trans.html', '<div ng-transclude></div>');
element = $compile('<div replaced-trans><span>unicorn!</span></div>')($rootScope);
$rootScope.$apply();
expect(log).toEqual('pre(); post(unicorn!)');
});
});
it('should terminate compilation only for element trasclusion', function() {
module(function() {
directive('elementTrans', function(log) {