Zachowaj model zakresu podczas zmiany między widokami w AngularJS


152

Uczę się AngularJS. Powiedzmy, że mam / view1 używając My1Ctrl i / view2 używając My2Ctrl ; można przejść do zakładek, gdzie każdy widok ma swój własny, prosty, ale inny formularz.

Jak mam się upewnić, że wartości wpisane w formularzu view1 nie zostaną zresetowane, gdy użytkownik wyjdzie, a następnie wróci do view1 ?

Chodzi mi o to, jak druga wizyta w programie view1 może zachować dokładnie ten sam stan modelu, w jakim go zostawiłem?

Odpowiedzi:


174

Poświęciłem trochę czasu, aby znaleźć najlepszy sposób na zrobienie tego. Chciałem też zachować stan, w którym użytkownik opuszcza stronę, a następnie naciska przycisk Wstecz, aby wrócić do starej strony; a nie tylko umieszczać wszystkie moje dane w głównym zakresie .

Końcowym rezultatem jest usługa dla każdego kontrolera . W kontrolerze masz tylko funkcje i zmienne, o które nie dbasz, jeśli są wyczyszczone.

Usługa dla kontrolera jest wstrzykiwana przez iniekcję zależności. Ponieważ usługi są singletonami, ich dane nie są niszczone tak jak dane w kontrolerze.

W serwisie mam modelkę. model zawiera TYLKO dane - bez funkcji -. W ten sposób można go konwertować z formatu JSON do przodu i do tyłu, aby go zachować. Użyłem lokalnego magazynu html5 dla trwałości.

Na koniec użyłem window.onbeforeunloadi $rootScope.$broadcast('saveState');powiadomiłem wszystkie usługi, że powinny zapisać swój stan, i $rootScope.$broadcast('restoreState')aby dać im znać, aby przywrócić ich stan (używane, gdy użytkownik opuszcza stronę i naciska przycisk Wstecz, aby odpowiednio wrócić do strony).

Przykładowa usługa o nazwie userService dla mojego 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;
}]);

Przykład userController

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

Widok następnie używa takiego wiązania

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

Natomiast w module aplikacji , w ramach funkcji run, obsługuję rozgłaszanie stanów saveState i 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');
};

Jak wspomniałem, dojście do tego punktu zajęło trochę czasu. Jest to bardzo czysty sposób, ale zrobienie czegoś, co podejrzewam, że jest bardzo częstym problemem podczas programowania w Angular, wymaga sporo inżynierii.

Chciałbym widzieć łatwiejsze, ale jako przejrzyste sposoby radzenia sobie z utrzymywaniem stanu między kontrolerami, w tym gdy użytkownik opuszcza stronę i wraca do niej.


10
Opisuje, jak utrwalać dane podczas odświeżania strony, a nie jak je utrwalać, gdy trasa się zmienia, więc nie zawiera odpowiedzi na pytanie. Ponadto nie będzie działać w przypadku aplikacji, które nie używają tras. Poza tym nie pokazuje, gdzie sessionStorage.restoreStatejest ustawione "true". Aby uzyskać prawidłowe rozwiązanie do utrwalania danych podczas odświeżania strony, zajrzyj tutaj . Aby uzyskać trwałe dane, gdy zmienia się trasa, spójrz na odpowiedź Carloscarcamo. Wszystko, czego potrzebujesz, to umieścić dane w usłudze.
Hugo Wood

2
zachowuje się w różnych widokach, dokładnie w taki sam sposób, w jaki sugerujesz, przechowując go w usłudze. co ciekawe, utrzymuje się również na zmianie strony, tak samo jak sugestia z window.onbeforeunload. dziękuję za podwójne sprawdzenie. ta odpowiedź ma ponad rok i nadal wydaje się być najprostszym sposobem na zrobienie rzeczy z kątami.
Anton

Dziękuję za powrót, mimo że jest stary :). Z pewnością rozwiązanie obejmuje zarówno widoki, jak i strony, ale nie wyjaśnia, który kod służy do jakiego celu, i nie jest na tyle kompletne, aby można go było używać . Po prostu ostrzegałem czytelników, aby unikali dalszych pytań . Byłoby wspaniale, gdybyś miał czas na wprowadzenie zmian w celu poprawienia odpowiedzi.
Hugo Wood

Co zrobić, jeśli w kontrolerze zamiast tego: $scope.user = userService;masz coś takiego: $scope.name = userService.model.name; $scope.email = userService.model.email;. Czy to zadziała, czy zmiana widocznych zmiennych nie zmieni tego w usłudze?
sport

2
myślę, że angular nie ma wbudowanego mechanizmu do tak powszechnej rzeczy
obe6

22

Trochę za późno na odpowiedź, ale właśnie zaktualizowałem skrzypce z najlepszymi praktykami

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

Muszę to nawet robić między nawigacjami po stronie (nie odświeżaniem strony). czy to jest poprawne?
FutuToad

Myślę, że powinienem dodać część, aby uzupełnić tę odpowiedź, updateServicenależy wywołać, gdy kontroler zostanie zniszczony - $scope.$on('$destroy', function() { console.log('Ctrl destroyed'); $scope.updateServiceName($scope.name); });
Shivendra


