Caching URL view/state with parameters
Asked Answered
S

3

12

I'm making a mobile app using Cordova and AngularJS. Currently I have installed ui-router for routing but I'm open to any other alternative for routing.

My desire: I want to cache certain views bound with parameters. In other words I want to cache paths (or pages).

Example situation: let's say that we see some dashboard page, click on some book cover which redirects to the path book/2. This path is being loaded for the first time into app. Router redirects from HomeController to BooksController (whatever the name). Now the BooksController loads data for given $stateParams (book id = 2) and creates view filled with info about chosen book.

What I want in this situation:

  1. I go back to the dashboard page - it is already loaded (cached?)
  2. I choose book #2 again
  3. Controller or router notices that data about this book is already loaded
  4. The view isn't being recreated, instead it's being fetched from cache

Actually, it would be best to cache everything what I visit based on path. Preloading would be cool too.

Reason: performance. When I open some list of books then I want it to show fast. When view is being created every time, then animation of page change looks awful (it's not smooth).

Any help would be appreciated.

EDIT:

First of all, since I believe it's a common problem for many mobile HTML app programmers, I'd like to precise some information:

  1. I'm not looking for hacks but a clear solution if possible.
  2. Data in the views uses AngularJS, so YES, there are things like ng-bind, ng-repeat and so on.
  3. Caching is needed for both data and DOM elements. As far as I know, browser's layout operation is not as expensive as recreating whole DOM tree. And repaint is not what we can omit.
  4. Having separate controllers is a natural thing. Since I could leave without it I cannot imagine how it would work anyway.

I've got some semi-solutions but I'm gonna be strict about my desire.

Solution 1.

Put all views into one file (I may do it using gulp builder) and use ng-show. That's the simplest solution and I don't believe that anyone knowing AngularJS would not think about it.

A nice trick (from @DmitriZaitsev) is to create a helper function to show/hide element based on current location path.

Advantages:

  1. It's easy.
  2. KIND OF preload feature.

Disadvantages:

  1. all views have to be in a single file. Don't ask why it's not convenient.
  2. Since it's all about mobile devices, sometimes I'd like to "clear" memory. The only way I can think of is to remove those children from DOM. Dirty but ok.
  3. I cannot easily cache /book/2 and /book/3 at the same time. I would have to dynamically create DOM children on top of some templates for each view bound with parameters.

Solution 2.

Use Sticky States AND Future States from ui-router-extras which is awesome.

Advantages:

  1. Separated views.
  2. Very clear usage, very simple since it's just a plugin for ui-router.
  3. Can create dynamic substates. So it would be possible to cache book1, book2 but I'm not sure about book/1 and book/2

Disadvantages:

  1. Again, I'm not sure but I didn't found an example with caching a pair/tuple (view, parameters). Other than that it looks cool.
Scrivner answered 27/10, 2014 at 15:43 Comment(8)
The answer depends largely on what's in the view you're trying to cache.Reptant
All the binding stuff like ng-repeat and so on. So I suppose it's not just about using $templateCacheScrivner
I would try using ngHide instead of actually replacing the view. This would mean rewriting some of your code, of course. Perhaps combine the two -- ngHide/ngShow to go between the dashboard and the book, but the book view changes when a new book is selected. (I suppose a third scope/controller containing the other two would be necessary to preserve routing.)Reptant
Well, that's pretty simple approach, I hoped for a more generic solution than just hiding divs. The situation described in the example is not real, since I didn't want to pollute the sense of what I want. Actually, I come from gamedev and I'm pretty disappointed that caching results is not as common in webdev (OR I look at it differently, not sure).Scrivner
Did you try the Sticky States in ui-router-extras? Or does this not take it far enough?Detached
@Detached By looking at example I think it is not possible to make the parameterized state a sticky one. Or am I wrong? I'd like to make order/1 first state, order/2 other state and so on, but dynamically. I mean, I don't know what number of orders do I have. Do you think that's possible with Sticky States?Scrivner
The Sticky State examples uses ng-show exactly the way I described. Plus the overhead of loading extra library with no clear advantages. Sounds like a complicated solution for what can be done simple: christopherthielen.github.io/ui-router-extras/example/sticky/…Talbert
What we want to create is some sort of an MDI interface. @ChrisThielen, the creator of ui-router extras, commented that the library, which includes sticky and future states, does not support yet.Danieu
T
11

This is precisely the problem I had to solve for my site 33hotels.com. You can check it and play with the tabs "Filter" and "Filter List" (corresponding to different Routes), and see that the View is updated instantly without any delay!

How did I do it? The idea is surprisingly simple - get rid of the Router!

Why? Because the way the Router works is it re-compiles the View upon every single Route change. Yes, Angular does cache the Template but not the compiled View populated with data. Even if data do not change! As the result, when I used the Router in the past, the switch always felt sluggish and non-reactive. Every time I could notice annoying delay, it was a fraction of second but still noticeable.

Now the solution I used? Don't re-compile your Views! Keep them inside your DOM at all times! Then use ng-hide/ng-show to hide/show them depending on the routes:

<div ng-show="routeIs('/dashboard')">
    <-- Your template for Dashboard -->
</div>

<div ng-show="routeIs('/book')">
    <-- Your template for Book -->
</div>

Then create a function routeIs(string) inside your Controller to test if $location.path() matches string, or begins with string as I am using it. That way I still get my View for all pathes like /book/2. Here is the function I am using:

$scope.routeBegins = function () {
    return _.some(arguments, function (string) {
           return 0 === $location.path().indexOf(string);               
    });
};            

So no need to be smart with caching - just keep it in the DOM. It will cache your Views for you!

And the best part is - whenever your data is changed, Angular will instantly update all the Views inside your DOM, even the hidden ones!

Why is this awesome? Because, as user, I don't have to wait for all the parsing and compiling at the moment I want to see the result. I want to click the tab and see my results immediately! Why should the site wait for me to click it and then begin all the re-compiling as I am waiting? Especially when this could be easily done before, during the time my computer is idle.

Is there any downside? The only real one I can think of is loading memory with more DOM elements. However, this actual byte size of my views is negligible, comparing e.g. with all JS, CSS and images.

Another possible but avoidable downside is the re-compilation cost of the hidden views. This is where you can get smart and avoid computation-heavy parts depending on the current routes. Also, you are not re-compiling the whole View, just the parts affected by data changes, which also lowers computational cost.

I find it quite remarkable that everyone is using Routes and seems to be completely unaware (or ignorant) of this problem.

Talbert answered 8/11, 2014 at 17:31 Comment(7)
Thank you for your effort. This approach isn't too flexible which is why I asked for alternatives. I believed nobody will present this semi-solution here but now I can see I'm wrong, I didn't specify. Not recompiling views is my desire rather than a solution. Disadvantages of ng-show approach: 1. all views have to be in a single file. Don't ask why it sucks. 2. sometimes I'd like to "clear" memory. The only way I can think of is to remove those children from DOM. 3. I cannot easily cache /book/2 and /book/3 at the same time. I would have to create dynamic DOM children on top of some template.Scrivner
But anyway, I think you did put much more effort to understand my problem so thanks again. Anyway I won't mark this as an answer because maybe there is some better solution.Scrivner
@Scrivner 1. The views do not need to be in single file! Use ng-include!Talbert
@Scrivner 2. To clear memory from the View - use ng-if instead. But only for massive views you feed the difference. However, with a good architecture, this should never be the case! There is absolutely no reason to throw such a massive view onto your user, for which memory saving makes sense. In my opinion. Either it'll be insanely long page or too many details. In first case, you should dynamically hide the invisible parts, in second you overwhelm the user. In any case, it will have to re-compile the view, so you may loose the advantage. Only testing will show.Talbert
@Scrivner 3. I don't understand the problem. You can easily cache a compiled HTML and store it anywhere you want. If you want to cache the template, Angular does it anyway, but you have to compile it with new data this or other way. The key is to do it early when device is idle.Talbert
@Scrivner The "semi-solution" did solve this problem for me and I have yet to see a downside. Give a concrete practical example showing why this approach does not suit you. If there are indeed any problems with it, I'd happy to learn. But simply dismissing it for its simplicity isn't convincing. Are you looking for a 50kB JS library to load in your memory in order to save 5kB occupied by your View? :)Talbert
I'm not gonna argue. I did specify my reasons in EDIT of question. And the fact is that it wasn't me who downvoted you. I did upvote to make your answer zeroed. I think your response was practical. Thanks for all your suggestions. Unfortunately half of my bounty was given to the other guy, I missed the time :/Scrivner
N
1

