Wie kann ich den Zurück-Button mit einer AngularJS-UI-Router-Zustandsmaschine arbeiten lassen?

87

Ich habe eine AngularJS Single Page-Anwendung mit UI -Router implementiert .

Ursprünglich identifizierte ich jeden Zustand mit einer eigenen URL, was jedoch zu unfreundlichen, GUID-gepackten URLs führte.

Deshalb habe ich meine Site jetzt als eine viel einfachere Zustandsmaschine definiert. Die Zustände werden nicht durch URLs identifiziert, sondern einfach nach Bedarf wie folgt geändert:

Verschachtelte Zustände definieren

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'

Übergang in einen definierten Zustand

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

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

Dieses System funktioniert sehr gut und ich liebe seine saubere Syntax. Da ich jedoch keine URLs verwende, funktioniert der Zurück-Button nicht.

Wie behalte ich meine ordentliche UI-Router-Zustandsmaschine bei, aktiviere aber die Back-Button-Funktionalität?

Biofractal
quelle
1
"Zustände werden nicht durch URLs identifiziert" - und ich vermute, es gibt Ihr Problem. Die Schaltfläche "Zurück" ist ziemlich gut vor dem Code geschützt (andernfalls würden die Benutzer ihn überschreiben und Probleme verursachen). Warum nicht einfach Angular bessere URLs erstellen lassen, wie es SO tut (OK, sie verwenden möglicherweise keine Angulars, aber ihr URL-Schema ist illustrativ)?
JCollum
Auch diese Frage kann helfen: stackoverflow.com/questions/13499040/…
jcollum
Da Sie keine URLs verwenden, bedeutet dies nicht, dass Personen, um zum Status Z zu gelangen, durch den Status X und Y klicken müssen, um dorthin zu gelangen? Das könnte nervig werden.
JCollum
Wird es mit dem Status mit verschiedenen Parametern gehen? @jcollum
VijayVishnu
Ich habe keine Ahnung, das ist zu lange her
jcollum

Antworten:

78

Hinweis

Die Antworten, die die Verwendung von Variationen von vorschlagen, $window.history.back()haben alle einen entscheidenden Teil der Frage übersehen: Wie kann der Status der Anwendung beim Springen des Verlaufs an den richtigen Status zurückgesetzt werden (Zurück / Vorwärts / Aktualisieren)? In diesem Sinne; Bitte lesen Sie weiter.


Ja, es ist möglich, den Browser vor / zurück (Verlauf) zu halten und zu aktualisieren, während eine reine ui-routerZustandsmaschine ausgeführt wird, aber es ist ein wenig erforderlich.

Sie benötigen mehrere Komponenten:

  • Eindeutige URLs . Der Browser aktiviert die Vor- / Zurück-Schaltflächen nur, wenn Sie URLs ändern. Daher müssen Sie pro besuchtem Status eine eindeutige URL generieren. Diese URLs müssen jedoch keine Statusinformationen enthalten.

  • Ein Sitzungsdienst . Jede generierte URL ist mit einem bestimmten Status korreliert, sodass Sie eine Möglichkeit zum Speichern Ihrer URL-Status-Paare benötigen, damit Sie die Statusinformationen abrufen können, nachdem Ihre eckige App durch Zurück / Vorwärts- oder Aktualisierungsklicks neu gestartet wurde.

  • Eine Staatsgeschichte . Ein einfaches Wörterbuch mit UI-Router-Status, das durch eine eindeutige URL gekennzeichnet ist. Wenn Sie sich auf HTML5 verlassen können, können Sie die HTML5-Verlaufs-API verwenden . Wenn Sie dies jedoch nicht können, können Sie sie selbst in wenigen Codezeilen implementieren (siehe unten).

  • Ein Ortungsdienst . Schließlich müssen Sie in der Lage sein, sowohl Statusänderungen des UI-Routers, die intern durch Ihren Code ausgelöst werden, als auch normale Änderungen der Browser-URL zu verwalten, die normalerweise durch das Klicken des Benutzers auf Browser-Schaltflächen oder das Eingeben von Inhalten in die Browserleiste ausgelöst werden. Dies kann alles etwas schwierig werden, da es leicht zu verwechseln ist, was was ausgelöst hat.

Hier ist meine Implementierung jeder dieser Anforderungen. Ich habe alles in drei Dienste gebündelt:

Der Sitzungsdienst

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

Dies ist ein Wrapper für das Javascript- sessionStorageObjekt. Ich habe es hier aus Gründen der Klarheit gekürzt. Eine vollständige Erklärung hierzu finden Sie unter: Wie gehe ich mit der Seitenaktualisierung mit einer AngularJS-Einzelseitenanwendung um?

Der Staatliche Geschichtsdienst

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

Das StateHistoryServicekümmert sich um das Speichern und Abrufen von historischen Zuständen, die durch generierte, eindeutige URLs verschlüsselt sind. Es ist wirklich nur ein Convenience-Wrapper für ein Objekt im Wörterbuchstil.

