fix(ngInclude): $animate refactoring + use transclusion

BREAKING CHANGE: previously ngInclude only updated its content, after this change
ngInclude will recreate itself every time a new content is included. This ensures
that a single rootElement for all the included contents always exists, which makes
definition of css styles for animations much easier.
This commit is contained in:
Matias Niemelä 2013-07-06 00:48:54 -04:00 committed by Misko Hevery
parent 8ed0d5b6aa
commit aa2133ad81
4 changed files with 105 additions and 70 deletions

View file

@ -15,7 +15,7 @@
overflow:hidden; overflow:hidden;
} }
.slide-reveal > .ng-enter { .slide-reveal.ng-enter {
-webkit-transition:0.5s linear all; -webkit-transition:0.5s linear all;
-moz-transition:0.5s linear all; -moz-transition:0.5s linear all;
-o-transition:0.5s linear all; -o-transition:0.5s linear all;
@ -26,7 +26,7 @@
opacity:0; opacity:0;
top:10px; top:10px;
} }
.slide-reveal > .ng-enter.ng-enter-active { .slide-reveal.ng-enter.ng-enter-active {
top:0; top:0;
opacity:1; opacity:1;
} }

View file

@ -24,8 +24,10 @@
* access on some browsers) * access on some browsers)
* *
* @animations * @animations
* enter - happens just after the ngInclude contents change and a new DOM element is created and injected into the ngInclude container * enter - animation is used to bring new content into the browser.
* leave - happens just after the ngInclude contents change and just before the former contents are removed from the DOM * leave - animation is used to animate existing content away.
*
* The enter and leave animation occur concurrently.
* *
* @scope * @scope
* *
@ -49,9 +51,9 @@
</select> </select>
url of the template: <tt>{{template.url}}</tt> url of the template: <tt>{{template.url}}</tt>
<hr/> <hr/>
<div class="example-animate-container" <div class="example-animate-container">
ng-include="template.url" <div class="include-example" ng-include="template.url"></div>
ng-animate="{enter: 'example-enter', leave: 'example-leave'}"></div> </div>
</div> </div>
</file> </file>
<file name="script.js"> <file name="script.js">
@ -63,14 +65,13 @@
} }
</file> </file>
<file name="template1.html"> <file name="template1.html">
<div>Content of template1.html</div> Content of template1.html
</file> </file>
<file name="template2.html"> <file name="template2.html">
<div>Content of template2.html</div> Content of template2.html
</file> </file>
<file name="animations.css"> <file name="animations.css">
.example-leave, .include-example.ng-enter, .include-example.ng-leave {
.example-enter {
-webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
-moz-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; -moz-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
-ms-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; -ms-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
@ -82,24 +83,21 @@
left:0; left:0;
right:0; right:0;
bottom:0; bottom:0;
}
.example-animate-container > * {
display:block; display:block;
padding:10px; padding:10px;
} }
.example-enter { .include-example.ng-enter {
top:-50px; top:-50px;
} }
.example-enter.example-enter-active { .include-example.ng-enter.ng-enter-active {
top:0; top:0;
} }
.example-leave { .include-example.ng-leave {
top:0; top:0;
} }
.example-leave.example-leave-active { .include-example.ng-leave.ng-leave-active {
top:50px; top:50px;
} }
</file> </file>
@ -115,7 +113,7 @@
}); });
it('should change to blank', function() { it('should change to blank', function() {
select('template').option(''); select('template').option('');
expect(element('.doc-example-live [ng-include]').text()).toEqual(''); expect(element('.doc-example-live [ng-include]')).toBe(undefined);
}); });
</file> </file>
</example> </example>
@ -145,21 +143,26 @@ var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$compile'
return { return {
restrict: 'ECA', restrict: 'ECA',
terminal: true, terminal: true,
compile: function(element, attr) { transclude: 'element',
compile: function(element, attr, transclusion) {
var srcExp = attr.ngInclude || attr.src, var srcExp = attr.ngInclude || attr.src,
onloadExp = attr.onload || '', onloadExp = attr.onload || '',
autoScrollExp = attr.autoscroll; autoScrollExp = attr.autoscroll;
return function(scope, element, attr) { return function(scope, $element) {
var changeCounter = 0, var changeCounter = 0,
childScope; currentScope,
currentElement;
var clearContent = function() { var cleanupLastIncludeContent = function() {
if (childScope) { if (currentScope) {
childScope.$destroy(); currentScope.$destroy();
childScope = null; currentScope = null;
}
if(currentElement) {
$animate.leave(currentElement);
currentElement = null;
} }
$animate.leave(element.contents());
}; };
scope.$watch($sce.parseAsResourceUrl(srcExp), function ngIncludeWatchAction(src) { scope.$watch($sce.parseAsResourceUrl(srcExp), function ngIncludeWatchAction(src) {
@ -168,28 +171,31 @@ var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$compile'
if (src) { if (src) {
$http.get(src, {cache: $templateCache}).success(function(response) { $http.get(src, {cache: $templateCache}).success(function(response) {
if (thisChangeId !== changeCounter) return; if (thisChangeId !== changeCounter) return;
var newScope = scope.$new();
if (childScope) childScope.$destroy(); transclusion(newScope, function(clone) {
childScope = scope.$new(); cleanupLastIncludeContent();
$animate.leave(element.contents());
var contents = jqLite('<div/>').html(response).contents(); currentScope = newScope;
currentElement = clone;
$animate.enter(contents, element); currentElement.html(response);
$compile(contents)(childScope); $animate.enter(currentElement, null, $element);
$compile(currentElement.contents())(currentScope);
if (isDefined(autoScrollExp) && (!autoScrollExp || scope.$eval(autoScrollExp))) { if (isDefined(autoScrollExp) && (!autoScrollExp || scope.$eval(autoScrollExp))) {
$anchorScroll(); $anchorScroll();
} }
childScope.$emit('$includeContentLoaded'); currentScope.$emit('$includeContentLoaded');
scope.$eval(onloadExp); scope.$eval(onloadExp);
});
}).error(function() { }).error(function() {
if (thisChangeId === changeCounter) clearContent(); if (thisChangeId === changeCounter) cleanupLastIncludeContent();
}); });
scope.$emit('$includeContentRequested'); scope.$emit('$includeContentRequested');
} else { } else {
clearContent(); cleanupLastIncludeContent();
} }
}); });
}; };

View file

@ -47,10 +47,10 @@
* transition:0.5s linear all; * transition:0.5s linear all;
* } * }
* *
* .slide > .ng-enter { } /&#42; starting animations for enter &#42;/ * .slide.ng-enter { } /&#42; starting animations for enter &#42;/
* .slide > .ng-enter-active { } /&#42; terminal animations for enter &#42;/ * .slide.ng-enter-active { } /&#42; terminal animations for enter &#42;/
* .slide > .ng-leave { } /&#42; starting animations for leave &#42;/ * .slide.ng-leave { } /&#42; starting animations for leave &#42;/
* .slide > .ng-leave-active { } /&#42; terminal animations for leave &#42;/ * .slide.ng-leave-active { } /&#42; terminal animations for leave &#42;/
* </style> * </style>
* *
* <!-- * <!--

View file

@ -17,7 +17,7 @@ describe('ngInclude', function() {
it('should trust and use literal urls', inject(function( it('should trust and use literal urls', inject(function(
$rootScope, $httpBackend, $compile) { $rootScope, $httpBackend, $compile) {
element = $compile('<div ng-include="\'url\'"></div>')($rootScope); element = $compile('<div><div ng-include="\'url\'"></div></div>')($rootScope);
$httpBackend.expect('GET', 'url').respond('template text'); $httpBackend.expect('GET', 'url').respond('template text');
$rootScope.$digest(); $rootScope.$digest();
$httpBackend.flush(); $httpBackend.flush();
@ -27,7 +27,7 @@ describe('ngInclude', function() {
it('should trust and use trusted urls', inject(function($rootScope, $httpBackend, $compile, $sce) { it('should trust and use trusted urls', inject(function($rootScope, $httpBackend, $compile, $sce) {
element = $compile('<div ng-include="fooUrl"></div>')($rootScope); element = $compile('<div><div ng-include="fooUrl"></div></div>')($rootScope);
$httpBackend.expect('GET', 'http://foo.bar/url').respond('template text'); $httpBackend.expect('GET', 'http://foo.bar/url').respond('template text');
$rootScope.fooUrl = $sce.trustAsResourceUrl('http://foo.bar/url'); $rootScope.fooUrl = $sce.trustAsResourceUrl('http://foo.bar/url');
$rootScope.$digest(); $rootScope.$digest();
@ -39,20 +39,21 @@ describe('ngInclude', function() {
it('should include an external file', inject(putIntoCache('myUrl', '{{name}}'), it('should include an external file', inject(putIntoCache('myUrl', '{{name}}'),
function($rootScope, $compile) { function($rootScope, $compile) {
element = jqLite('<ng:include src="url"></ng:include>'); element = jqLite('<div><ng:include src="url"></ng:include></div>');
jqLite(document.body).append(element); var body = jqLite(document.body);
body.append(element);
element = $compile(element)($rootScope); element = $compile(element)($rootScope);
$rootScope.name = 'misko'; $rootScope.name = 'misko';
$rootScope.url = 'myUrl'; $rootScope.url = 'myUrl';
$rootScope.$digest(); $rootScope.$digest();
expect(element.text()).toEqual('misko'); expect(body.text()).toEqual('misko');
jqLite(document.body).html(''); body.html('');
})); }));
it('should support ng-include="src" syntax', inject(putIntoCache('myUrl', '{{name}}'), it('should support ng-include="src" syntax', inject(putIntoCache('myUrl', '{{name}}'),
function($rootScope, $compile) { function($rootScope, $compile) {
element = jqLite('<div ng-include="url"></div>'); element = jqLite('<div><div ng-include="url"></div></div>');
jqLite(document.body).append(element); jqLite(document.body).append(element);
element = $compile(element)($rootScope); element = $compile(element)($rootScope);
$rootScope.name = 'Alibaba'; $rootScope.name = 'Alibaba';
@ -89,7 +90,7 @@ describe('ngInclude', function() {
it('should remove previously included text if a falsy value is bound to src', inject( it('should remove previously included text if a falsy value is bound to src', inject(
putIntoCache('myUrl', '{{name}}'), putIntoCache('myUrl', '{{name}}'),
function($rootScope, $compile) { function($rootScope, $compile) {
element = jqLite('<ng:include src="url"></ng:include>'); element = jqLite('<div><ng:include src="url"></ng:include></div>');
element = $compile(element)($rootScope); element = $compile(element)($rootScope);
$rootScope.name = 'igor'; $rootScope.name = 'igor';
$rootScope.url = 'myUrl'; $rootScope.url = 'myUrl';
@ -112,7 +113,7 @@ describe('ngInclude', function() {
$httpBackend.whenGET('url').respond('my partial'); $httpBackend.whenGET('url').respond('my partial');
$rootScope.$on('$includeContentRequested', contentRequestedSpy); $rootScope.$on('$includeContentRequested', contentRequestedSpy);
element = $compile('<ng:include src="\'url\'"></ng:include>')($rootScope); element = $compile('<div><div><ng:include src="\'url\'"></ng:include></div></div>')($rootScope);
$rootScope.$digest(); $rootScope.$digest();
expect(contentRequestedSpy).toHaveBeenCalledOnce(); expect(contentRequestedSpy).toHaveBeenCalledOnce();
@ -130,7 +131,7 @@ describe('ngInclude', function() {
$templateCache.put('url', [200, 'partial content', {}]); $templateCache.put('url', [200, 'partial content', {}]);
$rootScope.$on('$includeContentLoaded', contentLoadedSpy); $rootScope.$on('$includeContentLoaded', contentLoadedSpy);
element = $compile('<ng:include src="\'url\'"></ng:include>')($rootScope); element = $compile('<div><div><ng:include src="\'url\'"></ng:include></div></div>')($rootScope);
$rootScope.$digest(); $rootScope.$digest();
expect(contentLoadedSpy).toHaveBeenCalledOnce(); expect(contentLoadedSpy).toHaveBeenCalledOnce();
@ -140,7 +141,7 @@ describe('ngInclude', function() {
it('should evaluate onload expression when a partial is loaded', inject( it('should evaluate onload expression when a partial is loaded', inject(
putIntoCache('myUrl', 'my partial'), putIntoCache('myUrl', 'my partial'),
function($rootScope, $compile) { function($rootScope, $compile) {
element = jqLite('<ng:include src="url" onload="loaded = true"></ng:include>'); element = jqLite('<div><div><ng:include src="url" onload="loaded = true"></ng:include></div></div>');
element = $compile(element)($rootScope); element = $compile(element)($rootScope);
expect($rootScope.loaded).not.toBeDefined(); expect($rootScope.loaded).not.toBeDefined();
@ -158,7 +159,7 @@ describe('ngInclude', function() {
$httpBackend.whenGET('url1').respond('partial {{$parent.url}}'); $httpBackend.whenGET('url1').respond('partial {{$parent.url}}');
$httpBackend.whenGET('url2').respond(404); $httpBackend.whenGET('url2').respond(404);
element = $compile('<ng:include src="url"></ng:include>')($rootScope); element = $compile('<div><ng:include src="url"></ng:include></div>')($rootScope);
expect(element.children().scope()).toBeFalsy(); expect(element.children().scope()).toBeFalsy();
$rootScope.url = 'url1'; $rootScope.url = 'url1';
@ -185,7 +186,7 @@ describe('ngInclude', function() {
it('should do xhr request and cache it', it('should do xhr request and cache it',
inject(function($rootScope, $httpBackend, $compile) { inject(function($rootScope, $httpBackend, $compile) {
element = $compile('<ng:include src="url"></ng:include>')($rootScope); element = $compile('<div><ng:include src="url"></ng:include></div>')($rootScope);
$httpBackend.expect('GET', 'myUrl').respond('my partial'); $httpBackend.expect('GET', 'myUrl').respond('my partial');
$rootScope.url = 'myUrl'; $rootScope.url = 'myUrl';
@ -206,7 +207,7 @@ describe('ngInclude', function() {
it('should clear content when error during xhr request', it('should clear content when error during xhr request',
inject(function($httpBackend, $compile, $rootScope) { inject(function($httpBackend, $compile, $rootScope) {
element = $compile('<ng:include src="url">content</ng:include>')($rootScope); element = $compile('<div><ng:include src="url">content</ng:include></div>')($rootScope);
$httpBackend.expect('GET', 'myUrl').respond(404, ''); $httpBackend.expect('GET', 'myUrl').respond(404, '');
$rootScope.url = 'myUrl'; $rootScope.url = 'myUrl';
@ -220,7 +221,7 @@ describe('ngInclude', function() {
it('should be async even if served from cache', inject( it('should be async even if served from cache', inject(
putIntoCache('myUrl', 'my partial'), putIntoCache('myUrl', 'my partial'),
function($rootScope, $compile) { function($rootScope, $compile) {
element = $compile('<ng:include src="url"></ng:include>')($rootScope); element = $compile('<div><ng:include src="url"></ng:include></div>')($rootScope);
$rootScope.url = 'myUrl'; $rootScope.url = 'myUrl';
@ -237,7 +238,7 @@ describe('ngInclude', function() {
it('should discard pending xhr callbacks if a new template is requested before the current ' + it('should discard pending xhr callbacks if a new template is requested before the current ' +
'finished loading', inject(function($rootScope, $compile, $httpBackend) { 'finished loading', inject(function($rootScope, $compile, $httpBackend) {
element = jqLite("<ng:include src='templateUrl'></ng:include>"); element = jqLite("<div><ng:include src='templateUrl'></ng:include></div>");
var log = {}; var log = {};
$rootScope.templateUrl = 'myUrl1'; $rootScope.templateUrl = 'myUrl1';
@ -273,6 +274,10 @@ describe('ngInclude', function() {
$rootScope.tpl = 'tpl.html'; $rootScope.tpl = 'tpl.html';
}); });
expect(onload).toHaveBeenCalledOnce(); expect(onload).toHaveBeenCalledOnce();
$rootScope.tpl = '';
$rootScope.$digest();
dealoc(element);
})); }));
@ -308,14 +313,14 @@ describe('ngInclude', function() {
it('should call $anchorScroll if autoscroll attribute is present', inject( it('should call $anchorScroll if autoscroll attribute is present', inject(
compileAndLink('<ng:include src="tpl" autoscroll></ng:include>'), compileAndLink('<div><ng:include src="tpl" autoscroll></ng:include></div>'),
changeTplAndValueTo('template.html'), function() { changeTplAndValueTo('template.html'), function() {
expect(autoScrollSpy).toHaveBeenCalledOnce(); expect(autoScrollSpy).toHaveBeenCalledOnce();
})); }));
it('should call $anchorScroll if autoscroll evaluates to true', inject( it('should call $anchorScroll if autoscroll evaluates to true', inject(
compileAndLink('<ng:include src="tpl" autoscroll="value"></ng:include>'), compileAndLink('<div><ng:include src="tpl" autoscroll="value"></ng:include></div>'),
changeTplAndValueTo('template.html', true), changeTplAndValueTo('template.html', true),
changeTplAndValueTo('another.html', 'some-string'), changeTplAndValueTo('another.html', 'some-string'),
changeTplAndValueTo('template.html', 100), function() { changeTplAndValueTo('template.html', 100), function() {
@ -325,14 +330,14 @@ describe('ngInclude', function() {
it('should not call $anchorScroll if autoscroll attribute is not present', inject( it('should not call $anchorScroll if autoscroll attribute is not present', inject(
compileAndLink('<ng:include src="tpl"></ng:include>'), compileAndLink('<div><ng:include src="tpl"></ng:include></div>'),
changeTplAndValueTo('template.html'), function() { changeTplAndValueTo('template.html'), function() {
expect(autoScrollSpy).not.toHaveBeenCalled(); expect(autoScrollSpy).not.toHaveBeenCalled();
})); }));
it('should not call $anchorScroll if autoscroll evaluates to false', inject( it('should not call $anchorScroll if autoscroll evaluates to false', inject(
compileAndLink('<ng:include src="tpl" autoscroll="value"></ng:include>'), compileAndLink('<div><ng:include src="tpl" autoscroll="value"></ng:include></div>'),
changeTplAndValueTo('template.html', false), changeTplAndValueTo('template.html', false),
changeTplAndValueTo('template.html', undefined), changeTplAndValueTo('template.html', undefined),
changeTplAndValueTo('template.html', null), function() { changeTplAndValueTo('template.html', null), function() {
@ -377,13 +382,12 @@ describe('ngInclude animations', function() {
$templateCache.put('enter', [200, '<div>data</div>', {}]); $templateCache.put('enter', [200, '<div>data</div>', {}]);
$rootScope.tpl = 'enter'; $rootScope.tpl = 'enter';
element = $compile(html( element = $compile(html(
'<div ' + '<div><div ' +
'ng-include="tpl">' + 'ng-include="tpl">' +
'</div>' '</div></div>'
))($rootScope); ))($rootScope);
$rootScope.$digest(); $rootScope.$digest();
item = $animate.process('leave').element;
item = $animate.process('enter').element; item = $animate.process('enter').element;
expect(item.text()).toBe('data'); expect(item.text()).toBe('data');
})); }));
@ -394,13 +398,12 @@ describe('ngInclude animations', function() {
$templateCache.put('enter', [200, '<div>data</div>', {}]); $templateCache.put('enter', [200, '<div>data</div>', {}]);
$rootScope.tpl = 'enter'; $rootScope.tpl = 'enter';
element = $compile(html( element = $compile(html(
'<div ' + '<div><div ' +
'ng-include="tpl">' + 'ng-include="tpl">' +
'</div>' '</div></div>'
))($rootScope); ))($rootScope);
$rootScope.$digest(); $rootScope.$digest();
item = $animate.process('leave').element;
item = $animate.process('enter').element; item = $animate.process('enter').element;
expect(item.text()).toBe('data'); expect(item.text()).toBe('data');
@ -411,4 +414,30 @@ describe('ngInclude animations', function() {
expect(item.text()).toBe('data'); expect(item.text()).toBe('data');
})); }));
it('should animate two separate ngInclude elements',
inject(function($compile, $rootScope, $templateCache, $animate) {
var item;
$templateCache.put('one', [200, 'one', {}]);
$templateCache.put('two', [200, 'two', {}]);
$rootScope.tpl = 'one';
element = $compile(html(
'<div><div ' +
'ng-include="tpl">' +
'</div></div>'
))($rootScope);
$rootScope.$digest();
item = $animate.process('enter').element;
expect(item.text()).toBe('one');
$rootScope.tpl = 'two';
$rootScope.$digest();
var itemA = $animate.process('leave').element;
var itemB = $animate.process('enter').element;
expect(itemA.attr('ng-include')).toBe('tpl');
expect(itemB.attr('ng-include')).toBe('tpl');
expect(itemA).not.toEqual(itemB);
}));
}); });