1) About static pages in the app (views), angular takes care of loading them.

for example: for your dashboard page you need not worry about caching the page, as angular will take care of it. Angular will only load the dashboard view once and on all next requests for the dashboard view, angular will just show you the view(not load the file for view), if it is all a static view without any data loaded by ajax calls.

2) if your dashboard is itself loading the book list(or similar data) via ajax, then you can tell your controller to only load the data once and store it to localstorage and on subsequent requests to the dashboard page can only load the data from the localStorage.

3) similar approach can be used when your BooksController loads the data into a view. You can check in your BooksController if the request for a particular book is been previously made and if not your can store it to localstorage or database. and if the data was previously requested then you can load the same data from the storage without making a request to server.

Example situation:

say user makes request for book1, then

  1. your controller i.e BooksController check whether the same data was requested before,
  2. if not, you can load the data via the ajax call from server and also save it to local storage.
  3. if it was loaded before you will load the data stored in the localstorage or in the database.
Nadeau answered 4/11, 2014 at 8:23 Comment(3)
Thanks for your input. Things you've noted are rather simple and common. Sorry, but I feel that you totally misunderstood the problem. There aren't any ajax calls and static pages neither. It's more about digest performance (creating DOM tree based on data) than remembering just the data. What I want is to cache whole DOMs for many URLs for the same view. For example I have route category/{id} containing some books list, I'd like to cache all those "separate" views: category/1, category/2, category/3. So it's all about routing.Scrivner
(continuing because of characters limit) When I open category/4 it gets the page from cache or loads it based on data. But when I open category/4 one again it surely gets it from cache. That's what I'd like to have. Maybe there is some built-in mechanism which nobody uses or there is a library made for similiar usage.Scrivner
you should edit the question little bit, so someone else could help you.Nadeau
D
1

If you're using ui.router, then you should take a look at ui.router extras, specifically the sticky states module. This allows you to cache the state 'tree' (including rendered views) so they don't have to be compiled or re-rendered on state changes.

http://christopherthielen.github.io/ui-router-extras/

Here's a demo:

http://christopherthielen.github.io/ui-router-extras/example/sticky/#/

Dishabille answered 14/12, 2014 at 18:18 Comment(2)
Thanks but that extension was already mentioned in comments and linked in my updated post.Scrivner
See comment above. UI-router is not that good about dynamically adding states/views and certainly don't not support removing them when you want to close a view. Sticky states will work nice in cases where you want to preserve the views among statically defined states. I ran into constant race conditions trying to add states on the fly and navigate to them (used Angular Material for my tabs).Danieu

© 2022 - 2024 — McMap. All rights reserved.