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
+});