Maintain model of scope when changing between views in AngularJS
Asked Answered
N

8

152

I am learning AngularJS. Let's say I have /view1 using My1Ctrl, and /view2 using My2Ctrl; that can be navigated to using tabs where each view has its own simple, but different form.

How would I make sure that the values entered in the form of view1 are not reset, when a user leaves and then returns to view1 ?

What I mean is, how can the second visit to view1 keep the exact same state of the model as I left it ?

Newbold answered 17/10, 2012 at 18:34 Comment(0)
M
175

I took a bit of time to work out what is the best way of doing this. I also wanted to keep the state, when the user leaves the page and then presses the back button, to get back to the old page; and not just put all my data into the rootscope.

The final result is to have a service for each controller. In the controller, you just have functions and variables that you dont care about, if they are cleared.

The service for the controller is injected by dependency injection. As services are singletons, their data is not destroyed like the data in the controller.

In the service, I have a model. the model ONLY has data - no functions -. That way it can be converted back and forth from JSON to persist it. I used the html5 localstorage for persistence.

Lastly i used window.onbeforeunload and $rootScope.$broadcast('saveState'); to let all the services know that they should save their state, and $rootScope.$broadcast('restoreState') to let them know to restore their state ( used for when the user leaves the page and presses the back button to return to the page respectively).

Example service called userService for my userController :

app.factory('userService', ['$rootScope', function ($rootScope) {

    var service = {

        model: {
            name: '',
            email: ''
        },

        SaveState: function () {
            sessionStorage.userService = angular.toJson(service.model);
        },

        RestoreState: function () {
            service.model = angular.fromJson(sessionStorage.userService);
        }
    }

    $rootScope.$on("savestate", service.SaveState);
    $rootScope.$on("restorestate", service.RestoreState);

    return service;
}]);

userController example

function userCtrl($scope, userService) {
    $scope.user = userService;
}

The view then uses binding like this

<h1>{{user.model.name}}</h1>

And in the app module, within the run function i handle the broadcasting of the saveState and restoreState

$rootScope.$on("$routeChangeStart", function (event, next, current) {
    if (sessionStorage.restorestate == "true") {
        $rootScope.$broadcast('restorestate'); //let everything know we need to restore state
        sessionStorage.restorestate = false;
    }
});

//let everthing know that we need to save state now.
window.onbeforeunload = function (event) {
    $rootScope.$broadcast('savestate');
};

As i mentioned this took a while to come to this point. It is a very clean way of doing it, but it is a fair bit of engineering to do something that i would suspect is a very common issue when developing in Angular.

I would love to see easier, but as clean ways to handle keeping state across controllers, including when the user leaves and returns to the page.

Mahalia answered 15/5, 2013 at 8:8 Comment(10)
This describes how to persist data when the page refreshes, not how to persist it when the route changes, so it doesn't answer the question. Plus it will not work for apps that don't use routes. Plus it doesn't show where sessionStorage.restoreState is set to "true". For a correct solution to persist data when the page refreshes, look here. For persisting data when the route changes, look at carloscarcamo's answer. All you need is to put the data in a service.Fadeout
it does persist across views, in exactly the same way you suggest, store it in a service. interestingly enough, it also persists on a page change the same way you are suggestion with the window.onbeforeunload. thanks for making me double check though. this answer is over a year old and it still seems to be the simplest way to do things with angular.Mahalia
Thank you for coming back here, even though it's old :). Sure your solution covers both views and pages, but doesn't explain which code serves what purpose, and it is not complete enough that it can be usable. I was merely warning readers to avoid further questions. It would be great if you had time to make edits to improve your answer.Fadeout
What if in your controller instead of this: $scope.user = userService; you have something like this: $scope.name = userService.model.name; $scope.email = userService.model.email;. Will it work or changing variables in view wouldn't change it in Service?Mountebank
i think you are right, if you use $scope.name and it changes in the view, then the binding will just update the controller scope and not the model in the service.Mahalia
i think that angular lacks a built-in mechanism for a such common thingGigantism
check my answer, it provides sticky states which is what this is all about and what angular does not provide out of the box. I think v2.0 will have something similar.Misdate
Thank you very much for this nice answer, did exactly what I was looking for :-)Barred
Isn't it way, way easier to just put the data object in a service instead? e.g. instead of angular.module(...).controller('FooCtrl', function ($scope) { $scope.data = ...; }) you do angular.module(...).factory('FooData', function () { return {}; }).controller('FooCtrl'. function ($scope, FooData) { $scope.data = FooData; }). Now it persists across route changes!Chlorosis
With later versions auf AngularJS you need the locationChangeStart-Event instead of routeChangeStart, which doesnt seem to trigger on initial load. So you have to broadcast the restorestate-event one time on app load i thinkPietrek
B
22

A bit late for an answer but just updated fiddle with some best practice

jsfiddle