8

Angular tak naprawdę nie zapewnia tego, czego szukasz po wyjęciu z pudełka. To, co chciałbym zrobić, aby osiągnąć to, czego szukasz, to użyć następujących dodatków

Router UI i Dodatki do routera UI

Te dwie opcje zapewniają routing oparty na stanach i stany trwałe. Możesz przełączać między stanami, a wszystkie informacje zostaną zapisane, ponieważ zakres „pozostaje żywy”, że tak powiem.

Sprawdź dokumentację obu, ponieważ jest całkiem prosta, dodatki do routera interfejsu użytkownika mają również dobrą demonstrację działania lepkich stanów.


Przeczytałem dokumenty, ale nie mogłem znaleźć sposobu zachowania danych wejściowych formularza, gdy użytkownik kliknie przycisk Wstecz w przeglądarce. Czy możesz wskazać mi szczegóły, proszę? Dzięki!
Dalibor

6

Miałem ten sam problem, oto co zrobiłem: mam SPA z wieloma widokami na tej samej stronie (bez Ajax), więc to jest kod modułu:

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

Mam tylko jeden kontroler do wszystkich widoków, ale problem jest taki sam jak pytanie, kontroler zawsze odświeża dane, aby uniknąć tego zachowania zrobiłem to co ludzie sugerują powyżej i stworzyłem w tym celu usługę, a następnie ją przekazuję do kontrolera w następujący sposób:

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

Teraz mogę wywołać funkcję aktualizacji z dowolnego z moich widoków, przekazać wartości i zaktualizować mój model, nie muszę używać interfejsu API html5 do danych trwałości (tak jest w moim przypadku, może w innych przypadkach byłoby konieczne użycie html5 api, takie jak localstorage i inne rzeczy).


tak, działało jak urok. Nasza fabryka danych była już zakodowana i używałem składni ControllerAs, więc przepisałem ją na kontroler $ scope, aby Angular mógł nią sterować. Wdrożenie było bardzo proste. To pierwsza aplikacja kątowa, którą piszę. tks
egidiocs

2

Alternatywą dla usług jest wykorzystanie magazynu wartości.

W bazie mojej aplikacji dodałem to

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

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

A potem w moim kontrolerze po prostu odwołuję się do magazynu wartości. Nie sądzę, aby miało to znaczenie, jeśli użytkownik zamyka przeglądarkę.

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

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

1

Rozwiązanie, które będzie działać dla wielu zakresów i wielu zmiennych w tych zakresach

Ta usługa została oparta na odpowiedzi Antona, ale jest bardziej rozszerzalna i będzie działać w wielu zakresach oraz umożliwia wybór wielu zmiennych zakresu w tym samym zakresie. Używa ścieżki trasy do indeksowania każdego zakresu, a następnie nazw zmiennych zakresu do indeksowania o jeden poziom głębiej.

Utwórz usługę za pomocą tego kodu:

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

Dodaj ten kod do funkcji uruchamiania w module aplikacji:

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

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

Wprowadź usługę restoreScope do kontrolera (przykład poniżej):

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

Powyższy przykład zainicjuje $ scope.user do zapisanej wartości, w przeciwnym razie domyślnie użyje podanej wartości i zapisze ją. Jeśli strona zostanie zamknięta, odświeżona lub trasa zostanie zmieniona, bieżące wartości wszystkich zarejestrowanych zmiennych zakresu zostaną zapisane i zostaną przywrócone podczas następnej wizyty na trasie / stronie.


Po dodaniu tego do mojego kodu pojawia się błąd „TypeError: Cannot read property 'locals' of undefined" Jak to naprawić? @brett
Michael Mahony

Czy używasz Angulara do obsługi tras? Zgaduję, że "nie", ponieważ $ route.current wydaje się być niezdefiniowana.
Brett Pennings

Tak, angular obsługuje mój routing. Oto próbka tego, co mam w routingu: JBenchApp.config (['$ routeProvider', '$ locationProvider', function ($ routeProvider, $ locationProvider) {$ routeProvider. When ('/ dashboard', {templateUrl: ' częściowe / dashboard.html ', kontroler:' JBenchCtrl '})
Michael Mahony

Moim jedynym innym przypuszczeniem jest to, że kontroler działa przed skonfigurowaniem aplikacji. Tak czy inaczej, prawdopodobnie można by obejść ten problem, używając $ location zamiast $ route, a następnie przekazując $ scope z kontrolera zamiast uzyskiwać do niego dostęp w trasie. Spróbuj zamiast tego użyć gist.github.com/brettpennings/ae5d34de0961eef010c6 dla swojej usługi i przekaż $ scope jako trzeci parametr restoreScope.GetOrRegisterScopeVariables w kontrolerze. Jeśli to zadziała, mogę po prostu uczynić to moją nową odpowiedzią. Powodzenia!
Brett Pennings

1

Możesz użyć $locationChangeStartzdarzenia do przechowywania poprzedniej wartości w $rootScopeusłudze lub w usłudze. Kiedy wrócisz, po prostu zainicjuj wszystkie wcześniej zapisane wartości. Oto szybkie demo przy użyciu $rootScope.

wprowadź opis obrazu tutaj

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>

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.