3

I have an Angular app where I sometimes want to:

  • Store the currently entered form variables for the currently shown page. This can be search terms or other user input.
  • If the user goes back in history, make sure the same search parameters are used again

I have looked at ngRoute, stateProvider and html history and not found a good way to do it. It looks like something which should be fairly common, so I guess I just haven't understood how to do it.

Markus Johansson
  • 3,733
  • 8
  • 36
  • 55
  • 1
    When you say search parameters are you referring to the search portion of a URL? For example, in the url: http://example.com/page?search=set+of+terms I'm talking about the `?search=set+of+terms` portion. – David Jan 22 '15 at 15:04
  • use a module like Angular-Local-Storage (https://github.com/grevory/angular-local-storage) to store your variables to the client's local storage (or session, or cookies). read the values back out if they exist and assign your variables. There are many things to consider, however; notably when to save and when to update the storage. – Claies Jan 22 '15 at 15:07
  • David: I tried to clarify in the text. The search parameters or form variables doesn't have to end up in the url. – Markus Johansson Jan 22 '15 at 15:10
  • Use a Service. Those are singletons and point to the same object/reference for the whole lifetime of the AngularJS application. Additionally you can persist this data using cookies or local storage. – Sergiu Paraschiv Jan 22 '15 at 15:11
  • I don't want the data to be persisted so that if the user goes back to the same page without going back in history the old data is shown. In that case the form fields should be in default state. – Markus Johansson Jan 22 '15 at 15:13
  • Then you plug into the HTML5 history API and handle that case yourself. No other way really. – Sergiu Paraschiv Jan 22 '15 at 15:16
  • How would I plug that in and make it roll with angular? Currently I use ngRoute to handle different views. – Markus Johansson Jan 22 '15 at 15:22
  • maybe that is what u search https://github.com/decipherinc/angular-history – micha Jan 22 '15 at 15:36

4 Answers4

0

To deal with this in our current project we declared a directive which watches the model and bounds this value to a variable in a data service. Moreover on render, this directive checks if the appropriate value is already set in the data service. If so, the model is being set to this value.

As requested in comments, my StorageService:

'use strict';

/**
 * storage wrapper for session- or local-storage operations
 */
app.factory('StorageService', function(
        $rootScope,
        $http,
        $location) {

    /**
     * get an item
     *
     * @param item - string - the item identifier
     * @return - mixed -
     */
    var get = function(item) {
        return JSON.parse(sessionStorage.getItem(item) ||localStorage.getItem(item));
    };

    /**
     * set an item
     *
     * @param item - string - the item identifier
     * @param value - mixed - the value to set
     * @param usePersistentStorage - boolean - the flag for session- or local-storage
     * @return void
     */
    var set = function(item, value, usePersistentStorage) {
        var obj = {
            value: value,
            ts: new Date().getTime()
        };
        window[usePersistentStorage ? 'localStorage' : 'sessionStorage'][value === null ? 'removeItem' : 'setItem'](item, JSON.stringify(obj));
    };

    /**
     * remove an item
     *
     * @param item - string - the item identifier
     * @return void
     */
    var remove = function(item) {
        set(item, null, true);
        set(item, null);
    };

    /**
     * clear the whole session- and local-storage
     *
     * @return void
     */
    var clear = function() {
        sessionStorage.clear();
        localStorage.clear();
    };

    /**
     * check if item has expired
     *
     * @return boolean
     */
    var checkExpiration = function(str, minutes) {
        var now = new Date(),
            nowts = now.getTime(),
            item = get(str);
        if(item && typeof item.ts != 'undefined' && (new Date(nowts) - new Date(item.ts) < minutes * 60 * 1000)) {
            return true;
        } else {
            remove(str);
            return false;
        }
    };

    return {
        get: get,
        set: set,
        remove: remove,
        clear: clear,
        checkExpiration: checkExpiration
    };
}

);

DonJuwe
  • 4,477
  • 3
  • 34
  • 59
  • Cool. But do you ever clear the value in the data service? Let's say the user clicks "Add customer", fills out half of it and then decides not to continue and clicks somewhere else. If he then comes back to the same form, without using history navigation, how do you prevent that the old values are filled in? – Markus Johansson Jan 23 '15 at 08:41
  • The data is cleared on reload since you do not write them into local sotrage, cookies, etc... If the user clicks back to the page with the previously filled out form, the directive checks if the model has some data in the service and sets the model value. – DonJuwe Jan 23 '15 at 09:00
  • I see, so the cancelled creation of a customer would linger in the app until reload? In my case I think I would prefer if that data was only present on history navigation. Any ideas how that could be accomplished? – Markus Johansson Jan 23 '15 at 09:38
  • Well, the data is being kept in the service only site-wide. If you want to keep it on browser-actions like refresh or history back button I suggest to write a service which writes it to the browsers local storage. Let me know if you need the code for a nice localStorageService. – DonJuwe Jan 23 '15 at 09:47
  • Sounds like it could be really helpful. If you would like to share it, I'm interested. – Markus Johansson Jan 23 '15 at 09:55
0

I've saved view state in the past by having a service that held it in an object.

Any reference to a field I wanted to maintain (like the sort column of a table or the value of a specific field) would be saved in the service's view state object instead of in the controller/scope.

The controller would check the 'source' or 'type' of page transfer when it initialized by looking for a specific url parameter or state (eg. #/search?type=new). If it was considered a new search, it would reset the values. Otherwise it would display and use the previously used values.

An application reload would wipe out the data in the Service, giving a brand new form.


The approach I described above is simple because angular handles the saving for you. Services are singletons, so by binding directly to the service's fields, it will survive route changes automatically.

In your view:

<input ng-model="criteria.firstName">    

In your controller initialization:

$scope.criteria = ViewStateService.criteria;

If you'd prefer saving view state only at specific points, you can set up an event handler on a page change/route change event and do a copy of the data at that point.

$scope.$on('$locationChangeStart', function(next, current) {
    //code to copy/save fields you want to the Service.
});

how to watch for a route change in angularjs

Community
  • 1
  • 1
JGefroh
  • 206
  • 2
  • 4
  • It sounds interesting. This answer is in the line of what I'm looking for. What I'm thinking about is how to know WHEN to store something. If the user has entered a lot of values and then clicks a link within the application, I want those values to be kept. In that case I somehow how to hook into the navigation. – Markus Johansson Jan 23 '15 at 08:32
  • I've edited the answer with some more information. If you bind directly to the service you won't need to do anything specific to know when to save - you'll be saving directly to the service instead of the scope. – JGefroh Jan 23 '15 at 16:43
0

Based on what you are describing I see a few ways that this could be done.

There's LocalStorage which is supported in most all browsers, and even so there's polyfills for it. This would mean that you store using a name/value pair.

Here's a quick example:

var searchParameters = {
  filters: [
    'recent'
  ],
  searchString: 'bacon'
}

// Store the value(s)
localStorage.setItem('searchParameters', JSON.stringify(searchParameters));

// Retrieve the value(s)
searchParameters = JSON.parse(localStorage.getItem('searchParameters'));

// Delete the value(s)
localStorage.removeItem('searchParameters');

Depending on your flow you could make use of the browser history stack. So if someone searches for bacon then you could send them to the page with ?query=bacon appended to it. This would allow you to easily maintain the history and allow easy use of the back button. At the end of the day, it all really comes down to how your application is setup as to what is the best choice. There's also other ways that this could be accomplished depending on requirements. For example, if you need to synchronize across devices a server side component would need to be implemented to store the values and retrieve them.

David
  • 7,005
  • 2
  • 22
  • 29
0

There are several methods and two seemed to be quite reliable. I ended up choosing the first approach for my app since my search parameters needed to propagate to other controllers.

Storing them in cookies

Angular.js offers $cookies that allows to get/set parameters on the browser. I use this as true source of search params.

For example:

search.service.js

angular
    .module('app')
    .service('SearchService', SearchService);


    SearchService.$inject = [
        '$cookies'
    ];

    function SearchService(
        $cookies
    ) {

    var searchCookieKey = 'searchHistoryCookieKey';
    var searchCookieMaxSize = 10;

    return {
        search: search,
        getSearchHistory: getSearchHistory
    };

    function search(arg1, arg2) {
        storeSearchHistory({arg1: arg1, arg2: arg2});

        // do your search here
        // also, you should cache your search so 
        // when you use the 'most recent' params
        // then it won't create another network request 
    }

    // Store search params in cookies
    function storeSearchHistory(params) {
        var history = getSearchHistory();
        history.unshift(params); // first one is most recent
        if(history.length > searchCookieMaxSize) {
            history.pop();
        }
        $cookies.putObject(searchCookieKey, history);
    }

    // Get recent history from cookies
    function getSearchHistory() {
        return $cookies.getObject(searchCookieKey) || [];
    }

}

app.states.js

   .state('search', {
        url: "/search",
        templateUrl: "/dashboard/search/templates/index.html",
        controller: 'SearchController',
        resolve: {
            searchResults: ['SearchService', '$stateParams', function(SearchService, $stateParams) {
                if(!$stateParams.arg1 || !$stateParams.arg2) {
                    var history = SearchService.getSearchHistory();
                    var mostRecent = history.length ? history[0] : null;
                    if(mostRecent) {
                        return SearchService.search(mostRecent.arg1, mostRecent.arg2);
                    }
                }
                return SearchService.search($stateParams.arg1, $stateParams.arg2);
            }]
        }
    })

If you are not caching these search network calls, then your app will need to wait for the request to return, thus slowing down your app.

Passing them around in states

You can create a parent controller which holds your $stateParams and your child controllers will inherit the parameters. The parameters are not overwritten when going back/forward or accessing between child states. However, they are overwritten when you move states with specific params. So, you will just need to explicity specify those parent's $stateParams when moving between states.

Taku
  • 5,639
  • 2
  • 42
  • 31