var myApp = angular.module('myApp',[]);
myApp.factory('UserService', function() {
    var userService = {};

    userService.name = "HI Atul";

    userService.ChangeName = function (value) {

       userService.name = value;
    };

    return userService;
});

function MyCtrl($scope, UserService) {
    $scope.name = UserService.name;
    $scope.updatedname="";
    $scope.changeName=function(data){
        $scope.updateServiceName(data);
    }
    $scope.updateServiceName = function(name){
        UserService.ChangeName(name);
        $scope.name = UserService.name;
    }
}
Blowgun answered 26/9, 2013 at 5:29 Comment(2)
I even have to do this inbetween page navigations (not page refreshes). is this correct?Scut
I think I should add the part to complete this answer, the updateService should be called when the controller is destroyed - $scope.$on('$destroy', function() { console.log('Ctrl destroyed'); $scope.updateServiceName($scope.name); });Ovovitellin
M
10

$rootScope is a big global variable, which is fine for one-off things, or small apps. Use a service if you want to encapsulate your model and/or behavior (and possibly reuse it elsewhere). In addition to the google group post the OP mentioned, see also https://groups.google.com/d/topic/angular/eegk_lB6kVs/discussion.

Milka answered 18/10, 2012 at 18:23 Comment(0)
M
8

Angular doesn't really provide what you are looking for out of the box. What i would do to accomplish what you're after is use the following add ons

UI Router & UI Router Extras

These two will provide you with state based routing and sticky states, you can tab between states and all information will be saved as the scope "stays alive" so to speak.

Check the documentation on both as it's pretty straight forward, ui router extras also has a good demonstration of how sticky states works.

Misdate answered 29/5, 2015 at 11:0 Comment(1)
I've read the documents, but couldn't find how to preserve form inputs when user clicks browser back button. Can you point me to the specifics, please? Thanks!Chief
S
6

I had the same problem, This is what I did: I have a SPA with multiple views in the same page (without ajax), so this is the code of the module:

var app = angular.module('otisApp', ['chieffancypants.loadingBar', 'ngRoute']);

app.config(['$routeProvider', function($routeProvider){
    $routeProvider.when('/:page', {
        templateUrl: function(page){return page.page + '.html';},
        controller:'otisCtrl'
    })
    .otherwise({redirectTo:'/otis'});
}]);

I have only one controller for all views, but, the problem is the same as the question, the controller always refresh data, in order to avoid this behavior I did what people suggest above and I created a service for that purpose, then pass it to the controller as follows:

app.factory('otisService', function($http){
    var service = {            
        answers:[],
        ...

    }        
    return service;
});

app.controller('otisCtrl', ['$scope', '$window', 'otisService', '$routeParams',  
function($scope, $window, otisService, $routeParams){        
    $scope.message = "Hello from page: " + $routeParams.page;
    $scope.update = function(answer){
        otisService.answers.push(answers);
    };
    ...
}]);

Now I can call the update function from any of my views, pass values and update my model, I haven't no needed to use html5 apis for persistence data (this is in my case, maybe in other cases would be necessary to use html5 apis like localstorage and other stuff).

Sinistrality answered 27/6, 2014 at 20:37 Comment(1)
yes, worked like a charm. Our data factory was coded already and I was using ControllerAs syntax, so I've rewrote it to $scope controller, so Angular could control it. Implementation was very straightforward. This is the very first angular app I´m writtng. tksAccord
I
2

An alternative to services is to use the value store.

In the base of my app I added this

var agentApp = angular.module('rbAgent', ['ui.router', 'rbApp.tryGoal', 'rbApp.tryGoal.service', 'ui.bootstrap']);

agentApp.value('agentMemory',
    {
        contextId: '',
        sessionId: ''
    }
);
...

And then in my controller I just reference the value store. I don't think it holds thing if the user closes the browser.

