diff --git a/sample/empty.content.html b/sample/empty.content.html new file mode 100644 index 000000000..ac9be0156 --- /dev/null +++ b/sample/empty.content.html @@ -0,0 +1,3 @@ +

Current initial view title:

+

+

diff --git a/sample/empty.html b/sample/empty.html new file mode 100644 index 000000000..1deaa39c8 --- /dev/null +++ b/sample/empty.html @@ -0,0 +1,5 @@ +
This view contains a nested view below:
+
+

{{data.initialViewTitle}}

+

+
diff --git a/sample/index.html b/sample/index.html index 313d3aba6..4d8a55588 100644 --- a/sample/index.html +++ b/sample/index.html @@ -1,7 +1,7 @@ - + - + ui-router @@ -195,6 +195,32 @@ function ($timeout) { return $timeout(function () { return "Hello world" }, 100); }], + }) + .state('empty', { + url: '/empty', + templateUrl: 'empty.html', + controller: + [ '$scope', '$state', + function ($scope, $state) { + // Using an object to access it via ng-model from child scope + $scope.data = { + initialViewTitle: "I am an initial view" + } + $scope.changeInitialViewTitle = function($event) { + $state.transitionTo('empty.emptycontent'); + }; + $scope.showInitialView = function($event) { + $state.transitionTo('empty'); + }; + }] + }) + .state('empty.emptycontent', { + url: '/content', + views: { + 'emptycontent': { + templateUrl: 'empty.content.html' + } + } }); }]) .run( diff --git a/src/viewDirective.js b/src/viewDirective.js index 0dba40754..07e539554 100644 --- a/src/viewDirective.js +++ b/src/viewDirective.js @@ -7,80 +7,86 @@ function $ViewDirective( $state, $compile, $controller, $injector, $an var directive = { restrict: 'ECA', terminal: true, - link: function(scope, element, attr) { - var viewScope, viewLocals, - initialContent = element.contents(), - name = attr[directive.name] || attr.name || '', - onloadExp = attr.onload || '', - animate = isDefined($animator) && $animator(scope, attr); + transclude: true, + compile: function (element, attr, transclude) { + return function(scope, element, attr) { + var viewScope, viewLocals, + name = attr[directive.name] || attr.name || '', + onloadExp = attr.onload || '', + animate = isDefined($animator) && $animator(scope, attr); - // Find the details of the parent view directive (if any) and use it - // to derive our own qualified view name, then hang our own details - // off the DOM so child directives can find it. - var parent = element.parent().inheritedData('$uiView'); - if (name.indexOf('@') < 0) name = name + '@' + (parent ? parent.state.name : ''); - var view = { name: name, state: null }; - element.data('$uiView', view); + // Put back the compiled initial view + element.append(transclude(scope)); - scope.$on('$stateChangeSuccess', function() { updateView(true); }); - updateView(false); + // Find the details of the parent view directive (if any) and use it + // to derive our own qualified view name, then hang our own details + // off the DOM so child directives can find it. + var parent = element.parent().inheritedData('$uiView'); + if (name.indexOf('@') < 0) name = name + '@' + (parent ? parent.state.name : ''); + var view = { name: name, state: null }; + element.data('$uiView', view); - function updateView(doAnimate) { - var locals = $state.$current && $state.$current.locals[name]; - if (locals === viewLocals) return; // nothing to do + scope.$on('$stateChangeSuccess', function() { updateView(true); }); + updateView(false); - // Remove existing content - if (animate && doAnimate) { - animate.leave(element.contents(), element); - } else { - element.html(''); - } - - // Destroy previous view scope - if (viewScope) { - viewScope.$destroy(); - viewScope = null; - } - - if (locals) { - viewLocals = locals; - view.state = locals.$$state; + function updateView(doAnimate) { + var locals = $state.$current && $state.$current.locals[name]; + if (locals === viewLocals) return; // nothing to do - var contents; + // Remove existing content if (animate && doAnimate) { - contents = angular.element('
').html(locals.$template).contents(); - animate.enter(contents, element); + animate.leave(element.contents(), element); } else { - element.html(locals.$template); - contents = element.contents(); + element.html(''); } - var link = $compile(contents); - viewScope = scope.$new(); - if (locals.$$controller) { - locals.$scope = viewScope; - var controller = $controller(locals.$$controller, locals); - element.children().data('$ngControllerController', controller); + // Destroy previous view scope + if (viewScope) { + viewScope.$destroy(); + viewScope = null; } - link(viewScope); - viewScope.$emit('$viewContentLoaded'); - viewScope.$eval(onloadExp); - // TODO: This seems strange, shouldn't $anchorScroll listen for $viewContentLoaded if necessary? - // $anchorScroll might listen on event... - $anchorScroll(); - } else { - viewLocals = null; - view.state = null; + if (locals) { + viewLocals = locals; + view.state = locals.$$state; - // Restore initial view - if (doAnimate) { - animate.enter(initialContent, element); + var contents; + if (animate && doAnimate) { + contents = angular.element('
').html(locals.$template).contents(); + animate.enter(contents, element); + } else { + element.html(locals.$template); + contents = element.contents(); + } + + var link = $compile(contents); + viewScope = scope.$new(); + if (locals.$$controller) { + locals.$scope = viewScope; + var controller = $controller(locals.$$controller, locals); + element.children().data('$ngControllerController', controller); + } + link(viewScope); + viewScope.$emit('$viewContentLoaded'); + viewScope.$eval(onloadExp); + + // TODO: This seems strange, shouldn't $anchorScroll listen for $viewContentLoaded if necessary? + // $anchorScroll might listen on event... + $anchorScroll(); } else { - element.html(initialContent); + viewLocals = null; + view.state = null; + + // Restore the initial view + var compiledElem = transclude(scope); + if (animate && doAnimate) { + animate.enter(compiledElem, element); + } else { + element.append(compiledElem); + } } } - } + }; } }; return directive; diff --git a/test/viewDirectiveSpec.js b/test/viewDirectiveSpec.js index e69de29bb..164cd7ca6 100644 --- a/test/viewDirectiveSpec.js +++ b/test/viewDirectiveSpec.js @@ -0,0 +1,158 @@ +/*jshint browser: true, indent: 2 */ +/*global describe: false, it: false, beforeEach: false, expect: false, resolvedValue: false, module: false, inject: false, angular: false */ + +describe('uiView', function () { + 'use strict'; + + var scope, $compile, elem; + + beforeEach(module('ui.state')); + + var aState = { + template: 'aState template' + }, + bState = { + template: 'bState template' + }, + cState = { + views: { + 'cview': { + template: 'cState cview template' + } + } + }, + dState = { + views: { + 'dview1': { + template: 'dState dview1 template' + }, + 'dview2': { + template: 'dState dview2 template' + } + } + }, + eState = { + template: '
' + }, + fState = { + views: { + 'eview': { + template: 'fState eview template' + } + } + }, + gState = { + template: '
{{content}}
' + }, + hState = { + views: { + 'inner': { + template: 'hState inner template' + } + } + }; + + beforeEach(module(function ($stateProvider) { + $stateProvider + .state('a', aState) + .state('b', bState) + .state('c', cState) + .state('d', dState) + .state('e', eState) + .state('e.f', fState) + .state('g', gState) + .state('g.h', hState); + })); + + beforeEach(inject(function ($rootScope, _$compile_) { + scope = $rootScope.$new(); + $compile = _$compile_; + elem = angular.element('
'); + })); + + describe('linking ui-directive', function () { + it('anonymous ui-view should be replaced with the template of the current $state', inject(function ($state, $q) { + elem.append($compile('
')(scope)); + + $state.transitionTo(aState); + $q.flush(); + + expect(elem.text()).toBe(aState.template); + })); + + it('named ui-view should be replaced with the template of the current $state', inject(function ($state, $q) { + elem.append($compile('
')(scope)); + + $state.transitionTo(cState); + $q.flush(); + + expect(elem.text()).toBe(cState.views.cview.template); + })); + + it('ui-view should be updated after transition to another state', inject(function ($state, $q) { + elem.append($compile('
')(scope)); + + $state.transitionTo(aState); + $q.flush(); + + expect(elem.text()).toBe(aState.template); + + $state.transitionTo(bState); + $q.flush(); + + expect(elem.text()).toBe(bState.template); + })); + + it('should handle NOT nested ui-views', inject(function ($state, $q) { + elem.append($compile('
')(scope)); + + $state.transitionTo(dState); + $q.flush(); + + expect(elem[0].querySelector('.dview1').innerText).toBe(dState.views.dview1.template); + expect(elem[0].querySelector('.dview2').innerText).toBe(dState.views.dview2.template); + })); + + it('should handle nested ui-views (testing two levels deep)', inject(function ($state, $q) { + elem.append($compile('
')(scope)); + + $state.transitionTo(fState); + $q.flush(); + + expect(elem[0].querySelector('.view').querySelector('.eview').innerText).toBe(fState.views.eview.template); + })); + }); + + describe('handling initial view', function () { + it('initial view should be compiled if the view is empty', inject(function ($state, $q) { + var content = 'inner content'; + + elem.append($compile('
')(scope)); + scope.$apply('content = "' + content + '"'); + + $state.transitionTo(gState); + $q.flush(); + + expect(elem[0].querySelector('.test').innerText).toBe(content); + })); + + it('initial view should be put back after removal of the view', inject(function ($state, $q) { + var content = 'inner content'; + + elem.append($compile('
')(scope)); + scope.$apply('content = "' + content + '"'); + + $state.transitionTo(hState); + $q.flush(); + + expect(elem.text()).toBe(hState.views.inner.template); + + // going to the parent state which makes the inner view empty + $state.transitionTo(gState); + $q.flush(); + + expect(elem[0].querySelector('.test').innerText).toBe(content); + })); + }); + +});