Der staatliche Ortungsdienst

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

Das StateLocationServicebehandelt zwei Ereignisse:

  • locationChange . Dies wird aufgerufen, wenn der Speicherort des Browsers geändert wird, normalerweise wenn die Schaltfläche Zurück / Vorwärts / Aktualisieren gedrückt wird oder wenn die App zum ersten Mal gestartet wird oder wenn der Benutzer eine URL eingibt. Wenn ein Status für die aktuelle location.url in der vorhanden StateHistoryServiceist, wird er verwendet, um den Status über den UI-Router wiederherzustellen $state.go.

  • stateChange . Dies wird aufgerufen, wenn Sie den Status intern verschieben. Der Name und die Parameter des aktuellen Status werden in der StateHistoryServicevon einer generierten URL eingegebenen URL gespeichert . Diese generierte URL kann alles sein, was Sie wollen, sie kann den Zustand auf eine für Menschen lesbare Weise identifizieren oder nicht. In meinem Fall verwende ich einen Statusparameter plus eine zufällig generierte Folge von Ziffern, die von einem Guid abgeleitet wurden (siehe Fuß für das Guid-Generator-Snippet). Die generierte URL wird in der Browserleiste angezeigt und mit entscheidend zum internen Verlaufsstapel des Browsers hinzugefügt @location.url url. Es fügt die URL zum Verlaufsstapel des Browsers hinzu, der die Vorwärts- / Zurück-Schaltflächen aktiviert.

Das große Problem bei dieser Technik ist, dass das Aufrufen @location.url urlder stateChangeMethode das $locationChangeSuccessEreignis auslöst und somit die locationChangeMethode aufruft . Gleiches Ausrufen von @state.gofrom locationChangelöst das $stateChangeSuccessEreignis und damit die stateChangeMethode aus. Dies wird sehr verwirrend und bringt den Browserverlauf ohne Ende durcheinander.

Die Lösung ist sehr einfach. Sie können sehen, dass das preventCallArray als Stapel ( popund push) verwendet wird. Jedes Mal, wenn eine der Methoden aufgerufen wird, wird verhindert, dass die andere Methode nur einmal aufgerufen wird. Diese Technik beeinträchtigt nicht die korrekte Auslösung der $ -Ereignisse und hält alles gerade.

Jetzt müssen wir nur noch die HistoryServiceMethoden zum richtigen Zeitpunkt im Lebenszyklus des Zustandsübergangs aufrufen . Dies geschieht in der AngularJS Apps- .runMethode wie folgt :

Winkel app.run

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

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

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

Generieren Sie eine Guid

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

Wenn dies alles vorhanden ist, funktionieren die Vorwärts- / Rückwärts-Tasten und die Aktualisierungs-Taste wie erwartet.

Biofractal
quelle
1
Im obigen Beispiel für den SessionService sollte die accessor: -Methode @setStorageund @getStoragenicht "@ get / setSession" verwenden.
Peter Whitfield
3
Welche Sprache wird für dieses Beispiel verwendet? Scheint nicht eckig zu sein, mit dem ich vertraut bin.
Deepak
Die Sprache war Javascript, die Syntax war Coffeescript.
Biofractal
1
@jlguenego Sie haben einen funktionierenden Browserverlauf / Browser-Hin- und Her-Schaltflächen und URLs, die Sie mit einem Lesezeichen versehen können.
Torsten Barthel
3
@jlguenego - Die Antworten, die die Verwendung von Variationen von vorschlagen, $window.history.back()haben einen entscheidenden Teil der Frage übersehen. Es ging darum, den Status der Anwendung wiederherzustellen an der richtigen Position wenn der Verlauf springt (zurück / vorwärts / aktualisieren). Dies würde normalerweise durch die Bereitstellung der Statusdaten über den URI erreicht. Bei der Frage wurde gefragt, wie zwischen Statuspositionen ohne (explizite) URI-Statusdaten gesprungen werden soll. Angesichts dieser Einschränkung reicht es nicht aus, nur die Zurück-Schaltfläche zu replizieren, da dies auf den URI-Statusdaten beruht, um den Statusspeicherort wiederherzustellen.
Biofractal
46
app.run(['$window', '$rootScope', 
function ($window ,  $rootScope) {
  $rootScope.goBack = function(){
    $window.history.back();
  }
}]);

<a href="#" ng-click="goBack()">Back</a>
Guillaume Massé
quelle
2
liebe das! ... lol ... aus Gründen $window.history.backder Klarheit macht die Magie nicht $rootScope... also könnte das Zurückgehen an Ihren Navigationsbereich gebunden sein, wenn Sie möchten.
Benjamin Conant
@BenjaminConant Für Leute, die nicht wissen, wie man dies implementiert, fügen Sie einfach die $window.history.back();Funktion ein, die aufgerufen werden soll ng-click.
Chakeda
korrektes rootScope dient nur dazu, die Funktion in einer beliebigen Vorlage zugänglich zu machen
Guillaume Massé
Hühnchen Abendessen.
Cody
23

Nachdem ich verschiedene Vorschläge getestet hatte, stellte ich fest, dass der einfachste Weg oft der beste ist.

Wenn Sie einen eckigen UI-Router verwenden und eine Schaltfläche benötigen, um am besten zurückzukehren, ist dies:

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

oder

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

// Warnung setze nicht die href, sonst wird der Pfad unterbrochen.

Erläuterung: Angenommen, eine Standardverwaltungsanwendung. Suchobjekt -> Objekt anzeigen -> Objekt bearbeiten

Verwendung der Winkellösungen Aus diesem Zustand:

Suchen -> Anzeigen -> Bearbeiten

An:

Suchen -> Anzeigen

Nun, das wollten wir, außer wenn Sie jetzt auf die Schaltfläche "Zurück" des Browsers klicken, sind Sie wieder da:

Suchen -> Anzeigen -> Bearbeiten

Und das ist nicht logisch

Jedoch mit der einfachen Lösung

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

von :

Suchen -> Anzeigen -> Bearbeiten

nach dem Klicken auf die Schaltfläche:

Suchen -> Anzeigen

nach dem Klicken auf die Zurück-Schaltfläche des Browsers:

Suche

Konsistenz wird respektiert. :-)

bArraxas
quelle
7

Wenn Sie nach dem einfachsten "Zurück" -Button suchen, können Sie eine Direktive wie folgt einrichten:

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

Beachten Sie, dass dies eine ziemlich unintelligente Schaltfläche "Zurück" ist, da sie den Verlauf des Browsers verwendet. Wenn Sie es auf Ihrer Zielseite einfügen, wird ein Benutzer zu jeder URL zurückgeschickt, von der er vor der Landung auf Ihrer stammt.

dgrant069
quelle
3

Lösung für die Zurück / Vorwärts-Schaltfläche des Browsers
Ich habe das gleiche Problem festgestellt und es mit dem popstate eventObjekt aus dem $ -Fenster und gelöst ui-router's $state object. Jedes Mal, wenn sich der aktive Verlaufseintrag ändert, wird ein Popstate-Ereignis an das Fenster gesendet.
Die $stateChangeSuccessund $locationChangeSuccessEreignisse werden beim Klicken auf die Schaltfläche des Browsers nicht ausgelöst, obwohl die Adressleiste den neuen Speicherort anzeigt.
Also, vorausgesetzt , Sie von den Zuständen gesteuert haben , mainum folderzu mainwieder, wenn sie getroffen werden backim Browser, sollten Sie zurück zu dem sein folderWeg. Der Pfad wird aktualisiert, aber in der Ansicht wird nicht angezeigt, worauf Sie sich befinden main. Versuche dies:

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

Die Zurück / Vorwärts-Tasten sollten danach reibungslos funktionieren.

Hinweis: Überprüfen Sie die Browserkompatibilität für window.onpopstate (), um sicherzugehen

jfab fab
quelle
3

Kann mit einer einfachen Direktive "Go-Back-History" gelöst werden. Diese schließt auch das Fenster, falls keine Vorgeschichte vorhanden ist.

Verwendung der Richtlinie

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

Erklärung der Winkelrichtlinie

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

Hinweis: Arbeiten mit UI-Router oder nicht.

hthetiot
quelle
0

Die Schaltfläche "Zurück" funktionierte auch bei mir nicht, aber ich stellte fest, dass das Problem darin bestand, dass ich htmlInhalt auf meiner Hauptseite im ui-viewElement hatte.

dh

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

Also habe ich den Inhalt in eine neue .htmlDatei verschoben und ihn als Vorlage in der .jsDatei mit den Routen markiert .

dh

   .state("parent.mystuff", {
        url: "/mystuff",
        controller: 'myStuffCtrl',
        templateUrl: "myStuff.html"
    })
CodyBugstein
quelle
-1

history.back()und in den vorherigen Zustand wechseln oft nicht den gewünschten Effekt. Wenn Sie beispielsweise ein Formular mit Registerkarten haben und jede Registerkarte einen eigenen Status hat, wird die zuvor ausgewählte Registerkarte umgeschaltet und nicht vom Formular zurückgegeben. Bei verschachtelten Zuständen müssen Sie normalerweise über die übergeordneten Zustände nachdenken, die Sie zurücksetzen möchten.

Diese Richtlinie löst das Problem

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

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

Es kehrt zu dem Zustand zurück, der aktiv war, bevor die Schaltfläche angezeigt wurde. OptionaldefaultState ist der Statusname, der verwendet wird, wenn kein vorheriger Status im Speicher vorhanden ist. Außerdem wird die Bildlaufposition wiederhergestellt

Code

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
quelle