Mantenere il modello di ambito quando si cambia tra le viste in AngularJS


152

Sto imparando AngularJS. Diciamo che ho / view1 usando My1Ctrl e / view2 usando My2Ctrl ; che può essere utilizzato per utilizzare le schede in cui ogni vista ha la sua forma semplice, ma diversa.

Come posso assicurarmi che i valori immessi sotto forma di view1 non vengano ripristinati quando un utente lascia e poi torna a view1 ?

Ciò che intendo è: come può la seconda visita a view1 mantenere lo stesso stato esatto del modello in cui l'ho lasciato?

Risposte:


174

Mi sono preso un po 'di tempo per capire qual è il modo migliore per farlo. Volevo anche mantenere lo stato, quando l'utente lascia la pagina e quindi preme il pulsante Indietro, per tornare alla vecchia pagina; e non solo mettere tutti i miei dati nel rootcope .

Il risultato finale è avere un servizio per ciascun controller . Nel controller, hai solo funzioni e variabili che non ti interessano, se vengono cancellate.

Il servizio per il controller viene iniettato tramite iniezione di dipendenza. Poiché i servizi sono singoli, i loro dati non vengono distrutti come i dati nel controller.

Nel servizio, ho un modello. il modello ha SOLO dati - nessuna funzione -. In questo modo può essere convertito avanti e indietro da JSON per persistere. Ho usato l'html5 localstorage per la persistenza.

Infine ho usato window.onbeforeunloade $rootScope.$broadcast('saveState');per far sapere a tutti i servizi che avrebbero dovuto salvare il loro stato, e $rootScope.$broadcast('restoreState')per far loro sapere per ripristinare il loro stato (usato per quando l'utente lascia la pagina e preme il pulsante Indietro per tornare rispettivamente alla pagina).

Esempio di servizio chiamato userService per 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;
}]);

esempio userController

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

La vista utilizza quindi l'associazione in questo modo

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

E nel modulo dell'app , all'interno della funzione di esecuzione gestisco la trasmissione di saveState e 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');
};

Come ho già detto, ci è voluto un po 'per arrivare a questo punto. È un modo molto pulito di farlo, ma è un bel po 'di ingegneria fare qualcosa che sospetto sia un problema molto comune quando si sviluppa in Angular.

Mi piacerebbe vedere più facile, ma come modi chiari per gestire lo stato tra i controller, anche quando l'utente lascia e torna alla pagina.


10
Descrive come conservare i dati quando la pagina si aggiorna, non come mantenerli quando cambia il percorso, quindi non risponde alla domanda. Inoltre, non funzionerà con le app che non utilizzano route. Inoltre non mostra dove sessionStorage.restoreStateè impostato "true". Per una soluzione corretta per conservare i dati quando la pagina si aggiorna, guarda qui . Per i dati persistenti quando il percorso cambia, guarda la risposta di Carloscarcamo. Tutto ciò che serve è mettere i dati in un servizio.
Hugo Wood,

2
persiste su tutte le visualizzazioni, esattamente nello stesso modo in cui si suggerisce, memorizzarlo in un servizio. abbastanza interessante, persiste anche su un cambio di pagina nello stesso modo in cui si è suggerito con la finestra.onbeforeunload. grazie per avermi fatto ricontrollare però. questa risposta ha più di un anno e sembra ancora essere il modo più semplice per fare le cose con angolare.
Anton,

Grazie per essere tornato qui, anche se è vecchio :). Sicuramente la tua soluzione copre sia le viste che le pagine, ma non spiega quale codice serve a quale scopo e non è abbastanza completo da poter essere utilizzato . Stavo semplicemente avvertendo i lettori di evitare ulteriori domande . Sarebbe bello se avessi il tempo di apportare modifiche per migliorare la tua risposta.
Hugo Wood,

Che cosa succede se nel controller invece di questo: $scope.user = userService;avete qualcosa di simile: $scope.name = userService.model.name; $scope.email = userService.model.email;. Funzionerà o cambiando le variabili in vista non cambierebbe in Servizio?
sport

2
penso che angolare manchi di un meccanismo incorporato per una cosa così comune
obe6

22

Un po 'in ritardo per una risposta, ma ho appena aggiornato il violino con alcune buone pratiche

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;
    }
}

Devo anche fare questo tra le navigazioni delle pagine (non gli aggiornamenti della pagina). è corretto?
FutuToad,

Penso che dovrei aggiungere la parte per completare questa risposta, che updateServicedovrebbe essere chiamata quando il controller viene distrutto - $scope.$on('$destroy', function() { console.log('Ctrl destroyed'); $scope.updateServiceName($scope.name); });
Shivendra


8

Angular non fornisce realmente ciò che stai cercando fuori dalla scatola. Quello che farei per realizzare ciò che stai cercando è usare i seguenti componenti aggiuntivi

UI Router & UI Router Extras

Questi due ti forniranno il routing basato sullo stato e gli stati appiccicosi, puoi passare da uno stato all'altro e tutte le informazioni verranno salvate poiché l'ambito "rimane in vita" per così dire.

Controlla la documentazione su entrambi poiché è piuttosto semplice, gli extra del router hanno anche una buona dimostrazione di come funzionano gli stati appiccicosi.


Ho letto i documenti, ma non sono riuscito a trovare come conservare gli input del modulo quando l'utente fa clic sul pulsante Indietro del browser. Puoi indicarmi le specifiche, per favore? Grazie!
Dalibor,

6

Ho avuto lo stesso problema, questo è quello che ho fatto: ho una SPA con più viste nella stessa pagina (senza ajax), quindi questo è il codice del modulo:

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

Ho un solo controller per tutte le visualizzazioni, ma il problema è lo stesso della domanda, il controller aggiorna sempre i dati, al fine di evitare questo comportamento ho fatto ciò che la gente suggerisce sopra e ho creato un servizio a tale scopo, quindi passarlo al controller come segue:

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

Ora posso chiamare la funzione di aggiornamento da una qualsiasi delle mie viste, passare valori e aggiornare il mio modello, non ho avuto bisogno di usare apis html5 per i dati di persistenza (questo è nel mio caso, forse in altri casi sarebbe necessario usare html5 API come localstorage e altre cose).


sì, ha funzionato come un fascino. Il nostro data factory era già codificato e stavo usando la sintassi di ControllerAs, quindi l'ho riscritto in $ scope controller, in modo che Angular potesse controllarlo. L'attuazione è stata molto semplice. Questa è la prima app angolare che sto scrivendo. tks
egidiocs,

2

Un'alternativa ai servizi è utilizzare l'archivio valori.

Nella base della mia app ho aggiunto questo

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

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

E poi nel mio controller faccio solo riferimento al negozio di valori. Non credo che valga la pena se l'utente chiude il browser.

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

$scope.config = config;
$scope.contextId = agentMemory.contextId;
...

1

Soluzione che funzionerà per più ambiti e più variabili all'interno di tali ambiti

Questo servizio si basava sulla risposta di Anton, ma è più estensibile e funzionerà su più ambiti e consente la selezione di più variabili di ambito nello stesso ambito. Utilizza il percorso della route per indicizzare ciascun ambito, quindi i nomi delle variabili dell'ambito per indicizzare un livello più in profondità.

Crea servizio con questo codice:

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

Aggiungi questo codice alla funzione di esecuzione nel modulo dell'app:

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

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

Iniettare il servizio restoreScope nel controller (esempio seguente):

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

L'esempio sopra inizializzerà $ scope.user sul valore memorizzato, altrimenti verrà impostato automaticamente sul valore fornito e lo salverà. Se la pagina viene chiusa, aggiornata o il percorso viene modificato, i valori correnti di tutte le variabili dell'ambito registrate verranno salvati e verranno ripristinati alla successiva visita del percorso / pagina.


Quando l'ho aggiunto al mio codice, viene visualizzato l'errore "TypeError: Impossibile leggere la proprietà 'locals' di undefined" Come posso risolvere il problema? @brett
Michael Mahony,

Stai utilizzando Angular per gestire i tuoi percorsi? Sto indovinando "no" poiché $ route.current sembra essere indefinito.
Brett Pennings

Sì, angolare sta gestendo il mio percorso. Ecco un esempio di ciò che ho nel routing: JBenchApp.config (['$ routeProvider', '$ locationProvider', funzione ($ routeProvider, $ locationProvider) {$ routeProvider. When ('/ dashboard', {templateUrl: ' partials / dashboard.html ', controller:' JBenchCtrl '})
Michael Mahony,

La mia unica altra ipotesi è che il controller sia in esecuzione prima della configurazione dell'app. In entrambi i casi, è possibile aggirare il problema utilizzando $ location anziché $ route e quindi passando $ scope dal controller invece di accedervi nella route. Prova invece a utilizzare gist.github.com/brettpennings/ae5d34de0961eef010c6 per il tuo servizio e passa $ scope come terzo parametro di restoreScope.GetOrRegisterScopeVariables nel controller. Se questo funziona per te, potrei semplicemente fare della mia nuova risposta. In bocca al lupo!
Brett Pennings

1

È possibile utilizzare l' $locationChangeStartevento per archiviare il valore precedente in $rootScopeo in un servizio. Quando torni, inizializza tutti i valori precedentemente memorizzati. Ecco una demo rapida usando $rootScope.

inserisci qui la descrizione dell'immagine

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>

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.