Skip to content

Commit 9d2e80a

Browse files
author
Cody Lundquist
committed
fix(uiView): Properly handle uiView within a ngIf
Use the linking function element instead of the template element. Closes angular-ui#857
1 parent d9f7b89 commit 9d2e80a

File tree

2 files changed

+155
-106
lines changed

2 files changed

+155
-106
lines changed

src/viewDirective.js

Lines changed: 120 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -137,31 +137,34 @@ function $ViewDirective( $state, $compile, $controller, $injector, $ui
137137

138138
// Returns a set of DOM manipulation functions based on whether animation
139139
// should be performed
140-
function getRenderer(element, attrs, scope) {
140+
function getRenderer(attrs, scope) {
141141
var statics = function() {
142142
return {
143-
leave: function (element) { element.remove(); },
144-
enter: function (element, parent, anchor) { anchor.after(element); }
143+
enter: function (element, target, cb) {
144+
target.after(element);
145+
target.remove();
146+
if (typeof cb === 'function') cb();
147+
},
148+
leave: function (element, cb) {
149+
element.remove();
150+
if (typeof cb === 'function') cb();
151+
}
145152
};
146153
};
147154

148155
if ($animate) {
149-
return function(shouldAnimate) {
150-
return !shouldAnimate ? statics() : {
151-
enter: function(element, parent, anchor) { $animate.enter(element, null, anchor); },
152-
leave: function(element) { $animate.leave(element, function() { element.remove(); }); }
153-
};
156+
return {
157+
enter: function(element, target, cb) { $animate.enter(element, null, target, cb || angular.noop); },
158+
leave: function(element, cb) { $animate.leave(element, cb || angular.noop); }
154159
};
155160
}
156161

157162
if ($animator) {
158163
var animate = $animator && $animator(scope, attrs);
159164

160-
return function(shouldAnimate) {
161-
return !shouldAnimate ? statics() : {
162-
enter: function(element, parent, anchor) { animate.enter(element, parent); },
163-
leave: function(element) { animate.leave(element.contents(), element); }
164-
};
165+
return {
166+
enter: function(element, target, cb) { animate.enter(element, target, cb || angular.noop); },
167+
leave: function(element, cb) { animate.leave(element.contents(), element, cb || angular.noop); }
165168
};
166169
}
167170

@@ -170,118 +173,130 @@ function $ViewDirective( $state, $compile, $controller, $injector, $ui
170173

171174
var directive = {
172175
restrict: 'ECA',
173-
compile: function (element, attrs) {
174-
var initial = element.html(),
175-
isDefault = true,
176-
anchor = angular.element($document[0].createComment(' ui-view-anchor ')),
177-
parentEl = element.parent();
178-
179-
element.prepend(anchor);
180-
181-
return function ($scope) {
182-
var inherited = parentEl.inheritedData('$uiView');
183-
184-
var currentScope, currentEl, viewLocals,
185-
name = attrs[directive.name] || attrs.name || '',
186-
onloadExp = attrs.onload || '',
187-
autoscrollExp = attrs.autoscroll,
188-
renderer = getRenderer(element, attrs, $scope);
189-
190-
if (name.indexOf('@') < 0) name = name + '@' + (inherited ? inherited.state.name : '');
191-
var view = { name: name, state: null };
192-
193-
var eventHook = function () {
194-
if (viewIsUpdating) return;
195-
viewIsUpdating = true;
196-
197-
try { updateView(true); } catch (e) {
198-
viewIsUpdating = false;
199-
throw e;
200-
}
176+
terminal: true,
177+
priority: 400,
178+
transclude: 'element',
179+
link: function (scope, $element, attrs, ctrl, $transclude) {
180+
var currentScope, currentEl, previousEl, viewLocals,
181+
onloadExp = attrs.onload || '',
182+
autoscrollExp = attrs.autoscroll,
183+
renderer = getRenderer(attrs, scope),
184+
parentEl = $element.parent(),
185+
inherited = parentEl.inheritedData('$uiView'),
186+
name = attrs[directive.name] || attrs.name || '';
187+
188+
if (name.indexOf('@') < 0) name = name + '@' + (inherited ? inherited.state.name : '');
189+
190+
var eventHook = function () {
191+
if (viewIsUpdating) return;
192+
193+
viewIsUpdating = true;
194+
195+
try { updateView(); } catch (e) {
196+
throw e;
197+
} finally {
201198
viewIsUpdating = false;
202-
};
203-
204-
$scope.$on('$stateChangeSuccess', eventHook);
205-
$scope.$on('$viewContentLoading', eventHook);
199+
}
200+
};
206201

207-
updateView(false);
202+
scope.$on('$stateChangeSuccess', eventHook);
203+
scope.$on('$viewContentLoading', eventHook);
204+
updateView();
208205

209-
function cleanupLastView() {
210-
if (currentEl) {
211-
renderer(true).leave(currentEl);
212-
currentEl = null;
213-
}
206+
function cleanupLastView() {
207+
if (previousEl) {
208+
previousEl.remove();
209+
previousEl = null;
210+
}
214211

215-
if (currentScope) {
216-
currentScope.$destroy();
217-
currentScope = null;
218-
}
212+
if (currentScope) {
213+
currentScope.$destroy();
214+
currentScope = null;
219215
}
220216

221-
function updateView(shouldAnimate) {
222-
var locals = $state.$current && $state.$current.locals[name];
217+
if (currentEl) {
218+
renderer.leave(currentEl, function() {
219+
previousEl = null;
220+
});
223221

224-
if (isDefault) {
225-
isDefault = false;
226-
element.replaceWith(anchor);
227-
}
222+
previousEl = currentEl;
223+
currentEl = null;
224+
}
225+
}
228226

229-
if (!locals) {
230-
cleanupLastView();
231-
currentEl = element.clone();
232-
currentEl.html(initial);
233-
renderer(shouldAnimate).enter(currentEl, parentEl, anchor);
227+
function updateView() {
228+
var newScope = scope.$new(),
229+
locals = $state.$current && $state.$current.locals[name];
234230

235-
currentScope = $scope.$new();
236-
$compile(currentEl.contents())(currentScope);
237-
return;
238-
}
231+
if (locals === viewLocals) return; // nothing to do
239232

240-
if (locals === viewLocals) return; // nothing to do
233+
viewLocals = locals;
241234

235+
var clone = $transclude(newScope, function(clone) {
236+
clone.data('$uiViewName', name);
237+
renderer.enter(clone, currentEl || $element, function onUiViewEnter () {
238+
if (!angular.isDefined(autoscrollExp) || !autoscrollExp || scope.$eval(autoscrollExp)) {
239+
$uiViewScroll(clone);
240+
}
241+
});
242242
cleanupLastView();
243+
});
244+
245+
currentEl = clone;
246+
currentScope = newScope;
247+
248+
/**
249+
* @ngdoc event
250+
* @name ui.router.state.directive:ui-view#$viewContentLoaded
251+
* @eventOf ui.router.state.directive:ui-view
252+
* @eventType emits on ui-view directive scope
253+
* @description *
254+
* Fired once the view is **loaded**, *after* the DOM is rendered.
255+
*
256+
* @param {Object} event Event object.
257+
*/
258+
currentScope.$emit('$viewContentLoaded');
259+
if (onloadExp) currentScope.$eval(onloadExp);
260+
}
261+
}
262+
};
243263

244-
currentEl = element.clone();
245-
currentEl.html(locals.$template ? locals.$template : initial);
246-
renderer(true).enter(currentEl, parentEl, anchor);
264+
return directive;
265+
}
247266

248-
currentEl.data('$uiView', view);
267+
$ViewDirectiveFill.$inject = ['$compile', '$controller', '$state'];
268+
function $ViewDirectiveFill ($compile, $controller, $state) {
269+
var directive = {
270+
restrict: 'ECA',
271+
priority: -400,
272+
compile: function(tElement) {
273+
var initial = tElement.html();
249274

250-
viewLocals = locals;
251-
view.state = locals.$$state;
275+
return function(scope, $element) {
276+
var current = $state.$current,
277+
name = $element.data('$uiViewName'),
278+
locals = current && current.locals[name];
252279

253-
var link = $compile(currentEl.contents());
280+
if (! locals) {
281+
return;
282+
}
254283

255-
currentScope = $scope.$new();
284+
$element.data('$uiView', { name: name, state: locals.$$state });
285+
$element.html(locals.$template ? locals.$template : initial);
256286

257-
if (locals.$$controller) {
258-
locals.$scope = currentScope;
259-
var controller = $controller(locals.$$controller, locals);
260-
if ($state.$current.controllerAs) {
261-
currentScope[$state.$current.controllerAs] = controller;
262-
}
263-
currentEl.children().data('$ngControllerController', controller);
264-
}
287+
var link = $compile($element.contents());
265288

266-
link(currentScope);
267-
268-
/**
269-
* @ngdoc event
270-
* @name ui.router.state.directive:ui-view#$viewContentLoaded
271-
* @eventOf ui.router.state.directive:ui-view
272-
* @eventType emits on ui-view directive scope
273-
* @description *
274-
* Fired once the view is **loaded**, *after* the DOM is rendered.
275-
*
276-
* @param {Object} event Event object.
277-
*/
278-
currentScope.$emit('$viewContentLoaded');
279-
if (onloadExp) currentScope.$eval(onloadExp);
280-
281-
if (!angular.isDefined(autoscrollExp) || !autoscrollExp || $scope.$eval(autoscrollExp)) {
282-
$uiViewScroll(currentEl);
289+
if (locals.$$controller) {
290+
locals.$scope = scope;
291+
var controller = $controller(locals.$$controller, locals);
292+
if (current.controllerAs) {
293+
scope[current.controllerAs] = controller;
283294
}
295+
$element.data('$ngControllerController', controller);
296+
$element.children().data('$ngControllerController', controller);
284297
}
298+
299+
link(scope);
285300
};
286301
}
287302
};
@@ -290,3 +305,4 @@ function $ViewDirective( $state, $compile, $controller, $injector, $ui
290305
}
291306

292307
angular.module('ui.router.state').directive('uiView', $ViewDirective);
308+
angular.module('ui.router.state').directive('uiView', $ViewDirectiveFill);

test/viewDirectiveSpec.js

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ describe('uiView', function () {
8282
controller: function() {
8383
this.someProperty = "value"
8484
},
85-
controllerAs: "vm",
85+
controllerAs: "vm"
8686
};
8787

8888
beforeEach(module(function ($stateProvider) {
@@ -275,6 +275,39 @@ describe('uiView', function () {
275275
// verify if the initial view has been updated
276276
expect(elem.find('li').length).toBe(scope.items.length);
277277
}));
278+
279+
// related to issue #857
280+
it('should handle ui-view inside ng-if', inject(function ($state, $q, $compile, $animate) {
281+
elem.append($compile('<div ng-if="someBoolean"><ui-view></ui-view></div>')(scope));
282+
283+
scope.someBoolean = false;
284+
285+
$state.transitionTo(aState);
286+
$q.flush();
287+
if ($animate) $animate.flush();
288+
289+
// Verify there is no ui-view in the DOM
290+
expect(elem.find('ui-view').length).toBe(0);
291+
292+
// Turn on the div that holds the ui-view
293+
scope.$apply(function() {
294+
scope.someBoolean = true;
295+
});
296+
297+
if ($animate) $animate.flush();
298+
299+
// Verify that the ui-view is there and it has the correct content
300+
expect(elem.find('ui-view').text()).toBe(aState.template);
301+
302+
scope.$apply(function() {
303+
scope.someBoolean = false;
304+
});
305+
306+
if ($animate) $animate.flush();
307+
308+
// Verify there is no ui-view in the DOM
309+
expect(elem.find('ui-view').length).toBe(0);
310+
}));
278311
});
279312

280313
describe('autoscroll attribute', function () {
@@ -325,4 +358,4 @@ describe('uiView', function () {
325358
}))
326359
});
327360

328-
});
361+
});

0 commit comments

Comments
 (0)