Skip to content

feat(uiViewCache): outer most views can now be reused #2333

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

Closed
wants to merge 2 commits into from
Closed
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
25 changes: 22 additions & 3 deletions sample/app/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,17 +56,36 @@ angular.module('uiRouterSample', [

// Use a url of "/" to set a state as the "index".
url: "/",

persistent:true,
controller: ['$scope','$timeout',function($scope, $timeout){
$scope.date = new Date();
$scope.invalidated = false;
$scope.restored = false;
$scope.cleanCache = angular.noop;
$scope.$on('$viewRestored', function(evt, name){
$scope.restored = true;
var expires = new Date();
expires.setSeconds(expires.getSeconds()-10);
if($scope.date < expires){
$scope.invalidated = true;
$scope.cleanCache();
}
});
$scope.$on('$viewCached', function(evt, data){
$scope.cleanCache = data.reset;
});
}],
// Example of an inline template string. By default, templates
// will populate the ui-view within the parent state's template.
// For top level states, like this one, the parent template is
// the index.html file. So this template will be inserted into the
// ui-view within index.html.
template: '<p class="lead">Welcome to the UI-Router Demo</p>' +
template: '<p class="lead">Welcome to the UI-Router {{restored?"restored ":"fresh "}}{{invalidated?"but now invalidated ":""}}Demo</p>' +
'<p>Use the menu above to navigate. ' +
'Pay attention to the <code>$state</code> and <code>$stateParams</code> values below.</p>' +
'<p>Click these links—<a href="#/c?id=1">Alice</a> or ' +
'<a href="#/user/42">Bob</a>—to see a url redirect in action.</p>'
'<a href="#/user/42">Bob</a>—to see a url redirect in action.</p>'+
'Generation date: {{date}}'

})

Expand Down
3 changes: 2 additions & 1 deletion sample/app/contacts/contacts.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ angular.module('uiRouterSample.contacts', [
// Using a '.' within a state name declares a child within a parent.
// So you have a new state 'list' within the parent 'contacts' state.
.state('contacts.list', {

// it is ok to enable this flag for leafnodes (view that doesnt have inner views)
persistent: true,
// Using an empty url means that this child state will become active
// when its parent's url is navigated to. Urls of child states are
// automatically appended to the urls of their parent. So this state's
Expand Down
116 changes: 96 additions & 20 deletions src/viewDirective.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ function $ViewDirective( $state, $injector, $uiViewScroll, $interpolate)

return statics();
}

var directive = {
restrict: 'ECA',
terminal: true,
Expand All @@ -188,50 +188,92 @@ function $ViewDirective( $state, $injector, $uiViewScroll, $interpolate)
});

updateView(true);

function cleanupLastView() {
var persistent = currentScope && currentScope.$persistent;
var _previousEl = previousEl;
var _currentScope = currentScope;

if (_currentScope) {
if (currentScope && !persistent) {
_currentScope._willBeDestroyed = true;
}

function cleanOld() {
if (_previousEl) {
_previousEl.remove();
previousEl = null;

}

if (_currentScope) {
_currentScope.$destroy();
_currentScope = null;
}
}

if (currentEl) {
renderer.leave(currentEl, function() {
cleanOld();
previousEl = null;
});

if (currentEl) {
if (persistent) {
// This is not very pretty but there is no way to prevent ngAnimate from removing element in the end (after invoking `leave`).
// Standard `remove` clears out all relavant element data, approach below only removes element so that it could be safely attached to DOM again.
var el = currentEl;
// only unlink element, do not remove data
el.remove = function(){
var parent = el[0].parentNode;
if (parent) parent.removeChild(el[0]);
};
renderer.leave(el, function(){
previousEl = null;
// restore remove functionality
delete el.remove;
});
} else {
renderer.leave(currentEl, function() {
cleanOld();
previousEl = null;
});
}
previousEl = currentEl;
} else {
cleanOld();
previousEl = null;
currentEl = null;
}

currentEl = null;
currentScope = null;
}


function restoreFromCache(name, cached){

renderer.enter(cached.element, $element);
/**
* @ngdoc event
* @name ui.router.state.directive:ui-view#$viewRestored
* @eventOf ui.router.state.directive:ui-view
* @eventType emits on ui-view directive scope
* @description
* Fired once view is restored (only when persistent flag is set to true)
* @param {Object} event Event object.
* @param {Object} data, object with view name.
*/
//TODO: Potentialy proper $stateParams could be forwarded aswell.
cached.scope.$emit('$viewRestored', {name:name});

currentEl = cached.element;
currentScope = cached.scope;
}

function updateView(firstTime) {
var newScope,
name = getUiViewName(scope, attrs, $element, $interpolate),
previousLocals = name && $state.$current && $state.$current.locals[name];

if (!firstTime && previousLocals === latestLocals || scope._willBeDestroyed) return; // nothing to do
newScope = scope.$new();

latestLocals = $state.$current.locals[name];

var cached = $state.$current.persistent && $state.$current.viewCache && $state.$current.viewCache[name];
if (cached) {
cleanupLastView();
restoreFromCache(name, cached);
return;
}

newScope = scope.$new();

/**
* @ngdoc event
* @name ui.router.state.directive:ui-view#$viewContentLoading
Expand All @@ -247,15 +289,49 @@ function $ViewDirective( $state, $injector, $uiViewScroll, $interpolate)
newScope.$emit('$viewContentLoading', name);

var clone = $transclude(newScope, function(clone) {
var cached;
renderer.enter(clone, $element, function onUiViewEnter() {
if(currentScope) {
if (currentScope) {
currentScope.$emit('$viewContentAnimationEnded');
}

if (angular.isDefined(autoScrollExp) && !autoScrollExp || scope.$eval(autoScrollExp)) {
$uiViewScroll(clone);
}
// when controller is compiled
if (cached) {
/**
* @ngdoc event
* @name ui.router.state.directive:ui-view#$viewCached
* @eventOf ui.router.state.directive:ui-view
* @eventType emits on ui-view directive scope
* @description
* Fired once view is cached (only when persistent flag is set to true)
* @param {Object} event Event object.
* @param {Object} data, object with view name and reset function (clears cache for this view)
*/
var tmp = $state.$current;
cached.scope.$emit('$viewCached', {
name : name,
reset: function() {
delete tmp.viewCache[name];
}
});
}
});
// caching of persistent states
// TODO: figure out what to do with nested views, behavior is undefined now
if ($state.$current.persistent) {
if (!$state.$current.viewCache) {
$state.$current.viewCache = {};
}
cached = {
element: clone,
scope: newScope
};
cached.scope.$persistent = true;
$state.$current.viewCache[name] = cached;
}
cleanupLastView();
});

Expand Down