diff --git a/src/ngRoute/directive/ngView.js b/src/ngRoute/directive/ngView.js index 3074df49127d..c4add9ced888 100644 --- a/src/ngRoute/directive/ngView.js +++ b/src/ngRoute/directive/ngView.js @@ -15,8 +15,10 @@ ngRouteModule.directive('ngView', ngViewFactory); * configuration of the `$route` service. * * @animations - * enter - happens just after the ngView contents are changed (when the new view DOM element is inserted into the DOM) - * leave - happens just after the current ngView contents change and just before the former contents are removed from the DOM + * enter - animation is used to bring new content into the browser. + * leave - animation is used to animate existing content away. + * + * The enter and leave animation occur concurrently. * * @scope * @example @@ -30,10 +32,9 @@ ngRouteModule.directive('ngView', ngViewFactory); Gatsby: Ch4 | Scarlet Letter
-
+
+
+

$location.path() = {{main.$location.path()}}
@@ -60,20 +61,13 @@ ngRouteModule.directive('ngView', ngViewFactory); - .example-leave, .example-enter { + .view-example { -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s; -moz-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s; -ms-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s; -o-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s; transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s; - } - - .example-$animate-container { - position:relative; - height:100px; - } - .example-$animate-container > * { display:block; width:100%; border-left:1px solid black; @@ -86,15 +80,20 @@ ngRouteModule.directive('ngView', ngViewFactory); padding:10px; } - .example-enter { + .example-animate-container { + position:relative; + height:100px; + } + + .view-example.ng-enter { left:100%; } - .example-enter.example-enter-active { + .view-example.ng-enter.ng-enter-active { left:0; } - .example-leave { } - .example-leave.example-leave-active { + .view-example.ng-leave { } + .view-example.ng-leave.ng-leave-active { left:-100%; } @@ -164,57 +163,65 @@ function ngViewFactory( $route, $anchorScroll, $compile, $controller, return { restrict: 'ECA', terminal: true, - link: function(scope, element, attr) { - var lastScope, - onloadExp = attr.onload || ''; - - scope.$on('$routeChangeSuccess', update); - update(); - - - function destroyLastScope() { - if (lastScope) { - lastScope.$destroy(); - lastScope = null; + transclude: 'element', + compile: function(element, attr, linker) { + return function(scope, $element, attr) { + var currentScope, + currentElement, + onloadExp = attr.onload || ''; + + scope.$on('$routeChangeSuccess', update); + update(); + + function cleanupLastView() { + if (currentScope) { + currentScope.$destroy(); + currentScope = null; + } + if(currentElement) { + $animate.leave(currentElement); + currentElement = null; + } } - } - - function clearContent() { - $animate.leave(element.contents()); - destroyLastScope(); - } - function update() { - var locals = $route.current && $route.current.locals, - template = locals && locals.$template; - - if (template) { - clearContent(); - var enterElements = jqLite('
').html(template).contents(); - $animate.enter(enterElements, element); - - var link = $compile(enterElements), - current = $route.current, - controller; - - lastScope = current.scope = scope.$new(); - if (current.controller) { - locals.$scope = lastScope; - controller = $controller(current.controller, locals); - if (current.controllerAs) { - lastScope[current.controllerAs] = controller; - } - element.children().data('$ngControllerController', controller); + function update() { + var locals = $route.current && $route.current.locals, + template = locals && locals.$template; + + if (template) { + var newScope = scope.$new(); + linker(newScope, function(clone) { + cleanupLastView(); + + clone.html(template); + $animate.enter(clone, null, $element); + + var link = $compile(clone.contents()), + current = $route.current; + + currentScope = current.scope = newScope; + currentElement = clone; + + if (current.controller) { + locals.$scope = currentScope; + var controller = $controller(current.controller, locals); + if (current.controllerAs) { + currentScope[current.controllerAs] = controller; + } + clone.data('$ngControllerController', controller); + clone.contents().data('$ngControllerController', controller); + } + + link(currentScope); + currentScope.$emit('$viewContentLoaded'); + currentScope.$eval(onloadExp); + + // $anchorScroll might listen on event... + $anchorScroll(); + }); + } else { + cleanupLastView(); } - - link(lastScope); - lastScope.$emit('$viewContentLoaded'); - lastScope.$eval(onloadExp); - - // $anchorScroll might listen on event... - $anchorScroll(); - } else { - clearContent(); } } } diff --git a/test/ngRoute/directive/ngViewSpec.js b/test/ngRoute/directive/ngViewSpec.js index 5f021f2dc894..7d96a5819569 100644 --- a/test/ngRoute/directive/ngViewSpec.js +++ b/test/ngRoute/directive/ngViewSpec.js @@ -8,8 +8,7 @@ describe('ngView', function() { beforeEach(module(function($provide) { $provide.value('$window', angular.mock.createMockWindow()); return function($rootScope, $compile, $animate) { - element = $compile('')($rootScope); - $animate.enabled(true); + element = $compile('
')($rootScope); }; })); @@ -93,7 +92,7 @@ describe('ngView', function() { $rootScope.$digest(); expect($route.current.controller).toBe('MyCtrl'); - expect(MyCtrl).toHaveBeenCalledWith(element.contents().scope()); + expect(MyCtrl).toHaveBeenCalledWith(element.children().scope()); }); }); @@ -498,9 +497,12 @@ describe('ngView', function() { $rootScope.$digest(); forEach(element.contents(), function(node) { - if ( node.nodeType == 3 /* text node */) { + if(node.nodeType == 3 /* text node */) { expect(jqLite(node).scope()).not.toBe($route.current.scope); expect(jqLite(node).controller()).not.toBeDefined(); + } else if(node.nodeType == 8 /* comment node */) { + expect(jqLite(node).scope()).toBe(element.scope()); + expect(jqLite(node).controller()).toBe(element.controller()); } else { expect(jqLite(node).scope()).toBe($route.current.scope); expect(jqLite(node).controller()).toBeDefined(); @@ -508,163 +510,178 @@ describe('ngView', function() { }); }); }); +}); - describe('animations', function() { - var body, element, $rootElement; +describe('ngView animations', function() { + var body, element, $rootElement; - function html(html) { - $rootElement.html(html); - body.append($rootElement); - element = $rootElement.children().eq(0); - return element; - } + beforeEach(module('ngRoute')); - beforeEach(module(function() { - // we need to run animation on attached elements; - return function(_$rootElement_) { - $rootElement = _$rootElement_; - body = jqLite(document.body); - }; - })); + function html(html) { + $rootElement.html(html); + body.append($rootElement); + element = $rootElement.children().eq(0); + return element; + } + + beforeEach(module(function() { + // we need to run animation on attached elements; + return function(_$rootElement_) { + $rootElement = _$rootElement_; + body = jqLite(document.body); + }; + })); - afterEach(function(){ - dealoc(body); - dealoc(element); - }); + afterEach(function(){ + dealoc(body); + dealoc(element); + }); - beforeEach(module(function($provide, $routeProvider) { - $routeProvider.when('/foo', {controller: noop, templateUrl: '/foo.html'}); - return function($templateCache) { - $templateCache.put('/foo.html', [200, '
data
', {}]); - } - })); + beforeEach(module(function($provide, $routeProvider) { + $provide.value('$window', angular.mock.createMockWindow()); + $routeProvider.when('/foo', {controller: noop, templateUrl: '/foo.html'}); + $routeProvider.when('/bar', {controller: noop, templateUrl: '/bar.html'}); + return function($templateCache) { + $templateCache.put('/foo.html', [200, '
data
', {}]); + $templateCache.put('/bar.html', [200, '
data2
', {}]); + } + })); - describe('hooks', function() { - beforeEach(module('mock.animate')); + describe('hooks', function() { + beforeEach(module('mock.animate')); - it('should fire off the enter animation', - inject(function($compile, $rootScope, $location, $animate) { - var item; - element = $compile(html('
'))($rootScope); + it('should fire off the enter animation', + inject(function($compile, $rootScope, $location, $animate) { + element = $compile(html('
'))($rootScope); - $location.path('/foo'); - $rootScope.$digest(); + $location.path('/foo'); + $rootScope.$digest(); - item = $animate.process('leave').element; - item = $animate.process('leave').element; - item = $animate.process('leave').element; + var item = $animate.process('enter').element; + expect(item.text()).toBe('data'); + })); - item = $animate.process('enter').element; - expect(item.text()).toBe('data'); + it('should fire off the leave animation', + inject(function($compile, $rootScope, $location, $templateCache, $animate) { - item = $animate.process('leave').element; - item = $animate.process('enter').element; - expect(item.text()).toBe('data'); - })); + var item; + $templateCache.put('/foo.html', [200, '
foo
', {}]); + element = $compile(html('
'))($rootScope); - it('should fire off the leave animation', - inject(function($compile, $rootScope, $location, $templateCache, $animate) { + $location.path('/foo'); + $rootScope.$digest(); + + item = $animate.process('enter').element; + expect(item.text()).toBe('foo'); + + $location.path('/'); + $rootScope.$digest(); + item = $animate.process('leave').element; + expect(item.text()).toBe('foo'); + })); + + it('should animate two separate ngView elements', + inject(function($compile, $rootScope, $templateCache, $animate, $location) { var item; - $templateCache.put('/foo.html', [200, '
foo
', {}]); - element = $compile(html('
'))($rootScope); + $rootScope.tpl = 'one'; + element = $compile(html('
'))($rootScope); + $rootScope.$digest(); $location.path('/foo'); $rootScope.$digest(); - item = $animate.process('leave').element; - item = $animate.process('leave').element; - item = $animate.process('leave').element; - item = $animate.process('enter').element; - expect(item.text()).toBe('foo'); + expect(item.text()).toBe('data'); - item = $animate.process('leave').element; - item = $animate.process('enter').element; - expect(item.text()).toBe('foo'); - - $location.path('/'); + $location.path('/bar'); $rootScope.$digest(); - item = $animate.process('leave').element; - expect(item.text()).toBe('foo'); - - item = $animate.process('leave').element; - expect(item.text()).toBe('foo'); - })); - }); - - it('should not double compile when route changes', function() { - module('ngAnimate'); - module('mock.animate'); - module(function($routeProvider, $animateProvider, $provide) { - $routeProvider.when('/foo', {template: '
{{i}}
'}); - $routeProvider.when('/bar', {template: '
{{i}}
'}); - $animateProvider.register('.my-animation', function() { - return { - leave: function(element, done) { - dump('yes'); - done(); - } - }; - }); + var itemA = $animate.process('leave').element; + expect(itemA).not.toEqual(itemB); + var itemB = $animate.process('enter').element; + })); + }); + + it('should not double compile when the route changes', function() { + + module('ngAnimate'); + module('mock.animate'); + + var window; + module(function($routeProvider, $animateProvider, $provide) { + $provide.value('$window', window = angular.mock.createMockWindow()); + $routeProvider.when('/foo', {template: '
{{i}}
'}); + $routeProvider.when('/bar', {template: '
{{i}}
'}); + $animateProvider.register('.my-animation', function() { + return { + leave: function(element, done) { + done(); + } + }; }); + }); - inject(function($rootScope, $compile, $location, $route, $window, $rootElement, $sniffer, $animate) { - if (!$sniffer.transitions) return; + inject(function($rootScope, $compile, $location, $route, $window, $rootElement, $sniffer, $animate) { + element = $compile(html('
'))($rootScope); + $animate.enabled(true); - element = $compile(html(''))($rootScope); + $location.path('/foo'); + $rootScope.$digest(); - $location.path('/foo'); - $rootScope.$digest(); + $animate.process('enter'); //ngView - $animate.process('leave'); - $animate.process('leave'); - $animate.process('leave'); - $animate.process('enter'); - $animate.process('leave'); - $animate.process('enter'); - $animate.process('enter'); - $animate.process('enter'); - $animate.process('enter'); - $animate.process('enter'); + if($sniffer.transitions) { $window.setTimeout.expect(1).process(); + $window.setTimeout.expect(0).process(); + } + + $animate.process('enter'); //repeat 1 + $animate.process('enter'); //repeat 2 + + if($sniffer.transitions) { $window.setTimeout.expect(1).process(); $window.setTimeout.expect(1).process(); $window.setTimeout.expect(0).process(); $window.setTimeout.expect(0).process(); - $window.setTimeout.expect(0).process(); + } - expect(element.text()).toEqual('12'); + expect(element.text()).toEqual('12'); - $location.path('/bar'); - $rootScope.$digest(); - $animate.process('leave'); - $animate.process('enter'); - $animate.process('leave'); - $animate.process('enter'); - $animate.process('enter'); - $animate.process('enter'); - $animate.process('enter'); - $animate.process('enter'); - expect(n(element.text())).toEqual('1234'); + $location.path('/bar'); + $rootScope.$digest(); + $animate.process('leave'); //ngView old + if($sniffer.transitions) { $window.setTimeout.expect(1).process(); + $window.setTimeout.expect(0).process(); + } + + $animate.process('enter'); //ngView new + if($sniffer.transitions) { $window.setTimeout.expect(1).process(); + $window.setTimeout.expect(0).process(); + } + + expect(n(element.text())).toEqual(''); //this is midway during the animation + + $animate.process('enter'); //ngRepeat 3 + $animate.process('enter'); //ngRepeat 4 + + + if($sniffer.transitions) { $window.setTimeout.expect(1).process(); $window.setTimeout.expect(1).process(); $window.setTimeout.expect(0).process(); $window.setTimeout.expect(0).process(); - $window.setTimeout.expect(0).process(); - $window.setTimeout.expect(0).process(); + } - expect(element.text()).toEqual('34'); + expect(element.text()).toEqual('34'); - function n(text) { - return text.replace(/\r\n/m, '').replace(/\r\n/m, ''); - } - }); + function n(text) { + return text.replace(/\r\n/m, '').replace(/\r\n/m, ''); + } }); }); });