Optional support for keeping a view around #63

coli opened this Issue Mar 26, 2013 · 47 comments


None yet

coli commented Mar 26, 2013

Add an option to a state that tells ui-view to keep it around even after route changes (then switch back to the old one instead of re-create it)

Use case is using ui-view for the content section of a tab.


ksperling commented Mar 27, 2013

Hmmm, if you want to keep all tabs in memory even when they're not visible, maybe it's better to use ng-show or ui-show or something like that?

If we had an option for ui-view to keep old view instances (i.e. DOM + scope + controllers) around, we would need to have some way of preventing the number of 'back' views and memory used by the from growing out of bounds.

coli commented Mar 28, 2013

I'd imaging it's done per ui-view. So if the parent ui-view changes, the child ui-view will all be destroyed.

Hmm, the ability to arbitrary target named views makes this complicated.

coli commented Mar 28, 2013

It just occured to me this can be done at the state,view level.


  views: {
    'filters@report': {
      templateUrl: 'report-filters.html',
      persist: true

Then the specific ui-view would keep that template around, and it'll only be destroyed when the parent changes. This assumes that the view targeted is the same ui-view though...

coli commented Mar 28, 2013

Actually, restrict the usage to absolute view targeting would make it work for now. Am I right?

coli commented Mar 28, 2013

Also, if you think this is workable, I'll go ahead and implement it, we need something like it :) (for our tabs which contain iframe... )


timkindberg commented Mar 28, 2013

Isn't this what $templateCache is for?


ksperling commented Mar 29, 2013

I'd rather wait a bit with implementing this until we have the core features working fully. It's starting to become more apparent to me that we should really factor out view management into a separate $view service, which is where this sort of behaviour could then be plugged in.

I'm still not convinced it's really the right thing to do in many cases though -- needing to keep the DOM and controllers for these tabs around seems to imply that the DOM and controllers are holding more state than they should be?

coli commented Mar 29, 2013

Ah, okay. In my case, iframe has an extra restriction, if you pull them out of the dom tree then reinsert them, the iframe get's reloaded. Which we don't want when we host separate iframe in tabs. In chrome there is a workaround but other browser doesn't.

coli commented Mar 29, 2013

@timkindberg this is about keeping the dom (and associated controller/scope/etc) around hidden. It's mainly for tabs.


ksperling commented Mar 31, 2013

@coli I wonder if you'd also need to detach the scope from it's parent somehow, to avoid your hidden tabs receiving events and stuff like that.

coli commented Apr 3, 2013

@ksperling I never thought about it ;) Hmm, I'm not sure... although if I model it after ng-show, it means keep everything around, including the scope. There might be events like view-show, view-hidden, that the child can listen to, if they really want.


jeme commented Apr 3, 2013

@coli We have established as much as this is about tab, but what are you thinking in terms of fitting it into the state machine, is this because you are trying to have urls that would point to the page but open a specific tab etc?... Like


In which case we are talking only a single tab view pr. page, alternatively:


which would allow for:


In any case, I am asking for the use case, to see if there would be an alternative way of doing this while leaving the ui-router alone... Ultimately I think that modeling a tab-view as a directive would make sense, tabs could still lazy load, but just need to know what link to the routing your looking for.


  <ui-tab title="Some Title"> Content </ui-tab>
  <ui-tab title="Other Title" src="othertab.html" />

Then either let the tab view capture things after "?"... The stateProvider would need to support "ReloadOnSearc=False" though... OR through some sort of active tab binding to the scope we could maybe let sub-states control the current tab.

I am just trying to think out of the box, as allowing for the "persist" on the views seems like something that is more complex than it might sound...

Edit: Actually, might even be able to fit it up against:



coli commented Apr 3, 2013

@jeme I'm thinking of something like


Where tab as a concept disappears and becomes a row of navigation buttons :) I need the ability to nest tabs, just like ui-view. It makes the most sense to turn tabs into a list of buttons that does navigate. Then have ui-view host the tab content. And you get arbitrary nesting/url/navi for free.