angular.module('rbAgent')
.controller('AgentGoalListController', ['agentMemory', '$scope', '$rootScope', 'config', '$state', function(agentMemory, $scope, $rootScope, config, $state){

$scope.config = config;
$scope.contextId = agentMemory.contextId;
...
Illboding answered 23/1, 2017 at 17:8 Comment(0)
W
1

Solution that will work for multiple scopes and multiple variables within those scopes

This service was based off of Anton's answer, but is more extensible and will work across multiple scopes and allows the selection of multiple scope variables in the same scope. It uses the route path to index each scope, and then the scope variable names to index one level deeper.

Create service with this code:

angular.module('restoreScope', []).factory('restoreScope', ['$rootScope', '$route', function ($rootScope, $route) {

    var getOrRegisterScopeVariable = function (scope, name, defaultValue, storedScope) {
        if (storedScope[name] == null) {
            storedScope[name] = defaultValue;
        }
        scope[name] = storedScope[name];
    }

    var service = {

        GetOrRegisterScopeVariables: function (names, defaultValues) {
            var scope = $route.current.locals.$scope;
            var storedBaseScope = angular.fromJson(sessionStorage.restoreScope);
            if (storedBaseScope == null) {
                storedBaseScope = {};
            }
            // stored scope is indexed by route name
            var storedScope = storedBaseScope[$route.current.$$route.originalPath];
            if (storedScope == null) {
                storedScope = {};
            }
            if (typeof names === "string") {
                getOrRegisterScopeVariable(scope, names, defaultValues, storedScope);
            } else if (Array.isArray(names)) {
                angular.forEach(names, function (name, i) {
                    getOrRegisterScopeVariable(scope, name, defaultValues[i], storedScope);
                });
            } else {
                console.error("First argument to GetOrRegisterScopeVariables is not a string or array");
            }
            // save stored scope back off
            storedBaseScope[$route.current.$$route.originalPath] = storedScope;
            sessionStorage.restoreScope = angular.toJson(storedBaseScope);
        },

        SaveState: function () {
            // get current scope
            var scope = $route.current.locals.$scope;
            var storedBaseScope = angular.fromJson(sessionStorage.restoreScope);

            // save off scope based on registered indexes
            angular.forEach(storedBaseScope[$route.current.$$route.originalPath], function (item, i) {
                storedBaseScope[$route.current.$$route.originalPath][i] = scope[i];
            });

            sessionStorage.restoreScope = angular.toJson(storedBaseScope);
        }
    }

    $rootScope.$on("savestate", service.SaveState);

    return service;
}]);

Add this code to your run function in your app module:

$rootScope.$on('$locationChangeStart', function (event, next, current) {
    $rootScope.$broadcast('savestate');
});

window.onbeforeunload = function (event) {
    $rootScope.$broadcast('savestate');
};

Inject the restoreScope service into your controller (example below):

function My1Ctrl($scope, restoreScope) {
    restoreScope.GetOrRegisterScopeVariables([
         // scope variable name(s)
        'user',
        'anotherUser'
    ],[
        // default value(s)
        { name: 'user name', email: '[email protected]' },
        { name: 'another user name', email: '[email protected]' }
    ]);
}

The above example will initialize $scope.user to the stored value, otherwise will default to the provided value and save that off. If the page is closed, refreshed, or the route is changed, the current values of all registered scope variables will be saved off, and will be restored the next time the route/page is visited.

Wilmoth answered 26/8, 2015 at 22:19 Comment(4)
When I added this to my code I get an error "TypeError: Cannot read property 'locals' of undefined" How do I fix that? @brettDissected
Are you using Angular to handle your routes? I'm guessing "no" since $route.current appears to be undefined.Wilmoth
Yes, angular is handling my routing. Here is a sample of what I have in the routing: JBenchApp.config(['$routeProvider', '$locationProvider', function ($routeProvider, $locationProvider) { $routeProvider. when('/dashboard', { templateUrl: 'partials/dashboard.html', controller: 'JBenchCtrl' })Dissected
My only other guess is that your controller is running before your app is configured. Either way, you could probably get around the issue by using $location instead of $route and then passing in $scope from the controller instead of accessing it in the route. Try using gist.github.com/brettpennings/ae5d34de0961eef010c6 for your service instead and pass in $scope as your third parameter of restoreScope.GetOrRegisterScopeVariables in your controller. If this works for you I might just make that my new answer. Good luck!Wilmoth
S
1

You can use $locationChangeStart event to store the previous value in $rootScope or in a service. When you come back, just initialize all previously stored values. Here is a quick demo using $rootScope.

enter image description here

var app = angular.module("myApp", ["ngRoute"]);
app.controller("tab1Ctrl", function($scope, $rootScope) {
    if ($rootScope.savedScopes) {
        for (key in $rootScope.savedScopes) {
            $scope[key] = $rootScope.savedScopes[key];
        }
    }
    $scope.$on('$locationChangeStart', function(event, next, current) {
        $rootScope.savedScopes = {
            name: $scope.name,
            age: $scope.age
        };
    });
});
app.controller("tab2Ctrl", function($scope) {
    $scope.language = "English";
});
app.config(function($routeProvider) {
    $routeProvider
        .when("/", {
            template: "<h2>Tab1 content</h2>Name: <input ng-model='name'/><br/><br/>Age: <input type='number' ng-model='age' /><h4 style='color: red'>Fill the details and click on Tab2</h4>",
            controller: "tab1Ctrl"
        })
        .when("/tab2", {
            template: "<h2>Tab2 content</h2> My language: {{language}}<h4 style='color: red'>Now go back to Tab1</h4>",
            controller: "tab2Ctrl"
        });
});
<!DOCTYPE html>
<html>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.9/angular.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.9/angular-route.js"></script>
<body ng-app="myApp">
    <a href="#/!">Tab1</a>
    <a href="#!tab2">Tab2</a>
    <div ng-view></div>
</body>
</html>
Significance answered 7/7, 2018 at 10:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.