Conserver le modèle de portée lors du changement entre les vues dans AngularJS

152

J'apprends AngularJS. Disons que j'ai / view1 en utilisant My1Ctrl et / view2 en utilisant My2Ctrl ; qui peut être parcouru à l'aide d'onglets où chaque vue a sa propre forme simple mais différente.

Comment puis-je m'assurer que les valeurs saisies sous la forme de view1 ne sont pas réinitialisées, lorsqu'un utilisateur quitte et retourne à view1 ?

Ce que je veux dire, c'est, comment la deuxième visite de view1 peut-elle conserver exactement le même état du modèle que je l'ai laissé?

JeremyWeir
la source

Réponses:

174

J'ai pris un peu de temps pour déterminer quelle est la meilleure façon de procéder. Je voulais aussi conserver l'état, lorsque l'utilisateur quitte la page puis appuie sur le bouton retour, pour revenir à l'ancienne page; et pas seulement mettre toutes mes données dans le rootscope .

Le résultat final est d'avoir un service pour chaque contrôleur . Dans le contrôleur, vous n'avez que des fonctions et des variables dont vous ne vous souciez pas, si elles sont effacées.

Le service pour le contrôleur est injecté par injection de dépendances. Les services étant des singletons, leurs données ne sont pas détruites comme les données du contrôleur.

Dans le service, j'ai un modèle. le modèle a UNIQUEMENT des données - pas de fonctions -. De cette façon, il peut être converti d'avant en arrière à partir de JSON pour le conserver. J'ai utilisé le localstorage html5 pour la persistance.

Enfin, j'ai utilisé window.onbeforeunloadet $rootScope.$broadcast('saveState');pour faire savoir à tous les services qu'ils doivent enregistrer leur état et $rootScope.$broadcast('restoreState')pour leur faire savoir de restaurer leur état (utilisé lorsque l'utilisateur quitte la page et appuie sur le bouton retour pour revenir à la page respectivement).

Exemple de service appelé userService pour mon 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;
}]);

Exemple userController

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

La vue utilise ensuite une liaison comme celle-ci

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

Et dans le module d'application , dans la fonction d'exécution, je gère la diffusion de saveState et de 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');
};

Comme je l'ai mentionné, cela a pris un certain temps pour arriver à ce point. C'est une façon très propre de le faire, mais c'est un peu d'ingénierie de faire quelque chose qui, à mon avis, est un problème très courant lors du développement en Angular.

J'adorerais voir des moyens plus simples, mais aussi propres, de gérer le maintien de l'état sur les contrôleurs, y compris lorsque l'utilisateur quitte et revient à la page.

Anton
la source
10
Cela décrit comment conserver les données lors de l'actualisation de la page, et non comment les conserver lorsque l'itinéraire change, afin de ne pas répondre à la question. De plus, cela ne fonctionnera pas pour les applications qui n'utilisent pas de routes. De plus, il ne montre pas où sessionStorage.restoreStateest défini "true". Pour une solution correcte pour conserver les données lors de l'actualisation de la page, regardez ici . Pour des données persistantes lorsque l'itinéraire change, regardez la réponse de carloscarcamo. Tout ce dont vous avez besoin est de mettre les données dans un service.
Hugo Wood
2
il persiste dans les vues, exactement de la même manière que vous suggérez, de le stocker dans un service. Curieusement, il persiste également sur un changement de page de la même manière que vous suggérez avec le window.onbeforeunload. merci de m'avoir fait vérifier. cette réponse a plus d'un an et cela semble toujours être le moyen le plus simple de faire les choses avec angular.
Anton
Merci d'être revenu ici, même si c'est vieux :). Bien sûr, votre solution couvre à la fois les vues et les pages, mais n'explique pas quel code sert à quel but, et elle n'est pas suffisamment complète pour être utilisable . J'avertis simplement les lecteurs d'éviter d' autres questions . Ce serait formidable si vous aviez le temps de faire des modifications pour améliorer votre réponse.
Hugo Wood
Et si dans votre contrôleur au lieu de ceci: $scope.user = userService;vous avez quelque chose comme ceci: $scope.name = userService.model.name; $scope.email = userService.model.email;. Cela fonctionnera-t-il ou changer les variables en vue ne le changerait-il pas dans Service?
sports du
2
Je pense que angular n'a pas de mécanisme intégré pour une chose aussi courante
obe6
22

