Mantener el modelo de alcance al cambiar entre vistas en AngularJS

152

Estoy aprendiendo AngularJS. Digamos que tengo / view1 usando My1Ctrl y / view2 usando My2Ctrl ; que se puede navegar para usar pestañas donde cada vista tiene su propia forma simple pero diferente.

¿Cómo podría asegurarme de que los valores ingresados ​​en forma de view1 no se restablecen cuando un usuario se va y luego vuelve a view1 ?

Lo que quiero decir es, ¿cómo puede la segunda visita para ver1 mantener exactamente el mismo estado del modelo como lo dejé?

JeremyWeir
fuente

Respuestas:

174

Me tomé un poco de tiempo para averiguar cuál es la mejor manera de hacerlo. También quería mantener el estado, cuando el usuario abandona la página y luego presiona el botón Atrás, para volver a la página anterior; y no solo poner todos mis datos en el rootcope .

El resultado final es tener un servicio para cada controlador . En el controlador, solo tiene funciones y variables que no le importan, si se borran.

El servicio para el controlador se inyecta por inyección de dependencia. Como los servicios son singletons, sus datos no se destruyen como los datos en el controlador.

En el servicio, tengo un modelo. el modelo SOLO tiene datos, sin funciones. De esa manera, se puede convertir de JSON de un lado a otro para mantenerlo. Usé el almacenamiento local html5 para persistencia.

Por último, utilicé window.onbeforeunloady $rootScope.$broadcast('saveState');para que todos los servicios sepan que deben guardar su estado, y $rootScope.$broadcast('restoreState')para que sepan cómo restaurar su estado (utilizado cuando el usuario abandona la página y presiona el botón Atrás para volver a la página respectivamente).

Ejemplo de servicio llamado userService para mi 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;
}]);

ejemplo de userController

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

La vista usa un enlace como este

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

Y en el módulo de la aplicación , dentro de la función de ejecución, manejo la transmisión de saveState y 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');
};

Como mencioné, esto tomó un tiempo para llegar a este punto. Es una forma muy limpia de hacerlo, pero es un poco de ingeniería hacer algo que sospecho que es un problema muy común cuando se desarrolla en Angular.

Me encantaría ver más fácilmente, pero como formas limpias de manejar el mantenimiento del estado en todos los controladores, incluso cuando el usuario se va y vuelve a la página.

Anton
fuente
10
Esto describe cómo conservar los datos cuando la página se actualiza, no cómo conservarlos cuando la ruta cambia, por lo que no responde la pregunta. Además, no funcionará para aplicaciones que no usan rutas. Además, no muestra dónde sessionStorage.restoreStateestá configurado "true". Para obtener una solución correcta para conservar los datos cuando se actualiza la página, mira aquí . Para datos persistentes cuando la ruta cambia, mire la respuesta de carloscarcamo. Todo lo que necesita es poner los datos en un servicio.
Hugo Wood
2
persiste en las vistas, exactamente de la misma manera que sugieres, guárdalo en un servicio. Curiosamente, también persiste en un cambio de página de la misma manera que se sugiere con la ventana. onbeforeunload. gracias por hacerme verificar dos veces sin embargo. esta respuesta tiene más de un año y todavía parece ser la forma más sencilla de hacer cosas con angular.
Anton
Gracias por volver aquí, aunque sea viejo :). Seguro que su solución cubre tanto las vistas como las páginas, pero no explica qué código sirve para qué propósito, y no es lo suficientemente completo como para que pueda usarse . Simplemente estaba advirtiendo a los lectores que evitaran más preguntas . Sería genial si tuviera tiempo para hacer modificaciones para mejorar su respuesta.
Hugo Wood
¿Qué pasa si en su controlador en lugar de esto: $scope.user = userService;usted tiene algo como esto: $scope.name = userService.model.name; $scope.email = userService.model.email;. ¿Funcionará o cambiar las variables a la vista no lo cambiaría en el Servicio?
Deportes
2
Creo que angular carece de un mecanismo incorporado para algo tan común
obe6
22

Un poco tarde para una respuesta, pero acaba de actualizar el violín con algunas mejores prácticas

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
fuente
Incluso tengo que hacer esto entre navegaciones de página (no actualizaciones de página). ¿es esto correcto?
FutuToad
Creo que debería agregar la parte para completar esta respuesta, updateServicedebería llamarse cuando se destruye el controlador $scope.$on('$destroy', function() { console.log('Ctrl destroyed'); $scope.updateServiceName($scope.name); });
Shivendra
10

$ rootScope es una gran variable global, que está bien para cosas únicas o aplicaciones pequeñas. Use un servicio si desea encapsular su modelo y / o comportamiento (y posiblemente reutilizarlo en otro lugar). Además del grupo de Google, publique el OP mencionado, consulte también https://groups.google.com/d/topic/angular/eegk_lB6kVs/discussion .

Mark Rajcok
fuente
8

Angular realmente no proporciona lo que está buscando fuera de la caja. Lo que haría para lograr lo que busca es usar los siguientes complementos

UI Router y UI Router Extras

Estos dos le proporcionarán enrutamiento basado en estado y estados fijos, puede tabular entre estados y toda la información se guardará a medida que el alcance "se mantenga vivo", por así decirlo.

Verifique la documentación en ambos, ya que es bastante sencillo, los extras del enrutador ui también tienen una buena demostración de cómo funcionan los estados fijos.

Joshua Kelly
fuente
He leído los documentos, pero no pude encontrar cómo preservar las entradas de formulario cuando el usuario hace clic en el botón Atrás del navegador. ¿Me puede señalar los detalles, por favor? ¡Gracias!
Dalibor
6

Tuve el mismo problema, esto es lo que hice: tengo un SPA con múltiples vistas en la misma página (sin ajax), así que este es el código del módulo:

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

Solo tengo un controlador para todas las vistas, pero el problema es el mismo que el de la pregunta, el controlador siempre actualiza los datos, para evitar este comportamiento hice lo que la gente sugiere anteriormente y creé un servicio para ese propósito, luego lo pasé al controlador de la siguiente manera:

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

Ahora puedo llamar a la función de actualización desde cualquiera de mis vistas, pasar valores y actualizar mi modelo, no he necesitado usar html5 apis para datos de persistencia (esto es en mi caso, tal vez en otros casos sería necesario usar html5 apis como localstorage y otras cosas).

carloscarcamo
fuente
Sí, funcionó como un encanto. Nuestra fábrica de datos ya estaba codificada y estaba usando la sintaxis de ControllerAs, así que lo reescribí en $ scope controller, para que Angular pudiera controlarlo. La implementación fue muy sencilla. Esta es la primera aplicación angular que estoy escribiendo. TKS
egidiocs
2

Una alternativa a los servicios es usar el almacén de valores.

En la base de mi aplicación agregué esto

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

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

Y luego, en mi controlador, solo hago referencia al almacén de valores. No creo que tenga sentido si el usuario cierra el navegador.

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
fuente
1

Solución que funcionará para múltiples ámbitos y múltiples variables dentro de esos ámbitos

Este servicio se basó en la respuesta de Anton, pero es más extensible y funcionará en varios ámbitos y permite la selección de múltiples variables de alcance en el mismo alcance. Utiliza la ruta de ruta para indexar cada ámbito, y luego los nombres de las variables de ámbito para indexar un nivel más profundo.

Crear servicio con este código:

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

Agregue este código a su función de ejecución en el módulo de su aplicación:

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

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

Inyecte el servicio restoreScope en su controlador (ejemplo a continuación):

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

El ejemplo anterior inicializará $ scope.user en el valor almacenado; de lo contrario, el valor predeterminado se guardará de manera predeterminada. Si la página se cierra, se actualiza o se cambia la ruta, los valores actuales de todas las variables de alcance registradas se guardarán y se restaurarán la próxima vez que se visite la ruta / página.

Brett Pennings
fuente
Cuando agregué esto a mi código, recibí un error "TypeError: No se puede leer la propiedad 'locals' de undefined" ¿Cómo lo soluciono? @brett
Michael Mahony
¿Estás usando Angular para manejar tus rutas? Supongo que "no" ya que $ route.current parece no estar definido.
Brett Pennings
Sí, angular está manejando mi enrutamiento. Aquí hay una muestra de lo que tengo en el enrutamiento: JBenchApp.config (['$ routeProvider', '$ locationProvider', function ($ routeProvider, $ locationProvider) {$ routeProvider. When ('/ dashboard', {templateUrl: ' partials / dashboard.html ', controlador:' JBenchCtrl '})
Michael Mahony
Mi única otra suposición es que su controlador se está ejecutando antes de configurar su aplicación. De cualquier manera, probablemente podría solucionar el problema usando $ location en lugar de $ route y luego pasando $ scope desde el controlador en lugar de acceder a él en la ruta. Intente utilizar gist.github.com/brettpennings/ae5d34de0961eef010c6 para su servicio y pase $ scope como su tercer parámetro de restoreScope.GetOrRegisterScopeVariables en su controlador. Si esto funciona para ti, podría hacer que esa sea mi nueva respuesta. ¡Buena suerte!
Brett Pennings
1

Puede usar el $locationChangeStartevento para almacenar el valor anterior en $rootScopeo en un servicio. Cuando regrese, simplemente inicialice todos los valores almacenados previamente. Aquí hay una demostración rápida usando $rootScope.

ingrese la descripción de la imagen aquí

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
fuente