coli commented Apr 3, 2013

Currently I actually have /section?tab=tab1, but it breaks down as soon as I try to nest more navigation into it.


jeme commented Apr 3, 2013

@coli Ok so overall we are sort of talking a single tab view pr. level of state...So we would have:


but not:



?? (Just as a conceptual structure)

Although overall, for an application example as you link to, I wouldn't recommend keeping the DOM of these views around as that could end up consuming quite allot of memory.

Alternatively, since it's the iFrame that is causing problems, maybe look at some sort of iframe cache implementation. Ill just try to play around with something.

coli commented Apr 3, 2013

@jeme Yup

I'd like to do

<button ng-repeat="tab in tabs" ng-click="navigate(tab.stateName)>{{tab.title}}</button>

Which could then load anything including more tabs!

I don't think memory is going to be much of an issue, could be a famous last word but :)

Iframe workaround exists for Chrome. But definitely not IE. Actually, I can think of a nasty hack if I use js to relocate via style a iframe off of root, but I don't want to do that...


jeme commented Apr 3, 2013

@coli Y ok, that would be how such a cache somehow would work, because when you move an iframe element in the DOM it reloads.

Another option for now could be a custom ui-cached-view... something like:

var $CachedViewDirective =
         ['$state', '$compile', '$controller', '$anchorScroll',
function ($state, $compile, $controller, $anchorScroll) {
    return {
        restrict: 'ECA',
        terminal: true,
        link: function (scope, element, attr) {
            var viewScope,
                name = attr['uiCachedView'] || attr.name || '',
                onloadExp = attr.onload || '',
                cache = {},
                key = attr.key,

            // 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);

            scope.$on('$stateChangeSuccess', updateView);

            function updateView(event, state, params) {
                var locals = $state.$current && $state.$current.locals[name],
                    keyValue = params[key],

                if (locals === viewLocals)

                if (locals) {
                    if (current !== null)

                    if (keyValue in cache) {
                        current = cache[keyValue];
                    } else {
                        viewLocals = locals;
                        view.state = locals.$$state;

                        cache[keyValue] = {
                            element: $('<div></div>')



                        link = $compile(element.contents());
                        cache[keyValue].scope = scope.$new();

                    if (locals.$$controller) {
                        var controller = $controller(locals.$$controller, { $scope: cache[keyValue].scope });
                        element.data('$ngControllerController', controller);

                    if (isDefined(link)) link(viewScope);



                } else {
                    if (current !== null)
                    current = null;

angular.module('ui.state').directive('uiCachedView', $CachedViewDirective);

Just thrown together really quick and not tested, but I hope you get the general idea...

coli commented Apr 3, 2013

@jeme That is kind of what I have in mind when I say I'll implement it :) Hmm, this has the advantage of specifying caching at the template/directive level instead of at the state level.

I'll try it out, wow, thanks! 👍

(I should be able to report back sometime next week my experience with it)


jeme commented Apr 3, 2013

@coli Note the last part i said, was mostly to give you an idea and maybe a good starting point... I would be surprised if the above worked without quirks if it works at all. But it should be close enough so that it just needs some adjustments...

I didn't quite have time to set up an example where I would do the implementation. But feel free to get back with questions ect.


ksperling commented Apr 4, 2013

I think there will be quite a bit of devil in the detail with this one...

The idea of configuring this on the directive rather than the state seems nice. One key aspect of the whole cache mechanism is how the cache key is determined. Leaving this to the developer to specify is one option, but somewhat error prone. I wonder if combining some of this with a per-parameter equivalent of reloadOnSearch=false would make sense, and then those parameters that do cause a reload will also become part of the cache key.

There probably still needs to be some event to deliver changes in non-reload parameters to the correct controllers, and in the caching case you might need additional 'pause' and 'resume' events (one could reuse $viewContentLoaded for 'resume' and pass a flag that indicates if the content created from scratch or cached)


jeme commented Apr 4, 2013

@ksperling Actually, for a short while I was thinking of just using the url as is to capture all parameters... but that would mean we would have to break the url for matching "up to this state" somehow...

But for now, as I think this is a somewhat rare scenario that really is just centered arount e.g. iframs as in @coli's case, maybe we can live with it being a bit error prone.

And that also raises a question, if this should be a core thing or just something additional.

coli commented Apr 11, 2013

@jeme An update. Turns out dynamically creating iframe triggers lots of browser bugs with regard to history management. So I switched to a global pool of static iframes approach for my tabs...

What I had before I reverted is modifying ui-view to check the state definition for persist, then keep that state, otherwise, destroy.

View destruction and caching is still an open issue for tabs particularly for pages that are complex to render such as things dealing with data, visualizations or other heavy formatting. Right now this requires you to monitor the state params yourself and handle state for ng-show management manually. As the number of states in an application gets large this begins to become untenable. Did anyone ever find better ways to deal with these things? I'd say a view cache service might be worthwhile or perhaps some other mechanism that could be plugged into as mentioned above.

cm325 commented Aug 20, 2013

I'm looking for this too. I'm trying to implement something like this:


where each panel basically has some fairly complex content. I'd like the url to reflect where a user is in the accordion, and be able to navigate back to the last one-


nateabele commented Sep 11, 2013

@jeme Do you think you could update your version above to match the current code and submit a PR? It'd be cool to get this into a future release.


jeme commented Sep 12, 2013

@cm325 In my humble opinion you should not implement things like that with states, instead look to a custom directive instead (maybe someone already implemented one somewhere?)...

If you wish to have "active pane" (or something) linked to a specific url that is possible to, and you have a number of options moving forward on... Some more awkward than others (Some involving UI-Routers, others more isolated)... But if that is not desired then no need to dive deeper into that, but let me know and ill try to guide you a bit...

@nateabele Since this was based of a desire to keep tabs alive I would hold back on implementing it in UI-Router, because I don't think it would be the right approach on those things... And so I don't really see the need, I think it would be better to look to add something similar to "reloadOnSearch" (reloadOnParameterChange maybe?) to allow people to optionally keep the existing view but update the data...

Or maybe a completely different approach, there is so many ways to think about this actually so it gives me a headache trying to find the right approach...


nateabele commented Sep 12, 2013

@jeme Fair enough, I see your point. In that case, closing.

@nateabele nateabele closed this Sep 12, 2013

Re-confirming the final decision on this -- "because the original feature request is for the use-case of keeping tab-related DOM around, using ui-router is probably not the right approach in the first place"

How about an alternate use case: a dashboard shows a list of recent bookings with each item in the list linking off to a details page. The dashboard state & the booking-details state are mapped to URLs. The user would want to rapidly switch between the dashboard & the booking-details page, but on each state transition the template would end-up being compiled again.


jeme commented Oct 14, 2013

@saurabhnanda at first glance that doesn't sound like a use case to me, for an entire new page of information, I would expect that the user would wan't fresh data rather than aged data.

At the end of the day, you can still do this by alternate ways, that keeps it out of the router as a core functionality... or if you really must, implement your own ui-view to cater for it...

Thing is, things that maps correctly to actual view/application state (rather than component state as is with the tab view) it seems to be a rare use case that you wan't the page to be cached rather than building a fresh one from the server with new data...

@jeme -- I think I misunderstood what this issue is referring to. When a state transitions out, are the compiled DOM templates, which are no longer being used, still kept in memory or are they thrown away?

I've noticed a significant delay when switching between states using ui-router, which I presumed were due to DOM templates being compiled afresh each time.


jeme commented Oct 15, 2013

@saurabhnanda The DOM is "compiled" in angular terms yes... The template is cached though...

Keeping them around is not as easy as one may think, the above example merely caches the "DOM" and essentially also keeps the controller and scope around, that essentially means we keep multiple views alive in their full sense... that also means when we switch back to that cached view, the controller isn't called...

Another thing it means, is that anything that controller may or may not do as an effect of certain events is still performed while hidden, meaning that you end up putting another hit on your application, and so you need to build with that in mind...

Currently, if you wish to refresh data... the only way you can do it directly through UI-Router is the hard way... otherwise you need to create a workaround... and that is the same with caching views...

There is a number of ways to handle your use case outside the router, but first you need to figure out exactly what needs to happen from a user perspective... Does the page data need to refresh? is it a complex and dynamic page? and so on... and then simply make a component which is activated in a different fashion using onEnter... Let your self be inspired by how e.g. a modal dialog can be displayed... (@nateabele posted an example of how that could be done which you can find in the FAQ: https://github.com/angular-ui/ui-router/wiki/Frequently-Asked-Questions#how-to-open-a-dialogmodal-at-a-certain-state)

So basically, a single state with your standard view and a modal page component, then sub states that toggle that component, here is a bit of inspiration:

.state('main', {
  templateUrl: 'blabla.html',
  controller: ['$scope', function(scope) { 
    scope.$on('$toggleModalPage', function(event, show) {
      scope.showModalPage = show;
.state('main.activate', {
  onEnter: ['$rootScope', function(rs) { rs.$broadcast('$toggleModalPage', true); }]

Hope you can see the direction I am taking here... An alternative is obviously to pass on a parameter from the child state or even just use the $stateChangeSuccess event in the parent if it's more of a global thing...

Obviously the above would be far easier if we supported an ability to "refresh" a controller rather than "reload" it completely (there is a workaround for that as well somewhere)... I think there is an issue around for that somewhere....

Finally your obviously free to write your own version of the UI-View inspired by what I posted above, maybe even post it back as a PR and the team would evaluate it, but there is quite a bit of "unknowns" in that approach, and that is why I am reluctant to add the implementation my self...


timkindberg commented Oct 15, 2013

@jeme thank you for the inspiration.

@jeme thank you for the detailed explanation. Do you know where I could read about the details of what "compilation" means in the Angular world? I'm trying to understand why there is a sub-second lag when switching between states using ui-router, v/s just flipping a scope variable that has an ng-show depending on it.


jeme commented Oct 19, 2013

@saurabhnanda I don't know if such article exists...

The lag you experience all depends on you configuration, e.g. are you using resolve or fetching data directly in the controller etc. Template structure, directives used and so on.

@jeme the template is made available using a script type=text/ng-template tag, which means that no HTTP call is made to fetch the template. The data for the template is fetched using an AJAX called and then cached in a $cacheFactory object.

The sub-second lag that I talking about is present even when the data being rendered by a template is present in the local $cacheFactory


jeme commented Oct 21, 2013

@saurabhnanda It's difficult to say, I have been using another similar solution for a few projects and have only experienced something similar to what you say when data was queried directly within the controller... When using resolve to get data prior to transition I don't feel any lag... (obviously there is some, but as we stay on the same view until the data is loaded, as a user I don't experience anything)

So from that end, it will be difficult to help you further.


jeme commented Nov 7, 2013

Just to give an overview over "how little" is needed to support the scenario with tabs as the discussion goes on... This is on approach given in another routing solution (as I have a ready starter template for that at hand), but it should be no less possible with the ui-router...


This was implemented with tabs defined as parameters in a regular route, generally I would avoid doing that and instead use the optional search parameters, ill do another example later when I have the time...

So what did I essentially use?...

  • a tabService (tabs) which is badly named in the way it's used here, but what the hell...
  • a controller for the view holding the tab view...
  • a child state, this is because I used a route parameter approach.
<div class="bs-example">
  <ul class="nav nav-tabs nav-justified">
    <li ng-class="{ active: tab.active('default') }"><a sref="'home'">Home</a></li>
    <li ng-class="{ active: tab.active('profile') }"><a  sref="'home.tab'" params="{ activeTab: 'profile' }">Profile</a></li>
    <li ng-class="{ active: tab.active('messages') }"><a sref="'home.tab'" params="{ activeTab: 'messages' }">Messages</a></li>

<div ng-show="tab.current == 'default'" ng-include="tab.templates.default" />
<div ng-show="tab.current == 'profile'" ng-include="tab.templates.profile" />
<div ng-show="tab.current == 'messages'" ng-include="tab.templates.messages" />

Obviously having all 3 tab nodes in the DOM (but not yet loaded) is not great, but that is to cache tings... otherwise if we just go on a ng-include='tab.template' which we then change the value of will mean we lose the caching... ng-switch will do the same thing... and so we are back at where we started...

  .controller('homeCtrl', function($scope, tabs) {
    $scope.tab = tabs;
    $scope.model = { inst: 0 };

  .factory('tabs', function($state, $rootScope){
    var tabService = {
      templates: {},
      current: "default",
      active: function(tabName){
          return tabName === tabService.current

      var current = $state.params.activeTab || "default";
      tabService.current = $state.params.activeTab || "default";
      tabService.templates[current] = 'tab.'+current+'.html';

    return tabService

And that it apart from the templates and controllers within the tabs (note you don't need the controllers, but here it is to demonstrate that each controller and included tab is only loaded and compiled one time... Tabs are lazy loaded and preserved after that... This can be seen on the model.inst counter which is incremented each time we instantiate a tab controller... it starts at one and ones you have visited all 3 tabs it stays at 3...

The code above is just one approach which only targets a single tab view, it could definitely be expanded to manage several tab views and/or also be made simpler i guess... The best solution would obviously be to wrap this within a nice directive....

And with that in mind... considering just how few lines of code there is in the above... do a router REALLY need to cater for this? It's so dead simple...

Following on from discussion that strayed off-topic in #562:

@jeme I don't really follow your argument. I fully understand directives and I know how build components with them. But at some point those components must be aggregated into a view and presented to the user. Even if components are fully encapsulated into directives, they still have an instantiation cost, and it is still necessary to persist their state somewhere if you wish to navigate away from that view and then back and retain ephemeral UI state.

Even if my dashboard were a single directive called <awesome-dashboard></awesome-dashboard> or some such, I would still have the life-cycle problem if I wanted to include it in a UI Router view and not have the cost of reinstantiating it every time I loaded that view. By letting me control the life-cycle of the component, via the view it was loaded in, I could solve the problem, and it wouldn't require me to do anything in an un-Angular way.

As for avoiding Angular, UI Router is a collection Angular services and Angular directives that provide state-based navigation. To me it seems entirely natural to want to use them for all the navigation within the application, not to have certain parts handled via UI Router, and others outside the view hierarchy with their display controlled via ng-show and other mechanisms. That isn't avoiding Angular, it is just being consistent with how I implement navigation in my application.


jeme commented Nov 11, 2013


I haven't quite had time to dive deep into an example yet nor a long response so for now ill give you a brief version...

As for avoiding Angular, UI Router is a collection Angular services and Angular directives that provide state-based navigation. To me it seems entirely natural to want to use them for all the navigation within the application, not to have certain parts handled via UI Router, and others outside the view hierarchy with their display controlled via ng-show and other mechanisms. That isn't avoiding Angular, it is just being consistent with how I implement navigation in my application....

No one is saying you should handle any parts outside of UI Router, in fact if you go through the solution in my previous post you should find that we are handling the tab view under a state, even if we use optional parameters so we can add multiple tab views to the same page, we still use the "router" to manage all the routing needs...

Optional parameters instead: http://plnkr.co/edit/QWkQKljlTzY5YWfjZv3b?p=preview
Using a refresh/sticky feature: http://plnkr.co/edit/cqZfgfv69fzsfnrxj9Gz?p=preview

Note that refresh/sticky is not a feature than can be matched in the UI-Router atm... There is workarounds involving child states for it though... The rest should be 100% possible with the UI-Router as well..

The only thing we are avoiding is to load the tabs content as an UI-View... And the use of show/hide is really to just chosen to keep the DOM active for each tab without detaching... I am not sure how Angular it self will react when it tries to do stuff on a detached DOM in the first place... However I already stated that it should be written as a tabview component instead... So you could have:

<tab-view active="tab1.current">
  <tab title="Home" key="default" template="tab.home.html" />
  <tab title="Profile" key="profile" template="tab.profile.html" />
  <tab title="Messages" key="messages" template="tab.messages.html" />

Even if my dashboard were a single directive called or some such, I would still have the life-cycle problem if I wanted to include it in a UI Router view and not have the cost of reinstantiating it every time I loaded that view. By letting me control the life-cycle of the component, via the view it was loaded in, I could solve the problem, and it wouldn't require me to do anything in an un-Angular way.

It is true that if you wish to put things into a UI-View then you pay the price of instantiating it... But that price can be minimized to the point where you won't notice... and in fact... in most cases... you won't notice at all... And in many cases, won't you wan't to fetch fresh data anyways?...

For the Dashboard use-case as presented here though, I would treat it more as a modal thing along side of any UI-View there may exist. So place it outside... Or if you only wan't it for an admin area then embed it under the outer most view. But that is just me...

In the end, there is nothing that prevents you from solving your particular problem for a component you write... After all that would be no different than having a "Cached UI-View" inside a non-cached UI-View... so an "UI Router" integration won't give you any benefit you can't achieve your self...


dandv commented Oct 24, 2014

This is such an important feature! Tabs are far from the only use case. What about a mobile app with pages, one of them being a map instance? The user wants to preserve the state of the map DOM (pan, zoom, markers etc.) but ui-router will destroy the div which the map populates.

Use cases, of course, include preserving the state of any widget that has a non-trivial state that's expensive to recreate.

There is a package that addresses this problem, ui-router-extras. Perhaps it should be incorporated in core (at the moment it doesn't work for me, while a core solution would have the advantage of tight integration).


christopherthielen commented Oct 24, 2014

Don't worry, this is definitely on our radar. For now, use ui-router-extras.

I'll take a look at your linked issue early next week.

I have product items table in the first tab of a tab panel and clicking the product id from the product table, a new tab is created dynamically and shows the product details content to the ui-view="productContent". If i again navigate to the first tab and click another product id from the product items table, another new tab is dynamically created and showing the product details on the same ui-view("productContent"). The product details tab(dynamically created) contains forms and i do not want the entered value on the form lost when the user navigate to another product details tab. In my situation i could not use ui-router-extras as i am sharing the same view(productContent) for all the dynamically created tabs. Please give me advise on possible solutions.


christopherthielen commented Nov 5, 2014

We don't currently have any MDI-like (Multiple Document Interface) capabilities. I don't know of any existing implementation patterns using ui-router.

@christopherthielen christopherthielen removed this from the future milestone Nov 16, 2014

I would also like to mention that DOM heavy renders like like maps or graph visualizations (with a more detailed item/brake-down view) is a very common use-case. Judging by the amount of questions on SO, and elsewhere, it seems like something that is desired.

It seems like you could store the last X ui-view DOM's (if the user enables this feature) and just pop older ones off. The main problem seems to be the un-binding (and rebinding) of scope.

Still, for perceived performance I wouldn't mind keeping X number of previous views in memory so they don't have to be rendered again. Some maps and charts just take a few seconds to render which isn't ok if you have lots of markers or chart entry points to review in more detail.

neekey commented Apr 15, 2015

@jeme I'm doing something kind of like the use case you mentioned above。I use url search to navigate the active tabs:


and my use case is quite simple, I am rendering a list, including a tab to filter the type of the list and a pagination component follow the list for user to navigate. what I want to implement is when user changes the type tabs or navigate to a different page, the url search changes simultaneously . And if user manually type the exact url like /list?type=c&page=3, the state refreshes and the type tabs active to c and pagination component active to 3.

So the feature required to implement this is quite straight forward, providing a method to change the url search without reload the state, like:

$state.setSearch({ type: 'c', page: 3}, false); // the second parameter indicates the state not reload

or for now is there some way to work this around?

naorz commented Aug 10, 2015

If you using ionic, try this:
in this page scroll to "cashing".
Its works for every view of ionic view

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment