Jak ustawić przycisk Wstecz, aby działał z maszyną stanu routera AngularJS ui?

87

Zaimplementowałem jednostronicową aplikację angularjs przy użyciu routera ui .

Początkowo zidentyfikowałem każdy stan za pomocą odrębnego adresu URL, ale spowodowało to nieprzyjazne adresy URL z GUID.

Więc teraz zdefiniowałem moją witrynę jako znacznie prostszą maszynę stanu. Stany nie są identyfikowane przez adresy URL, ale są po prostu przenoszone zgodnie z wymaganiami, na przykład:

Zdefiniuj stany zagnieżdżone

angular
.module 'app', ['ui.router']
.config ($stateProvider) ->
    $stateProvider
    .state 'main', 
        templateUrl: 'main.html'
        controller: 'mainCtrl'
        params: ['locationId']

    .state 'folder', 
        templateUrl: 'folder.html'
        parent: 'main'
        controller: 'folderCtrl'
        resolve:
            folder:(apiService) -> apiService.get '#base/folder/#locationId'

Przejście do określonego stanu

#The ui-sref attrib transitions to the 'folder' state

a(ui-sref="folder({locationId:'{{folder.Id}}'})")
    | {{ folder.Name }}

Ten system działa bardzo dobrze i uwielbiam jego czystą składnię. Ponieważ jednak nie używam adresów URL, przycisk Wstecz nie działa.

Jak zachować schludny komputer stanu routera ui, ale włączyć funkcję przycisku Wstecz?

biofraktal
źródło
1
„stany nie są identyfikowane przez adresy URL” - i podejrzewam, że jest twój problem. Przycisk Wstecz jest dość chroniony przed kodem (w przeciwnym razie ludzie by go zastąpili, powodując problemy). Dlaczego po prostu nie pozwolić angularowi tworzyć lepszych adresów URL, tak jak robi to SO (OK, mogą nie używać kątowych, ale ich schemat adresów URL jest ilustracyjny)?
jcollum
Również to pytanie może pomóc: stackoverflow.com/questions/13499040/…
jcollum
Ponadto, skoro nie używasz adresów URL, czy nie oznacza to, że aby dostać się do stanu Z, ludzie będą musieli przejść przez stan X i Y, aby się do niego dostać? To może być denerwujące.
jcollum
czy będzie pasował do stanu z różnymi parametrami? @jcollum
VijayVishnu
Nie mam pojęcia, to było zbyt dawno
jcollum

Odpowiedzi:

78

Uwaga

Odpowiedzi, które sugerują użycie wariacji $window.history.back(), pominęły kluczową część pytania: Jak przywrócić stan aplikacji do prawidłowej lokalizacji stanu, gdy historia przeskakuje (wstecz / dalej / odświeżanie). Pamiętając o tym; proszę, czytaj dalej.


Tak, możliwe jest, aby przeglądarka przeszła wstecz / do przodu (historia) i odświeżyła się podczas uruchamiania czystego ui-routerautomatu stanowego, ale zajmuje to trochę czasu.

Potrzebujesz kilku komponentów:

  • Unikalne adresy URL . Przeglądarka włącza przyciski Wstecz / Dalej tylko podczas zmiany adresów URL, więc musisz wygenerować unikalny adres URL dla każdego odwiedzonego stanu. Te adresy URL nie muszą jednak zawierać żadnych informacji o stanie.

  • Usługa sesji . Każdy wygenerowany adres URL jest skorelowany z określonym stanem, więc potrzebujesz sposobu na przechowywanie par stanu adresu URL, aby można było pobrać informacje o stanie po ponownym uruchomieniu aplikacji kątowej za pomocą kliknięć wstecz / do przodu lub odświeżania.

  • Historia stanu . Prosty słownik stanów routera ui z kluczem unikalnym adresem URL. Jeśli możesz polegać na HTML5, możesz użyć interfejsu API historii HTML5 , ale jeśli tak jak ja nie możesz, możesz zaimplementować go samodzielnie w kilku wierszach kodu (patrz poniżej).

  • Usługa lokalizacji . Wreszcie, musisz mieć możliwość zarządzania zarówno zmianami stanu routera ui, wywoływanymi wewnętrznie przez Twój kod, jak i normalnymi zmianami adresu URL przeglądarki, zwykle wywoływanymi przez użytkownika klikającego przyciski przeglądarki lub wpisującego coś na pasku przeglądarki. To wszystko może być nieco skomplikowane, ponieważ łatwo jest zdezorientować, co spowodowało co.

Oto moja implementacja każdego z tych wymagań. Powiązałem wszystko w trzy usługi:

Usługa sesji

class SessionService

    setStorage:(key, value) ->
        json =  if value is undefined then null else JSON.stringify value
        sessionStorage.setItem key, json

    getStorage:(key)->
        JSON.parse sessionStorage.getItem key

    clear: ->
        @setStorage(key, null) for key of sessionStorage

    stateHistory:(value=null) ->
        @accessor 'stateHistory', value

    # other properties goes here

    accessor:(name, value)->
        return @getStorage name unless value?
        @setStorage name, value

angular
.module 'app.Services'
.service 'sessionService', SessionService

To jest opakowanie dla sessionStorageobiektu javascript . Obciąłem to dla jasności. Aby uzyskać pełne wyjaśnienie, zobacz: Jak obsłużyć odświeżanie strony za pomocą aplikacji jednostronicowej AngularJS

Usługa historii stanu

class StateHistoryService
    @$inject:['sessionService']
    constructor:(@sessionService) ->

    set:(key, state)->
        history = @sessionService.stateHistory() ? {}
        history[key] = state
        @sessionService.stateHistory history

    get:(key)->
        @sessionService.stateHistory()?[key]

angular
.module 'app.Services'
.service 'stateHistoryService', StateHistoryService

Do StateHistoryServicedba przechowywania i udostępniania stanów historycznych kluczach będących generowanych, unikalnych adresów URL. Tak naprawdę jest to po prostu wygodna aplikacja dla obiektu w stylu słownika.

Stanowa usługa lokalizacji

class StateLocationService
    preventCall:[]
    @$inject:['$location','$state', 'stateHistoryService']
    constructor:(@location, @state, @stateHistoryService) ->

    locationChange: ->
        return if @preventCall.pop('locationChange')?
        entry = @stateHistoryService.get @location.url()
        return unless entry?
        @preventCall.push 'stateChange'
        @state.go entry.name, entry.params, {location:false}

    stateChange: ->
        return if @preventCall.pop('stateChange')?
        entry = {name: @state.current.name, params: @state.params}
        #generate your site specific, unique url here
        url = "/#{@state.params.subscriptionUrl}/#{Math.guid().substr(0,8)}"
        @stateHistoryService.set url, entry
        @preventCall.push 'locationChange'
        @location.url url

angular
.module 'app.Services'
.service 'stateLocationService', StateLocationService

StateLocationServiceObsługuje dwa zdarzenia:

  • locationChange . Jest to wywoływane w przypadku zmiany lokalizacji przeglądarki, zwykle po naciśnięciu przycisku Wstecz / Dalej / Odśwież lub po pierwszym uruchomieniu aplikacji lub gdy użytkownik wpisze adres URL. Jeśli stan dla bieżącej lokalizacji.url istnieje w, StateHistoryServiceto jest używany do przywrócenia stanu za pośrednictwem routera ui $state.go.

  • stateChange . Nazywa się to, gdy przenosisz stan wewnętrznie. Nazwa i parametry bieżącego stanu są przechowywane w StateHistoryServicekluczu przez wygenerowany adres URL. Ten wygenerowany adres URL może być dowolny, może, ale nie musi, identyfikować stan w sposób czytelny dla człowieka. W moim przypadku używam parametru stanu oraz losowo generowanej sekwencji cyfr pochodzących z guid (patrz stopa dla fragmentu generatora guid). Wygenerowany adres URL jest wyświetlany na pasku przeglądarki i, co najważniejsze, dodawany do wewnętrznego stosu historii przeglądarki za pomocą @location.url url. Dodaje adres URL do stosu historii przeglądarki, który włącza przyciski Dalej / Wstecz.

Duży problem z tej techniki jest to, że wywołanie @location.url urlw stateChangemetodzie wywoła $locationChangeSuccesszdarzenie i tak wywołać locationChangemetodę. Równie wywołanie @state.gofrom locationChangewyzwoli $stateChangeSuccesszdarzenie, a więcstateChange metodę. To staje się bardzo mylące i bez końca psuje historię przeglądarki.

Rozwiązanie jest bardzo proste. Możesz zobaczyć preventCalltablicę używaną jako stos ( popipush ). Za każdym razem, gdy wywoływana jest jedna z metod, zapobiega to jednorazowemu wywołaniu drugiej metody. Ta technika nie koliduje z prawidłowym wyzwalaniem zdarzeń $ i utrzymuje wszystko prosto.

Teraz wszystko, co musimy zrobić, to wywołać HistoryServicemetody w odpowiednim momencie cyklu życia zmiany stanu. Odbywa się to w .runmetodzie AngularJS Apps , na przykład:

Angular app.run

angular
.module 'app', ['ui.router']
.run ($rootScope, stateLocationService) ->

    $rootScope.$on '$stateChangeSuccess', (event, toState, toParams) ->
        stateLocationService.stateChange()

    $rootScope.$on '$locationChangeSuccess', ->
        stateLocationService.locationChange()

Wygeneruj Guid

Math.guid = ->
    s4 = -> Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1)
    "#{s4()}#{s4()}-#{s4()}-#{s4()}-#{s4()}-#{s4()}#{s4()}#{s4()}"

Po tym wszystkim przyciski do przodu / do tyłu i przycisk odświeżania działają zgodnie z oczekiwaniami.

biofraktal
źródło
1
w powyższym przykładzie SessionService, myślę, że metoda accessor: powinna używać @setStoragei @getStoragezamiast `@ get / setSession '.
Peter Whitfield
3
Który język jest używany w tym przykładzie. Nie wydaje się być kanciasty, który znam.
deepak
Język to javascript, a składnia to coffeescript.
biofraktal
1
@jlguenego Masz funkcjonalną historię przeglądarki / przeglądarkę przycisków w przód iw tył oraz adresy URL, które możesz dodać do zakładek.
Torsten Barthel
3
@jlguenego - odpowiedzi sugerujące użycie wariacji pominęły $window.history.back()kluczową część pytania. Chodziło o to, aby przywrócić aplikacji stanu do właściwego państwowego miejscu jak historia skoków (tył / przód / odświeżania). Zwykle można to osiągnąć, dostarczając dane stanu za pośrednictwem identyfikatora URI. Pytanie dotyczyło sposobu przeskakiwania między lokalizacjami stanu bez (jawnych) danych stanu URI. Biorąc pod uwagę to ograniczenie, nie wystarczy po prostu zreplikować przycisku Wstecz, ponieważ opiera się on na danych stanu URI w celu ponownego ustanowienia lokalizacji stanu.
biofraktal
46
app.run(['$window', '$rootScope', 
function ($window ,  $rootScope) {
  $rootScope.goBack = function(){
    $window.history.back();
  }
}]);

<a href="#" ng-click="goBack()">Back</a>
Guillaume Massé
źródło
2
kocham to! ... lol ... dla jasności $window.history.backrobi magię, a nie $rootScope... więc powrót może być związany z zakresem dyrektywy navbar, jeśli chcesz.
Benjamin Conant
@BenjaminConant W przypadku osób, które nie wiedzą, jak to zaimplementować, wystarczy umieścić $window.history.back();funkcję, która ma zostać wywołana ng-click.
chakeda
prawidłowy rootScope jest tylko po to, aby funkcja była dostępna w dowolnym szablonie
Guillaume Massé
Kolacja z kurczakiem.
Cody
23

Po przetestowaniu różnych propozycji stwierdziłem, że najłatwiejszy sposób jest często najlepszy.

Jeśli używasz kątowego routera ui i potrzebujesz przycisku, aby wrócić najlepiej, to:

<button onclick="history.back()">Back</button>

lub

<a onclick="history.back()>Back</a>

// Uwaga, nie ustawiaj atrybutu href, bo ścieżka zostanie uszkodzona.

Objaśnienie: Załóżmy, że jest to standardowa aplikacja do zarządzania. Wyszukaj obiekt -> Wyświetl obiekt -> Edytuj obiekt

Korzystanie z rozwiązań kątowych Z tego stanu:

Wyszukaj -> Widok -> Edytuj

Do :

Wyszukaj -> Wyświetl

Chcieliśmy tego, z wyjątkiem tego, że teraz klikniesz przycisk Wstecz w przeglądarce, a będziesz tam ponownie:

Wyszukaj -> Widok -> Edytuj

I to nie jest logiczne

Jednak używając prostego rozwiązania

<a onclick="history.back()"> Back </a>

z :

Wyszukaj -> Widok -> Edytuj

po kliknięciu przycisku:

Wyszukaj -> Wyświetl

po kliknięciu przycisku Wstecz w przeglądarce:

Szukaj

Szanowana jest spójność. :-)

bArraxas
źródło
7

Jeśli szukasz najprostszego przycisku „Wstecz”, możesz ustawić taką dyrektywę:

    .directive('back', function factory($window) {
      return {
        restrict   : 'E',
        replace    : true,
        transclude : true,
        templateUrl: 'wherever your template is located',
        link: function (scope, element, attrs) {
          scope.navBack = function() {
            $window.history.back();
          };
        }
      };
    });

Należy pamiętać, że jest to dość nieinteligentny przycisk „Wstecz”, ponieważ korzysta z historii przeglądarki. Jeśli umieścisz go na swojej stronie docelowej, odeśle użytkownika z powrotem do dowolnego adresu URL, z którego przyszedł przed wylądowaniem na Twoim.

dgrant069
źródło
3

rozwiązanie przycisku wstecz / dalej przeglądarki
Napotkałem ten sam problem i rozwiązałem go za pomocą obiektu popstate eventz $ window i ui-router's $state object. Zdarzenie popstate jest wywoływane do okna za każdym razem, gdy zmienia się aktywny wpis historii.
Te $stateChangeSuccessi $locationChangeSuccesszdarzenia nie są wywoływane na przeglądarki kliknięcia przycisku chociaż pasek adresu wskazuje nową lokalizację.
Tak więc, zakładając, że już żeglował od stanów maindo foldercelu mainponownie, gdy trafisz backna przeglądarce, powinieneś być z powrotem na foldertrasie. Ścieżka jest aktualizowana, ale widok nie jest i nadal wyświetla wszystko, co masz main. Spróbuj tego:

angular
.module 'app', ['ui.router']
.run($state, $window) {

     $window.onpopstate = function(event) {

        var stateName = $state.current.name,
            pathname = $window.location.pathname.split('/')[1],
            routeParams = {};  // i.e.- $state.params

        console.log($state.current.name, pathname); // 'main', 'folder'

        if ($state.current.name.indexOf(pathname) === -1) {
            // Optionally set option.notify to false if you don't want 
            // to retrigger another $stateChangeStart event
            $state.go(
              $state.current.name, 
              routeParams,
              {reload:true, notify: false}
            );
        }
    };
}

przyciski wstecz / dalej powinny działać płynnie po tym.

uwaga: aby mieć pewność, sprawdź zgodność przeglądarki dla window.onpopstate ()

jfab fab
źródło
3

Można to rozwiązać za pomocą prostej dyrektywy „powrót do historii”, która również zamyka okno w przypadku braku historii.

Stosowanie dyrektywy

<a data-go-back-history>Previous State</a>

Deklaracja dyrektywy Angular

.directive('goBackHistory', ['$window', function ($window) {
    return {
        restrict: 'A',
        link: function (scope, elm, attrs) {
            elm.on('click', function ($event) {
                $event.stopPropagation();
                if ($window.history.length) {
                    $window.history.back();
                } else {
                    $window.close();  
                }
            });
        }
    };
}])

Uwaga: praca z routerem ui lub nie.

hthetiot
źródło
0

Przycisk Wstecz również nie działał dla mnie, ale zorientowałem się, że problem polegał na tym, że miałem htmlzawartość wewnątrz mojej strony głównej, w ui-viewelemencie.

to znaczy

<div ui-view>
     <h1> Hey Kids! </h1>
     <!-- More content -->
</div>

Dlatego przeniosłem zawartość do nowego .htmlpliku i oznaczyłem ją jako szablon w .jspliku z trasami.

to znaczy

   .state("parent.mystuff", {
        url: "/mystuff",
        controller: 'myStuffCtrl',
        templateUrl: "myStuff.html"
    })
CodyBugstein
źródło
-1

history.back()i przejdź do poprzedniego stanu często daje efekt nie taki, jakiego chcesz. Na przykład, jeśli masz formularz z zakładkami, a każda karta ma swój własny stan, to właśnie zmieniło wybraną poprzednią kartę, a nie powróci z formularza. W przypadku stanów zagnieżdżonych zwykle potrzebujesz, więc pomyśl o wiedźmie ze stanów macierzystych, które chcesz wycofać.

Ta dyrektywa rozwiązuje problem

angular.module('app', ['ui-router-back'])

<span ui-back='defaultState'> Go back </span>

Powraca do stanu, który był aktywny przed wyświetleniem przycisku. OpcjonalnydefaultState jest nazwa stanu używana, gdy w pamięci nie ma poprzedniego stanu. Przywraca również pozycję przewijania

Kod

class UiBackData {
    fromStateName: string;
    fromParams: any;
    fromStateScroll: number;
}

interface IRootScope1 extends ng.IScope {
    uiBackData: UiBackData;
}

class UiBackDirective implements ng.IDirective {
    uiBackDataSave: UiBackData;

    constructor(private $state: angular.ui.IStateService,
        private $rootScope: IRootScope1,
        private $timeout: ng.ITimeoutService) {
    }

    link: ng.IDirectiveLinkFn = (scope, element, attrs) => {
        this.uiBackDataSave = angular.copy(this.$rootScope.uiBackData);

        function parseStateRef(ref, current) {
            var preparsed = ref.match(/^\s*({[^}]*})\s*$/), parsed;
            if (preparsed) ref = current + '(' + preparsed[1] + ')';
            parsed = ref.replace(/\n/g, " ").match(/^([^(]+?)\s*(\((.*)\))?$/);
            if (!parsed || parsed.length !== 4)
                throw new Error("Invalid state ref '" + ref + "'");
            let paramExpr = parsed[3] || null;
            let copy = angular.copy(scope.$eval(paramExpr));
            return { state: parsed[1], paramExpr: copy };
        }

        element.on('click', (e) => {
            e.preventDefault();

            if (this.uiBackDataSave.fromStateName)
                this.$state.go(this.uiBackDataSave.fromStateName, this.uiBackDataSave.fromParams)
                    .then(state => {
                        // Override ui-router autoscroll 
                        this.$timeout(() => {
                            $(window).scrollTop(this.uiBackDataSave.fromStateScroll);
                        }, 500, false);
                    });
            else {
                var r = parseStateRef((<any>attrs).uiBack, this.$state.current);
                this.$state.go(r.state, r.paramExpr);
            }
        });
    };

    public static factory(): ng.IDirectiveFactory {
        const directive = ($state, $rootScope, $timeout) =>
            new UiBackDirective($state, $rootScope, $timeout);
        directive.$inject = ['$state', '$rootScope', '$timeout'];
        return directive;
    }
}

angular.module('ui-router-back')
    .directive('uiBack', UiBackDirective.factory())
    .run(['$rootScope',
        ($rootScope: IRootScope1) => {

            $rootScope.$on('$stateChangeSuccess',
                (event, toState, toParams, fromState, fromParams) => {
                    if ($rootScope.uiBackData == null)
                        $rootScope.uiBackData = new UiBackData();
                    $rootScope.uiBackData.fromStateName = fromState.name;
                    $rootScope.uiBackData.fromStateScroll = $(window).scrollTop();
                    $rootScope.uiBackData.fromParams = fromParams;
                });
        }]);
Sel
źródło