diff --git a/src/viewDirective.js b/src/viewDirective.js index 3712f1b37..d80443ada 100644 --- a/src/viewDirective.js +++ b/src/viewDirective.js @@ -1,39 +1,51 @@ -$ViewDirective.$inject = ['$state', '$compile', '$controller', '$anchorScroll', '$injector']; -function $ViewDirective($state, $compile, $controller, $anchorScroll, $injector) { +$ViewDirective.$inject = ['$state', '$compile', '$controller', '$anchorScroll', '$injector', '$rootScope']; +function $ViewDirective($state, $compile, $controller, $anchorScroll, $injector, $rootScope) { var viewIsUpdating = false, $animate = $injector.has('$animate') ? $injector.get('$animate') : null; - // Returns a set of DOM manipulation functions based on whether animation - // should be performed - var renderer = function (doAnimate) { - return ({ - "true": { - leave: function (element) { $animate.leave(element); }, - enter: function (element, anchor) { $animate.enter(element, null, anchor); } - }, - "false": { - leave: function (element) { element.remove(); }, - enter: function (element, anchor) { anchor.after(element); } - } - })[($animate && doAnimate).toString()]; - }; - var directive = { restrict: 'ECA', - compile: function (element, attrs) { - var defaultContent = element.html(), isDefault = true, - anchor = angular.element(document.createComment(' ui-view ')); + priority: 1000, + terminal: true, + transclude: 'element', + compile: function (element, attrs, transclude) { + return function ($scope, $element, $attrs) { + // Returns a set of DOM manipulation functions based on whether animation + // should be performed + var renderer = function (doAnimate) { + function animationEvent(animationType) { + $rootScope.$broadcast('$viewAnimationStart', animationType, $state.$current); + return function() { + $rootScope.$broadcast('$viewAnimationEnd', animationType, $state.$current); + }; + } - element.prepend(anchor); + if(doAnimate && $animate) { + return { + leave: function (view) { $animate.leave(view, animationEvent('leave')); }, + enter: function (view) { $animate.enter(view, null, $element, animationEvent('enter')); } + }; + } + else { + return { + leave: function (view) { view.remove(); }, + enter: function (view) { $element.after(view); } + }; + } + }; - return function ($scope) { var currentScope, currentElement, viewLocals, + noop = function() {}, name = attrs[directive.name] || attrs.name || '', - onloadExp = attrs.onload || ''; + onloadExp = attrs.onload || '', + initialView = transclude($scope, noop); + + currentElement = initialView; + $element.after(currentElement); - var parent = element.parent().inheritedData('$uiView'); + var parent = $element.parent().inheritedData('$uiView'); if (name.indexOf('@') < 0) name = name + '@' + (parent ? parent.state.name : ''); var view = { name: name, state: null }; @@ -50,70 +62,54 @@ function $ViewDirective($state, $compile, $controller, $anchorScroll, $injector) $scope.$on('$stateChangeSuccess', eventHook); $scope.$on('$viewContentLoading', eventHook); - updateView(false); - function cleanupLastView() { - if (currentElement) { - renderer(true).leave(currentElement); - currentElement = null; - } - - if (currentScope) { - currentScope.$destroy(); - currentScope = null; - } - } - function updateView(doAnimate) { var locals = $state.$current && $state.$current.locals[name], render = renderer(doAnimate); - if (isDefault) { - isDefault = false; - element.replaceWith(anchor); - } + if (locals === viewLocals) return; // nothing to do - if (!locals) { - cleanupLastView(); - currentElement = element.clone(); - currentElement.html(defaultContent); - render.enter(currentElement, anchor); + // remove old view + render.leave(currentElement); - currentScope = $scope.$new(); - $compile(currentElement.contents())(currentScope); - return; + if (currentScope) { + currentScope.$destroy(); + currentScope = null; } - if (locals === viewLocals) return; // nothing to do - - cleanupLastView(); - - currentElement = element.clone(); - currentElement.html(locals.$template ? locals.$template : defaultContent); - render.enter(currentElement, anchor); + // if empty view, restore initial view + if(!locals) { + currentElement = initialView; + render.enter(currentElement); + viewLocals = undefined; + view.state = undefined; + return; + } + // update the view with a new clone + currentElement = transclude($scope, noop); + currentElement.html(locals.$template); currentElement.data('$uiView', view); viewLocals = locals; view.state = locals.$$state; var link = $compile(currentElement.contents()); - currentScope = $scope.$new(); if (locals.$$controller) { locals.$scope = currentScope; var controller = $controller(locals.$$controller, locals); - currentElement.children().data('$ngControllerController', controller); + currentElement.contents().data('$ngControllerController', controller); } link(currentScope); + render.enter(currentElement); currentScope.$emit('$viewContentLoaded'); if (onloadExp) currentScope.$eval(onloadExp); - // TODO: This seems strange, shouldn't $anchorScroll listen for $viewContentLoaded if necessary? // $anchorScroll might listen on event... $anchorScroll(); } diff --git a/test/viewDirectiveSpec.js b/test/viewDirectiveSpec.js index cf4ce28df..7a3a33915 100644 --- a/test/viewDirectiveSpec.js +++ b/test/viewDirectiveSpec.js @@ -16,7 +16,7 @@ describe('uiView', function () { var scope, $compile, elem; beforeEach(function() { - angular.module('ui.router.test', ['ui.router', 'ngAnimate']); + angular.module('ui.router.test', ['ui.router']); module('ui.router.test'); module('mock.animate'); }); @@ -63,6 +63,14 @@ describe('uiView', function () { template: 'hState inner template' } } + }, + iState = { + template: '
'+ + ''+ + '
' + }, + jState = { + template: 'jState' }; beforeEach(module(function ($stateProvider) { @@ -74,7 +82,9 @@ describe('uiView', function () { .state('e', eState) .state('e.f', fState) .state('g', gState) - .state('g.h', hState); + .state('g.h', hState) + .state('i', iState) + .state('j', jState); })); beforeEach(inject(function ($rootScope, _$compile_) { @@ -141,6 +151,20 @@ describe('uiView', function () { expect($animate.flushNext('leave').element.text()).toBe(''); expect(innerText($animate.flushNext('enter').element.parent()[0].querySelector('.view').querySelector('.eview'))).toBe(fState.views.eview.template); })); + + it('should compile the cloned element', inject(function ($state, $q, $animate) { + scope.isTest = false; + + $compile(elem.append('
'))(scope); + + scope.isTest = true; + scope.$apply(); + + $animate.flushNext('addClass'); + + var child = angular.element(elem.children()[0]); + expect(child.hasClass('test')).toBe(true); + })); }); describe('handling initial view', function () { @@ -153,14 +177,9 @@ describe('uiView', function () { $state.transitionTo(gState); $q.flush(); - // Leave elem expect($animate.flushNext('leave').element.text()).toBe(""); - // Enter and leave ui-view insert of template - $animate.flushNext('enter'); - $animate.flushNext('leave'); - // Enter again after $scope.digest() expect($animate.flushNext('enter').element.text()).toEqual(content); - // Evaluate addClass + var item = $animate.flushNext('addClass').element; expect(item.text()).toEqual(content); })); @@ -174,7 +193,6 @@ describe('uiView', function () { $state.transitionTo(hState); $q.flush(); - expect($animate.queue.length).toEqual(4); expect($animate.flushNext('leave').element.text()).toBe(''); expect($animate.flushNext('enter').element.text()).toBe(hState.views.inner.template); @@ -188,6 +206,62 @@ describe('uiView', function () { expect($animate.flushNext('leave').element.text()).toBe(hState.views.inner.template); expect($animate.flushNext('enter').element.text()).toBe(content); })); + + it('initial view should be transcluded once to prevent breaking other directives', inject(function ($state, $q, $animate) { + + scope.items = ["I", "am", "a", "list", "of", "items"]; + + elem.append($compile('
')(scope)); + + function renderList() { + for (var i = 0; i < scope.items.length; i++) { + expect($animate.flushNext('enter').element.text()).toBe(scope.items[i]); + } + } + + // transition to state that has an initial view + $state.transitionTo(iState); + $q.flush(); + + expect($animate.flushNext('leave').element.text()).toBe(''); + expect($animate.flushNext('enter').element.text()).toBe(''); // ngRepeat items not yet entered + + // render the list items + renderList(); + + // verify if ng-repeat has been compiled + expect(elem.find('li').length).toBe(scope.items.length); + + // transition to another state that replace the initial content + $state.transitionTo(jState); + $q.flush(); + + expect($animate.flushNext('leave').element.text()).toBe(scope.items.join('')); + expect($animate.flushNext('enter').element.text()).toBe('jState'); + + // transition back to the state with empty subview and the initial view + $state.transitionTo(iState); + $q.flush(); + + expect($animate.flushNext('leave').element.text()).toBe('jState'); + expect($animate.flushNext('enter').element.text()).toBe(''); // ngRepeat items not yet entered + + renderList(); + + // verify if the initial view is correct + expect(elem.find('li').length).toBe(scope.items.length); + + // change scope properties + scope.$apply(function () { + scope.items.push(".", "Working?"); + }); + + expect($animate.flushNext('enter').element.text()).toBe('.'); + expect($animate.flushNext('enter').element.text()).toBe('Working?'); + + // verify if the initial view has been updated + expect(elem.find('li').length).toBe(scope.items.length); + })); }); });