Un peu en retard pour une réponse mais juste un violon mis à jour avec quelques bonnes pratiques

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;
    }
}
Atul Chaudhary
la source
Je dois même faire cela entre les navigations de page (pas d'actualisation de page). est-ce correct?
FutuToad
Je pense que je devrais ajouter la partie pour compléter cette réponse, le updateServicedevrait être appelé lorsque le contrôleur est détruit - $scope.$on('$destroy', function() { console.log('Ctrl destroyed'); $scope.updateServiceName($scope.name); });
Shivendra
10

$ rootScope est une grande variable globale, ce qui convient parfaitement aux choses ponctuelles ou aux petites applications. Utilisez un service si vous souhaitez encapsuler votre modèle et / ou comportement (et éventuellement le réutiliser ailleurs). En plus du groupe Google qui publie l'OP mentionné, voir également https://groups.google.com/d/topic/angular/eegk_lB6kVs/discussion .

Mark Rajcok
la source
8

Angular ne fournit pas vraiment ce que vous recherchez. Ce que je ferais pour accomplir ce que vous recherchez, c'est d'utiliser les modules complémentaires suivants

Routeur UI et Routeur UI Extras

Ces deux vous fourniront un routage basé sur l'état et des états persistants, vous pouvez passer d'un état à l'autre et toutes les informations seront sauvegardées car la portée "reste vivante" pour ainsi dire.

Consultez la documentation sur les deux car c'est assez simple, les extras du routeur ui ont également une bonne démonstration du fonctionnement des états persistants.

Joshua Kelly
la source
J'ai lu les documents, mais je n'ai pas trouvé comment conserver les entrées de formulaire lorsque l'utilisateur clique sur le bouton de retour du navigateur. Pouvez-vous m'indiquer les détails, s'il vous plaît? Merci!
Dalibor
6

J'ai eu le même problème, c'est ce que j'ai fait: j'ai un SPA avec plusieurs vues dans la même page (sans ajax), voici donc le code du 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'});
}]);

Je n'ai qu'un seul contrôleur pour toutes les vues, mais, le problème est le même que la question, le contrôleur actualise toujours les données, afin d'éviter ce comportement, j'ai fait ce que les gens suggèrent ci-dessus et j'ai créé un service à cette fin, puis le transmettre au contrôleur comme suit:

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);
    };
    ...
}]);

Maintenant, je peux appeler la fonction de mise à jour à partir de n'importe laquelle de mes vues, transmettre des valeurs et mettre à jour mon modèle, je n'ai pas besoin d'utiliser des apis html5 pour les données de persistance (c'est dans mon cas, peut-être que dans d'autres cas, il serait nécessaire d'utiliser html5 apis comme localstorage et autres).

carloscarcamo
la source
oui, a fonctionné comme un charme. Notre fabrique de données était déjà codée et j'utilisais la syntaxe ControllerAs, je l'ai donc réécrite dans $ scope controller, afin qu'Angular puisse la contrôler. La mise en œuvre a été très simple. Ceci est la toute première application angulaire que j'écris. tks
egidiocs
2

Une alternative aux services consiste à utiliser le magasin de valeur.

Dans la base de mon application, j'ai ajouté ceci

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

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

Et puis dans mon contrôleur, je fais simplement référence au magasin de valeur. Je ne pense pas que cela tienne quelque chose si l'utilisateur ferme le navigateur.

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

$scope.config = config;
$scope.contextId = agentMemory.contextId;
...
Lawrie R
la source
1

Solution qui fonctionnera pour plusieurs portées et plusieurs variables au sein de ces portées

Ce service était basé sur la réponse d'Anton, mais est plus extensible et fonctionnera sur plusieurs portées et permet la sélection de plusieurs variables de portée dans la même portée. Il utilise le chemin de l'itinéraire pour indexer chaque étendue, puis les noms des variables d'étendue pour indexer un niveau plus profond.

Créez un service avec ce 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;
}]);

Ajoutez ce code à votre fonction d'exécution dans votre module d'application:

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

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

Injectez le service restoreScope dans votre contrôleur (exemple ci-dessous):

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]' }
    ]);
}

L'exemple ci-dessus initialisera $ scope.user à la valeur stockée, sinon sera par défaut la valeur fournie et l'enregistrera. Si la page est fermée, actualisée ou si la route est modifiée, les valeurs actuelles de toutes les variables d'étendue enregistrées seront sauvegardées et seront restaurées la prochaine fois que la route / la page sera visitée.

Brett Pennings
la source
Quand j'ai ajouté ceci à mon code, j'obtiens une erreur "TypeError: Cannot read property 'locals' of undefined" Comment résoudre ce problème? @brett
Michael Mahony
Utilisez-vous Angular pour gérer vos itinéraires? Je suppose "non" car $ route.current semble indéfini.
Brett Pennings
Oui, angular gère mon routage. Voici un exemple de ce que j'ai dans le routage: JBenchApp.config (['$ routeProvider', '$ locationProvider', function ($ routeProvider, $ locationProvider) {$ routeProvider. When ('/ dashboard', {templateUrl: ' partials / dashboard.html ', controller:' JBenchCtrl '})
Michael Mahony
Ma seule autre hypothèse est que votre contrôleur est en cours d'exécution avant que votre application ne soit configurée. Dans tous les cas, vous pourriez probablement contourner le problème en utilisant $ location au lieu de $ route, puis en passant $ scope depuis le contrôleur au lieu d'y accéder par la route. Essayez plutôt d' utiliser gist.github.com/brettpennings/ae5d34de0961eef010c6 pour votre service et passez $ scope comme troisième paramètre de restoreScope.GetOrRegisterScopeVariables dans votre contrôleur. Si cela fonctionne pour vous, je pourrais en faire ma nouvelle réponse. Bonne chance!
Brett Pennings
1

Vous pouvez utiliser un $locationChangeStartévénement pour stocker la valeur précédente dans $rootScopeou dans un service. Lorsque vous revenez, initialisez simplement toutes les valeurs précédemment stockées. Voici une démo rapide utilisant $rootScope.

entrez la description de l'image ici

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>

Hari Das
la source