Skip to content

Transcluding the of ui-view directive, ui-view spec, example of the initial view in the sample app #171

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 22, 2013
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions sample/empty.content.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<h3>Current initial view title:</h3>
<p><input type="text" ng-model="data.initialViewTitle"></p>
<p><button class="btn" ng-click="showInitialView($event)">Back</button></p>
5 changes: 5 additions & 0 deletions sample/empty.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<div>This view contains a nested view below:</div>
<div ui-view="emptycontent">
<h3>{{data.initialViewTitle}}</h3>
<p><button class="btn" ng-click="changeInitialViewTitle($event)">Change</button></p>
</div>
30 changes: 28 additions & 2 deletions sample/index.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<!doctype html>
<html lang="en" ng-app="sample"><head>
<meta charset="utf-8">
<link rel="stylesheet" type="text/css" href="bootstrap.min.css">
<link rel="stylesheet" type="text/css" href="bootstrap.min.css">
<style type="text/css">
.fade-enter-setup, .fade-leave-setup {
transition: opacity 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) 0s;
Expand All @@ -17,7 +17,7 @@
</style>
<script src="../lib/angular-1.1.4.js"></script>
<script src="../build/angular-ui-router.js"></script>

<!-- could easily use a custom property of the state here instead of 'name' -->
<title ng-bind="$state.current.name + ' - ui-router'">ui-router</title>
</head><body>
Expand Down Expand Up @@ -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(
Expand Down
124 changes: 65 additions & 59 deletions src/viewDirective.js
Original file line number Diff line number Diff line change
Expand Up @@ -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('<div></div>').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('<div></div>').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;
Expand Down
158 changes: 158 additions & 0 deletions test/viewDirectiveSpec.js
Original file line number Diff line number Diff line change
@@ -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: '<div ui-view="eview" class="eview"></div>'
},
fState = {
views: {
'eview': {
template: 'fState eview template'
}
}
},
gState = {
template: '<div ui-view="inner"><span ng-class="{ test: true }">{{content}}</span></div>'
},
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('<div>');
}));

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('<div ui-view></div>')(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('<div ui-view="cview"></div>')(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('<div ui-view></div>')(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('<div ui-view="dview1" class="dview1"></div><div ui-view="dview2" class="dview2"></div>')(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('<div ui-view class="view"></div>')(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('<div ui-view></div>')(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('<div ui-view></div>')(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);
}));
});

});