diff --git a/src/viewDirective.js b/src/viewDirective.js index 4f2e2c169..c67bdf72d 100644 --- a/src/viewDirective.js +++ b/src/viewDirective.js @@ -115,8 +115,8 @@ * * */ -$ViewDirective.$inject = ['$state', '$compile', '$controller', '$injector', '$uiViewScroll', '$document']; -function $ViewDirective( $state, $compile, $controller, $injector, $uiViewScroll, $document) { +$ViewDirective.$inject = ['$state', '$injector', '$uiViewScroll']; +function $ViewDirective( $state, $injector, $uiViewScroll) { function getService() { return ($injector.has) ? function(service) { @@ -135,135 +135,98 @@ function $ViewDirective( $state, $compile, $controller, $injector, $ui $animator = service('$animator'), $animate = service('$animate'); - // Returns a set of DOM manipulation functions based on whether animation - // should be performed - function getRenderer(element, attrs, scope) { + // Returns a set of DOM manipulation functions based on which Angular version + // it should use + function getRenderer(attrs, scope) { var statics = function() { return { - leave: function (element) { element.remove(); }, - enter: function (element, parent, anchor) { anchor.after(element); } + enter: function (element, target) { target.after(element); }, + leave: function (element) { element.remove(); } }; }; if ($animate) { - return function(shouldAnimate) { - return !shouldAnimate ? statics() : { - enter: function(element, parent, anchor) { $animate.enter(element, null, anchor); }, - leave: function(element) { $animate.leave(element, function() { element.remove(); }); } - }; + return { + enter: function(element, target) { $animate.enter(element, null, target); }, + leave: function(element) { $animate.leave(element); } }; } if ($animator) { var animate = $animator && $animator(scope, attrs); - return function(shouldAnimate) { - return !shouldAnimate ? statics() : { - enter: function(element, parent, anchor) { animate.enter(element, parent); }, - leave: function(element) { animate.leave(element.contents(), element); } - }; + return { + enter: function(element, target) { animate.enter(element, null, target); }, + leave: function(element) { animate.leave(element.contents(), element); } }; } - return statics; + return statics(); } var directive = { restrict: 'ECA', - compile: function (element, attrs) { - var initial = element.html(), - isDefault = true, - anchor = angular.element($document[0].createComment(' ui-view-anchor ')), - parentEl = element.parent(); - - element.prepend(anchor); - - return function ($scope) { - var inherited = parentEl.inheritedData('$uiView'); - + terminal: true, + priority: 400, + transclude: 'element', + compile: function (tElement, tAttrs, $transclude) { + return function (scope, $element, attrs) { var currentScope, currentEl, viewLocals, - name = attrs[directive.name] || attrs.name || '', - onloadExp = attrs.onload || '', + loaded = false, + onloadExp = attrs.onload || '', autoscrollExp = attrs.autoscroll, - renderer = getRenderer(element, attrs, $scope); + renderer = getRenderer(attrs, scope), + parentEl = $element.parent(), + inherited = parentEl.inheritedData('$uiView'), + name = attrs[directive.name] || attrs.name || ''; if (name.indexOf('@') < 0) name = name + '@' + (inherited ? inherited.state.name : ''); - var view = { name: name, state: null }; var eventHook = function () { if (viewIsUpdating) return; + viewIsUpdating = true; - try { updateView(true); } catch (e) { - viewIsUpdating = false; + try { updateView(); } catch (e) { throw e; + } finally { + viewIsUpdating = false; } - viewIsUpdating = false; }; - $scope.$on('$stateChangeSuccess', eventHook); - $scope.$on('$viewContentLoading', eventHook); - - updateView(false); + scope.$on('$stateChangeSuccess', eventHook); + scope.$on('$viewContentLoading', eventHook); + updateView(); function cleanupLastView() { - if (currentEl) { - renderer(true).leave(currentEl); - currentEl = null; - } - if (currentScope) { currentScope.$destroy(); currentScope = null; } - } - - function updateView(shouldAnimate) { - var locals = $state.$current && $state.$current.locals[name]; - if (isDefault) { - isDefault = false; - element.replaceWith(anchor); - } - - if (!locals) { - cleanupLastView(); - currentEl = element.clone(); - currentEl.html(initial); - renderer(shouldAnimate).enter(currentEl, parentEl, anchor); - - currentScope = $scope.$new(); - $compile(currentEl.contents())(currentScope); - return; + if (currentEl) { + renderer.leave(currentEl); + currentEl = null; } + } - if (locals === viewLocals) return; // nothing to do - - cleanupLastView(); - - currentEl = element.clone(); - currentEl.html(locals.$template ? locals.$template : initial); - renderer(true).enter(currentEl, parentEl, anchor); + function updateView() { + var newScope = scope.$new(), + locals = $state.$current && $state.$current.locals[name]; - currentEl.data('$uiView', view); + if (loaded && locals === viewLocals) return; // nothing to do + loaded = true; viewLocals = locals; - view.state = locals.$$state; - var link = $compile(currentEl.contents()); - - currentScope = $scope.$new(); - - if (locals.$$controller) { - locals.$scope = currentScope; - var controller = $controller(locals.$$controller, locals); - if ($state.$current.controllerAs) { - currentScope[$state.$current.controllerAs] = controller; - } - currentEl.children().data('$ngControllerController', controller); - } + var clone = $transclude(newScope, function(clone) { + clone.data('$uiViewName', name); + renderer.enter(clone, currentEl || $element); + cleanupLastView(); + }); - link(currentScope); + currentEl = clone; + currentScope = newScope; /** * @ngdoc event @@ -278,7 +241,7 @@ function $ViewDirective( $state, $compile, $controller, $injector, $ui currentScope.$emit('$viewContentLoaded'); if (onloadExp) currentScope.$eval(onloadExp); - if (!angular.isDefined(autoscrollExp) || !autoscrollExp || $scope.$eval(autoscrollExp)) { + if (!angular.isDefined(autoscrollExp) || !autoscrollExp || scope.$eval(autoscrollExp)) { $uiViewScroll(currentEl); } } @@ -289,4 +252,43 @@ function $ViewDirective( $state, $compile, $controller, $injector, $ui return directive; } +$ViewDirectiveFill.$inject = ['$compile', '$controller', '$state']; +function $ViewDirectiveFill ($compile, $controller, $state) { + return { + restrict: 'ECA', + priority: -400, + compile: function (tElement) { + var initial = tElement.html(); + + return function (scope, $element) { + var current = $state.$current, + name = $element.data('$uiViewName'), + locals = current && current.locals[name]; + + if (!locals) { + return; + } + + $element.data('$uiView', { name: name, state: locals.$$state }); + $element.html(locals.$template ? locals.$template : initial); + + var link = $compile($element.contents()); + + if (locals.$$controller) { + locals.$scope = scope; + var controller = $controller(locals.$$controller, locals); + if (current.controllerAs) { + scope[current.controllerAs] = controller; + } + $element.data('$ngControllerController', controller); + $element.children().data('$ngControllerController', controller); + } + + link(scope); + }; + } + }; +} + angular.module('ui.router.state').directive('uiView', $ViewDirective); +angular.module('ui.router.state').directive('uiView', $ViewDirectiveFill); diff --git a/test/viewDirectiveSpec.js b/test/viewDirectiveSpec.js index 8e1cf17a7..70ec78fed 100644 --- a/test/viewDirectiveSpec.js +++ b/test/viewDirectiveSpec.js @@ -82,7 +82,7 @@ describe('uiView', function () { controller: function() { this.someProperty = "value" }, - controllerAs: "vm", + controllerAs: "vm" }; beforeEach(module(function ($stateProvider) { @@ -110,72 +110,81 @@ describe('uiView', function () { it('anonymous ui-view should be replaced with the template of the current $state', inject(function ($state, $q, $animate) { elem.append($compile('
')(scope)); + if ($animate) $animate.flushNext('enter'); $state.transitionTo(aState); + $q.flush(); if ($animate) { - expect($animate.flushNext('leave').element.text()).toBe(''); + expect(elem.find('ui-view').text()).toBe(''); expect($animate.flushNext('enter').element.text()).toBe(aState.template); } })); it('named ui-view should be replaced with the template of the current $state', inject(function ($state, $q, $animate) { elem.append($compile('
')(scope)); + if ($animate) $animate.flushNext('enter'); $state.transitionTo(cState); $q.flush(); if ($animate) { - expect($animate.flushNext('leave').element.text()).toBe(''); + expect(elem.find('ui-view[name="cview"]').text()).toBe(''); expect($animate.flushNext('enter').element.text()).toBe(cState.views.cview.template); } })); it('ui-view should be updated after transition to another state', inject(function ($state, $q, $animate) { elem.append($compile('
')(scope)); + if ($animate) $animate.flushNext('enter'); $state.transitionTo(aState); $q.flush(); if ($animate) { - expect($animate.flushNext('leave').element.text()).toBe(''); + expect(elem.find('ui-view').text()).toBe(''); expect($animate.flushNext('enter').element.text()).toBe(aState.template); + $animate.flushNext('leave'); } $state.transitionTo(bState); $q.flush(); if ($animate) { - expect($animate.flushNext('leave').element.text()).toBe(aState.template); expect($animate.flushNext('enter').element.text()).toBe(bState.template); } })); it('should handle NOT nested ui-views', inject(function ($state, $q, $animate) { elem.append($compile('
')(scope)); + if ($animate) { + $animate.flushNext('enter'); + $animate.flushNext('enter'); + } $state.transitionTo(dState); $q.flush(); if ($animate) { - expect($animate.flushNext('leave').element.html()).toBe(''); + expect(elem.find('ui-view[name="dview1"]').text()).toBe(''); expect($animate.flushNext('enter').element.text()).toBe(dState.views.dview1.template); - expect($animate.flushNext('leave').element.html()).toBe(''); + $animate.flushNext('leave'); + expect(elem.find('ui-view[name="dview2"]').text()).toBe(''); expect($animate.flushNext('enter').element.text()).toBe(dState.views.dview2.template); } })); it('should handle nested ui-views (testing two levels deep)', inject(function ($state, $q, $animate) { $compile(elem.append('
'))(scope); - + if ($animate) $animate.flushNext('enter'); $state.transitionTo(fState); $q.flush(); if ($animate) { - expect($animate.flushNext('leave').element.text()).toBe(''); + expect(elem.find('ui-view').text()).toBe(''); expect($animate.flushNext('enter').element.parent().find('.view')).toMatchText(''); - + $animate.flushNext('leave'); var target = $animate.flushNext('enter').element; expect(target).toHaveClass('eview'); expect(target).toMatchText(fState.views.eview.template); @@ -186,22 +195,21 @@ describe('uiView', function () { describe('handling initial view', function () { it('initial view should be compiled if the view is empty', inject(function ($state, $q, $animate) { var content = 'inner content'; - elem.append($compile('
')(scope)); + if ($animate) $animate.flushNext('enter'); + scope.$apply('content = "' + content + '"'); $state.transitionTo(gState); $q.flush(); if ($animate) { - var target = $animate.flushNext('leave').element; + var target = elem.find('ui-view'); expect(target.text()).toBe(""); $animate.flushNext('enter'); $animate.flushNext('leave'); $animate.flushNext('enter'); - $animate.flushNext('addClass'); - $animate.flushNext('addClass'); - target = $animate.flushNext('addClass').element; expect(target).toHaveClass('test'); expect(target.text()).toBe(content); @@ -212,14 +220,16 @@ describe('uiView', function () { var content = 'inner content'; elem.append($compile('
')(scope)); + if ($animate) $animate.flushNext('enter'); + scope.$apply('content = "' + content + '"'); $state.transitionTo(hState); $q.flush(); if ($animate) { - expect($animate.flushNext('leave').element.text()).toBe(''); - expect($animate.flushNext('enter').element.text()).toBe(''); + $animate.flushNext('enter'); + $animate.flushNext('leave'); expect($animate.flushNext('enter').element.text()).toBe(hState.views.inner.template); expect($animate.flushNext('addClass').element.text()).toBe(content); @@ -227,8 +237,8 @@ describe('uiView', function () { $state.transitionTo(gState); $q.flush(); - expect($animate.flushNext('leave').element).toMatchText(hState.views.inner.template); $animate.flushNext('enter'); + expect($animate.flushNext('leave').element).toMatchText(hState.views.inner.template); var target = $animate.flushNext('addClass').element; expect(target).toHaveClass('test'); @@ -275,6 +285,48 @@ describe('uiView', function () { // verify if the initial view has been updated expect(elem.find('li').length).toBe(scope.items.length); })); + + // related to issue #857 + it('should handle ui-view inside ng-if', inject(function ($state, $q, $compile, $animate) { + // ngIf does not exist in 1.0.8 + if (angular.version.full === '1.0.8') return; + + scope.someBoolean = false; + elem.append($compile('
')(scope)); + + $state.transitionTo(aState); + $q.flush(); + + // Verify there is no ui-view in the DOM + expect(elem.find('ui-view').length).toBe(0); + + // Turn on the div that holds the ui-view + scope.someBoolean = true; + scope.$digest(); + + if ($animate) $animate.flush(); + // Verify that the ui-view is there and it has the correct content + expect(elem.find('ui-view').text()).toBe(aState.template); + + scope.someBoolean = false; + scope.$digest(); + + if ($animate) { + $animate.flush(); + scope.$digest(); + } + + // Verify there is no ui-view in the DOM + expect(elem.find('ui-view').length).toBe(0); + + // Turn on the div that holds the ui-view once again + scope.someBoolean = true; + scope.$digest(); + + if ($animate) $animate.flush(); + // Verify that the ui-view is there and it has the correct content + expect(elem.find('ui-view').text()).toBe(aState.template); + })); }); describe('autoscroll attribute', function () { @@ -319,10 +371,10 @@ describe('uiView', function () { elem.append($compile('
{{vm.someProperty}}
')(scope)); $state.transitionTo(kState); $q.flush(); - var innerScope = scope.$$childHead - expect(innerScope.vm).not.toBeUndefined() - expect(innerScope.vm.someProperty).toBe("value") + var innerScope = scope.$$childHead; + expect(innerScope.vm).not.toBeUndefined(); + expect(innerScope.vm.someProperty).toBe("value"); })) }); -}); \ No newline at end of file +});