Manter modelo de escopo ao mudar entre visualizações no AngularJS

152

Estou aprendendo o AngularJS. Digamos que eu tenho / view1 usando My1Ctrl e / view2 usando My2Ctrl ; que pode ser navegado para usar guias em que cada visualização tem sua própria forma simples, mas diferente.

Como garantir que os valores inseridos na forma de view1 não sejam redefinidos quando um usuário sai e volta para view1 ?

O que quero dizer é: como a segunda visita à view1 mantém exatamente o mesmo estado do modelo que eu o deixei?

JeremyWeir
fonte

Respostas:

174

Demorei um pouco para descobrir qual é a melhor maneira de fazer isso. Eu também queria manter o estado, quando o usuário sai da página e pressiona o botão Voltar, para voltar à página antiga; e não apenas colocar todos os meus dados no rootscope .

O resultado final é ter um serviço para cada controlador . No controlador, você apenas possui funções e variáveis ​​com as quais não se importa, se elas estiverem limpas.

O serviço para o controlador é injetado por injeção de dependência. Como os serviços são singletons, seus dados não são destruídos como os dados no controlador.

No serviço, eu tenho um modelo. o modelo SOMENTE possui dados - sem funções -. Dessa forma, ele pode ser convertido para frente e para trás do JSON para persistir. Eu usei o armazenamento local html5 para persistência.

Por fim, usei window.onbeforeunloade $rootScope.$broadcast('saveState');avisei a todos os serviços que eles devem salvar seu estado e $rootScope.$broadcast('restoreState')avise-os para restaurar seu estado (usado para quando o usuário sai da página e pressiona o botão Voltar para retornar à página, respectivamente).

Exemplo de serviço chamado userService para meu 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;
}]);

exemplo userController

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

A visualização então usa ligação como esta

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

E no módulo do aplicativo , dentro da função de execução, eu lida com a transmissão do 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');
};

Como mencionei, demorou um pouco para chegar a esse ponto. É uma maneira muito limpa de fazer isso, mas é um pouco de engenharia fazer algo que eu suspeitaria ser um problema muito comum no desenvolvimento em Angular.

Eu adoraria ver mais fácil, mas como maneiras limpas de lidar com a manutenção do estado entre controladores, inclusive quando o usuário sai e retorna à página.

Anton
fonte
10
Isso descreve como manter os dados quando a página é atualizada, e não como persistir quando a rota é alterada, para que não responda à pergunta. Além disso, não funcionará para aplicativos que não usam rotas. Além disso, ele não mostra onde sessionStorage.restoreStateestá definido "true". Para que uma solução correta persista os dados quando a página for atualizada, consulte aqui . Para dados persistentes quando a rota muda, veja a resposta de carloscarcamo. Tudo que você precisa é colocar os dados em um serviço.
Hugo Wood
2
ele persiste nas visualizações, exatamente da mesma maneira que você sugere, armazena-o em um serviço. Curiosamente, ele também persiste em uma página alterada da mesma maneira que você sugere com a janela. obrigado por me fazer verificar novamente. esta resposta tem mais de um ano e ainda parece ser a maneira mais simples de fazer as coisas com angular.
Anton
Obrigado por voltar aqui, mesmo que seja antigo :). Certifique-se de que sua solução cubra visualizações e páginas, mas não explica qual código serve a qual propósito, e não está completo o suficiente para ser utilizável . Eu estava apenas avisando os leitores para evitar mais perguntas . Seria ótimo se você tivesse tempo para fazer edições para melhorar sua resposta.
Hugo Wood
E se em seu controlador, em vez disso: $scope.user = userService;você tem algo parecido com isto: $scope.name = userService.model.name; $scope.email = userService.model.email;. Funcionará ou alterar as variáveis ​​em vista não o alterará no Serviço?
sports
2
eu acho que angular carece de um built-in mecanismo para tal coisa comum
obe6
22

Um pouco atrasado para uma resposta, mas apenas atualizou o violino com algumas práticas recomendadas

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
fonte
Eu até tenho que fazer isso entre as navegações da página (não as atualizações da página). isto está correto?
FutuToad
Eu acho que devo adicionar a parte para completar esta resposta, a updateServicedeve ser chamada quando o controlador for destruído $scope.$on('$destroy', function() { console.log('Ctrl destroyed'); $scope.updateServiceName($scope.name); });
#
10

$ rootScope é uma grande variável global, adequada para coisas pontuais ou aplicativos pequenos. Use um serviço se desejar encapsular seu modelo e / ou comportamento (e possivelmente reutilizá-lo em outro lugar). Além da postagem do grupo do Google no OP mencionado, consulte também https://groups.google.com/d/topic/angular/eegk_lB6kVs/discussion .

Mark Rajcok
fonte
8

Angular realmente não fornece o que você está procurando imediatamente. O que eu faria para realizar o que você procura é usar os seguintes complementos

UI Router e UI Router Extras

Esses dois fornecerão roteamento baseado em estado e estados de aderência. Você pode alternar entre estados e todas as informações serão salvas, pois o escopo "permanece ativo", por assim dizer.

Verifique a documentação dos dois, já que é bastante simples, os extras do ui router também apresentam uma boa demonstração de como os estados de aderência funcionam.

Joshua Kelly
fonte
Eu li os documentos, mas não consegui encontrar como preservar as entradas do formulário quando o usuário clica no botão Voltar do navegador. Você pode me indicar os detalhes, por favor? Obrigado!
Dalibor
6

Eu tive o mesmo problema, foi o que fiz: Eu tenho um SPA com várias visualizações na mesma página (sem ajax), portanto, este é o código do 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'});
}]);

Eu tenho apenas um controlador para todas as visualizações, mas, o problema é o mesmo da pergunta, o controlador sempre atualiza os dados. Para evitar esse comportamento, fiz o que as pessoas sugerem acima e criei um serviço para esse fim, depois passe-o para o controlador da seguinte maneira:

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

Agora eu posso chamar a função de atualização de qualquer uma das minhas visualizações, passar valores e atualizar meu modelo. Não preciso usar apis html5 para dados de persistência (no meu caso, talvez em outros casos seja necessário usar html5 APIs como armazenamento local e outras coisas).

carloscarcamo
fonte
sim, funcionou como um encanto. Nossa fábrica de dados já estava codificada e eu estava usando a sintaxe ControllerAs, então eu a reescrevi para $ scope controller, para que a Angular pudesse controlá-la. A implementação foi muito direta. Este é o primeiro aplicativo angular que estou escrevendo. tks
egidiocs
2

Uma alternativa aos serviços é usar o armazenamento de valor.

Na base do meu aplicativo, adicionei este

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

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

E então, no meu controlador, apenas refiro o armazenamento de valor. Eu não acho que vale tudo se o usuário fechar o 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
fonte
1

Solução que funcionará para vários escopos e várias variáveis ​​dentro desses escopos

Esse serviço foi baseado na resposta de Anton, mas é mais extensível e funcionará em vários escopos e permite a seleção de várias variáveis ​​de escopo no mesmo escopo. Ele usa o caminho da rota para indexar cada escopo e, em seguida, os nomes de variáveis ​​do escopo para indexar um nível mais a fundo.

Crie um serviço com 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;
}]);

Adicione este código à sua função de execução no seu módulo de aplicativo:

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

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

Injete o serviço restoreScope no seu controlador (exemplo abaixo):

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

O exemplo acima inicializará $ scope.user com o valor armazenado, caso contrário, será padronizado com o valor fornecido e o salvará. Se a página for fechada, atualizada ou a rota for alterada, os valores atuais de todas as variáveis ​​de escopo registradas serão salvos e restaurados na próxima vez que a rota / página for visitada.

Brett Pennings
fonte
Quando adicionei isso ao meu código, recebo um erro "TypeError: Não é possível ler a propriedade 'locals' de indefinido" Como faço para corrigir isso? @brett
Michael Mahony
Você está usando o Angular para lidar com suas rotas? Estou supondo que "não", já que $ route.current parece estar indefinido.
Brett Pennings
Sim, angular está lidando com meu roteamento. Aqui está uma amostra do que eu tenho no roteamento: JBenchApp.config (['$ routeProvider', '$ locationProvider', função ($ routeProvider, $ locationProvider) {$ routeProvider. When ('/ dashboard', {templateUrl: ' parcials / dashboard.html ', controller:' JBenchCtrl '})
Michael Mahony
Meu único outro palpite é que seu controlador esteja em execução antes de seu aplicativo ser configurado. De qualquer forma, você provavelmente poderia contornar o problema usando $ location em vez de $ route e passando o $ scope do controlador em vez de acessá-lo na rota. Tente usar gist.github.com/brettpennings/ae5d34de0961eef010c6 para o seu serviço e passe $ scope como seu terceiro parâmetro de restoreScope.GetOrRegisterScopeVariables no seu controlador. Se isso funcionar para você, talvez seja a minha nova resposta. Boa sorte!
Brett Pennings
1

Você pode usar $locationChangeStartevent para armazenar o valor anterior em $rootScopeou em um serviço. Quando você voltar, basta inicializar todos os valores armazenados anteriormente. Aqui está uma demonstração rápida usando $rootScope.

insira a descrição da imagem aqui

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
fonte