mirror of
https://github.com/Hopiu/angular.js.git
synced 2026-04-10 01:51:00 +00:00
chore(ngView): $animate refactoring + transclusion & tests
BREAKING CHANGE: previously ngView only updated its content, after this change ngView 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:
parent
aa2133ad81
commit
7d69d52acf
2 changed files with 230 additions and 206 deletions
|
|
@ -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);
|
|||
<a href="Book/Gatsby/ch/4?key=value">Gatsby: Ch4</a> |
|
||||
<a href="Book/Scarlet">Scarlet Letter</a><br/>
|
||||
|
||||
<div
|
||||
ng-view
|
||||
class="example-$animate-container"
|
||||
ng-$animate="{enter: 'example-enter', leave: 'example-leave'}"></div>
|
||||
<div class="example-animate-container">
|
||||
<div ng-view class="view-example"></div>
|
||||
</div>
|
||||
<hr />
|
||||
|
||||
<pre>$location.path() = {{main.$location.path()}}</pre>
|
||||
|
|
@ -60,20 +61,13 @@ ngRouteModule.directive('ngView', ngViewFactory);
|
|||
</file>
|
||||
|
||||
<file name="animations.css">
|
||||
.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%;
|
||||
}
|
||||
</file>
|
||||
|
|
@ -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 || '';
|
||||
transclude: 'element',
|
||||
compile: function(element, attr, linker) {
|
||||
return function(scope, $element, attr) {
|
||||
var currentScope,
|
||||
currentElement,
|
||||
onloadExp = attr.onload || '';
|
||||
|
||||
scope.$on('$routeChangeSuccess', update);
|
||||
update();
|
||||
scope.$on('$routeChangeSuccess', update);
|
||||
update();
|
||||
|
||||
|
||||
function destroyLastScope() {
|
||||
if (lastScope) {
|
||||
lastScope.$destroy();
|
||||
lastScope = 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('<div></div>').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 cleanupLastView() {
|
||||
if (currentScope) {
|
||||
currentScope.$destroy();
|
||||
currentScope = null;
|
||||
}
|
||||
if(currentElement) {
|
||||
$animate.leave(currentElement);
|
||||
currentElement = null;
|
||||
}
|
||||
}
|
||||
|
||||
link(lastScope);
|
||||
lastScope.$emit('$viewContentLoaded');
|
||||
lastScope.$eval(onloadExp);
|
||||
function update() {
|
||||
var locals = $route.current && $route.current.locals,
|
||||
template = locals && locals.$template;
|
||||
|
||||
// $anchorScroll might listen on event...
|
||||
$anchorScroll();
|
||||
} else {
|
||||
clearContent();
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,8 +8,7 @@ describe('ngView', function() {
|
|||
beforeEach(module(function($provide) {
|
||||
$provide.value('$window', angular.mock.createMockWindow());
|
||||
return function($rootScope, $compile, $animate) {
|
||||
element = $compile('<ng:view onload="load()"></ng:view>')($rootScope);
|
||||
$animate.enabled(true);
|
||||
element = $compile('<div><ng:view onload="load()"></ng:view></div>')($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'));
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
|
||||
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, '<div>data</div>', {}]);
|
||||
$templateCache.put('/bar.html', [200, '<div>data2</div>', {}]);
|
||||
}
|
||||
}));
|
||||
|
||||
beforeEach(module(function() {
|
||||
// we need to run animation on attached elements;
|
||||
return function(_$rootElement_) {
|
||||
$rootElement = _$rootElement_;
|
||||
body = jqLite(document.body);
|
||||
};
|
||||
describe('hooks', function() {
|
||||
beforeEach(module('mock.animate'));
|
||||
|
||||
it('should fire off the enter animation',
|
||||
inject(function($compile, $rootScope, $location, $animate) {
|
||||
element = $compile(html('<div ng-view></div>'))($rootScope);
|
||||
|
||||
$location.path('/foo');
|
||||
$rootScope.$digest();
|
||||
|
||||
var item = $animate.process('enter').element;
|
||||
expect(item.text()).toBe('data');
|
||||
}));
|
||||
|
||||
it('should fire off the leave animation',
|
||||
inject(function($compile, $rootScope, $location, $templateCache, $animate) {
|
||||
|
||||
var item;
|
||||
$templateCache.put('/foo.html', [200, '<div>foo</div>', {}]);
|
||||
element = $compile(html('<div ng-view></div>'))($rootScope);
|
||||
|
||||
$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');
|
||||
}));
|
||||
|
||||
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, '<div>data</div>', {}]);
|
||||
}
|
||||
}));
|
||||
|
||||
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('<div ng-view></div>'))($rootScope);
|
||||
|
||||
$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('data');
|
||||
|
||||
item = $animate.process('leave').element;
|
||||
item = $animate.process('enter').element;
|
||||
expect(item.text()).toBe('data');
|
||||
}));
|
||||
|
||||
it('should fire off the leave animation',
|
||||
inject(function($compile, $rootScope, $location, $templateCache, $animate) {
|
||||
|
||||
it('should animate two separate ngView elements',
|
||||
inject(function($compile, $rootScope, $templateCache, $animate, $location) {
|
||||
var item;
|
||||
$templateCache.put('/foo.html', [200, '<div>foo</div>', {}]);
|
||||
element = $compile(html('<div ng-view></div>'))($rootScope);
|
||||
$rootScope.tpl = 'one';
|
||||
element = $compile(html('<div><div ng-view></div></div>'))($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');
|
||||
|
||||
item = $animate.process('leave').element;
|
||||
item = $animate.process('enter').element;
|
||||
expect(item.text()).toBe('foo');
|
||||
|
||||
$location.path('/');
|
||||
$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: '<div ng-repeat="i in [1,2]">{{i}}</div>'});
|
||||
$routeProvider.when('/bar', {template: '<div ng-repeat="i in [3,4]">{{i}}</div>'});
|
||||
$animateProvider.register('.my-animation', function() {
|
||||
return {
|
||||
leave: function(element, done) {
|
||||
dump('yes');
|
||||
done();
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
inject(function($rootScope, $compile, $location, $route, $window, $rootElement, $sniffer, $animate) {
|
||||
if (!$sniffer.transitions) return;
|
||||
|
||||
element = $compile(html('<ng:view onload="load()"></ng:view>'))($rootScope);
|
||||
|
||||
$location.path('/foo');
|
||||
$rootScope.$digest();
|
||||
|
||||
$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');
|
||||
$window.setTimeout.expect(1).process();
|
||||
$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(item.text()).toBe('data');
|
||||
|
||||
$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');
|
||||
|
||||
$window.setTimeout.expect(1).process();
|
||||
$window.setTimeout.expect(1).process();
|
||||
$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();
|
||||
var itemA = $animate.process('leave').element;
|
||||
expect(itemA).not.toEqual(itemB);
|
||||
var itemB = $animate.process('enter').element;
|
||||
}));
|
||||
});
|
||||
|
||||
expect(element.text()).toEqual('34');
|
||||
it('should not double compile when the route changes', function() {
|
||||
|
||||
function n(text) {
|
||||
return text.replace(/\r\n/m, '').replace(/\r\n/m, '');
|
||||
}
|
||||
module('ngAnimate');
|
||||
module('mock.animate');
|
||||
|
||||
var window;
|
||||
module(function($routeProvider, $animateProvider, $provide) {
|
||||
$provide.value('$window', window = angular.mock.createMockWindow());
|
||||
$routeProvider.when('/foo', {template: '<div ng-repeat="i in [1,2]">{{i}}</div>'});
|
||||
$routeProvider.when('/bar', {template: '<div ng-repeat="i in [3,4]">{{i}}</div>'});
|
||||
$animateProvider.register('.my-animation', function() {
|
||||
return {
|
||||
leave: function(element, done) {
|
||||
done();
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
inject(function($rootScope, $compile, $location, $route, $window, $rootElement, $sniffer, $animate) {
|
||||
element = $compile(html('<div><ng:view onload="load()" class="my-animation"></ng:view></div>'))($rootScope);
|
||||
$animate.enabled(true);
|
||||
|
||||
$location.path('/foo');
|
||||
$rootScope.$digest();
|
||||
|
||||
$animate.process('enter'); //ngView
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
expect(element.text()).toEqual('12');
|
||||
|
||||
$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();
|
||||
}
|
||||
|
||||
expect(element.text()).toEqual('34');
|
||||
|
||||
function n(text) {
|
||||
return text.replace(/\r\n/m, '').replace(/\r\n/m, '');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue