AngularJS ui-routerステートマシンで[戻る]ボタンを使用するにはどうすればよいですか?


87

私はui-routerを使用してangularjsシングルページアプリケーションを実装しました。

元々は個別のURLを使用して各州を識別していましたが、これはGUIDがパックされた非友好的なURLになりました。

それで、私は自分のサイトをより単純なステートマシンとして定義しました。状態はURLでは識別されませんが、次のように、必要に応じて単に移行されます。

ネストされた状態を定義する

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'

定義済み状態への移行

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

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

このシステムは非常にうまく機能し、私はそのクリーンな構文が大好きです。ただし、URLを使用していないため、戻るボタンが機能しません。

きちんとしたui-routerステートマシンを維持しながら、戻るボタン機能を有効にするにはどうすればよいですか?


1
「状態はURLで識別されません」-そして私が疑っているあなたの問題があります。戻るボタンはコードからかなり保護されています(そうしないと、コードが上書きされて問題が発生します)。SOのように、angularだけでURLを改善するだけでなく(angularを使用していない可能性がありますが、URLスキーマは例示です)?
jcollum 2014年

また、この質問が役立つ場合があります:stackoverflow.com/questions/13499040/…–
jcollum

また、URLを使用していないので、状態Zに到達するために、状態Xと状態Yをクリックする必要があります。それは迷惑になるかもしれません。
jcollum 2014年

それは異なるパラメーターを持つ状態で行きますか?@jcollum
VijayVishnu

私には
わから

回答:


78

注意

のバリエーションを使用することを提案する回答$window.history.back()はすべて、質問の重要な部分を逃しています。履歴がジャンプするときに、アプリケーションの状態を正しい状態の場所に復元する方法(戻る/進む/更新)。それを念頭に置いて。読んでください。


はい、純粋なui-routerステートマシンを実行している間にブラウザを元に戻したり(履歴)更新したりすることは可能ですが、少し時間がかかります。

いくつかのコンポーネントが必要です。

  • 一意のURL。ブラウザは、URLを変更したときにのみ戻る/進むボタンを有効にするため、訪問した状態ごとに一意のURLを生成する必要があります。ただし、これらのURLには状態情報を含める必要はありません。

  • セッションサービス。生成された各URLは特定の状態に関連付けられるため、バック/フォワードまたはリフレッシュクリックによって角度付きアプリが再起動された後に状態情報を取得できるように、URLと状態のペアを保存する方法が必要です。

  • 州の歴史。一意のURLをキーにしたui-router状態の単純な辞書。HTML5に依存できる場合は、HTML5 History APIを使用できますが、私のようにできない場合は、数行のコードで自分で実装できます(下記を参照)。

  • 位置サービス。最後に、コードによって内部的にトリガーされるui-router状態の変更と、ユーザーがブラウザーボタンをクリックするかブラウザーバーに何かを入力することによって通常トリガーされる通常のブラウザーURL変更の両方を管理できる必要があります。何が何をトリガーしたかについて混乱するのは簡単なので、これはすべて少しトリッキーになります。

これらの要件のそれぞれの私の実装です。私はすべてを3つのサービスにバンドルしました:

セッションサービス

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

これは、javascript sessionStorageオブジェクトのラッパーです。ここでは、わかりやすくするために省略しています。これの完全な説明については、AngularJS単一ページアプリケーションでページの更新を処理する方法を参照してください。

州歴史サービス

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

StateHistoryService生成された、固有のURLをキー歴史的状態の記憶および検索後ルックス。これは実際には、ディクショナリスタイルのオブジェクトの単なる便利なラッパーです。

州の位置情報サービス

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

StateLocationService2つのイベントを処理します。

  • locationChange。これは、ブラウザの場所が変更されたとき、通常は戻る/進む/更新ボタンが押されたとき、アプリが最初に起動したとき、またはユーザーがURLを入力したときに呼び出されます。現在のlocation.urlの状態がに存在する場合は、StateHistoryServiceui-routerを介して状態を復元するために使用されます$state.go

  • stateChange。これは、内部で状態を移動するときに呼び出されます。現在の状態の名前とパラメータはStateHistoryService、生成されたURLをキーとして格納されます。この生成されたURLは、あなたが望むものにすることができます。人間が読める方法で状態を識別してもしなくてもかまいません。私の場合、私は状態パラメーターに加えて、GUIDから派生したランダムに生成された一連の数字を使用しています(GUIDジェネレーターのスニペットについては、脚を参照してください)。生成されたURLはブラウザバーに表示され、重要なことに、を使用してブラウザの内部履歴スタックに追加されます@location.url url。ブラウザーの履歴スタックにURLを追加して、進む/戻るボタンを有効にします。

この手法の大きな問題@location.url urlは、stateChangeメソッドを$locationChangeSuccess呼び出すとイベントがトリガーされ、メソッドが呼び出されるlocationChangeことです。同様に@state.gofrom locationChangeを呼び出すと、$stateChangeSuccessイベントがトリガーされ、stateChangeメソッドがトリガーされます。これは非常に混乱し、終わりのないブラウザの履歴を台無しにしてしまいます。

解決策は非常に簡単です。preventCall配列がスタックとして使用されていることがわかります(popおよびpush)。メソッドの1つが呼び出されるたびに、他のメソッドが1回だけ呼び出されるのを防ぎます。この手法は、$イベントの正しいトリガーを妨げず、すべてをまっすぐに保ちます。

あとHistoryServiceは、状態遷移のライフサイクルの適切なタイミングでメソッドを呼び出すだけです。これは、AngularJS Apps .runメソッドで次のように行われます。

Angular app.run

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

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

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

GUIDを生成する

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

これがすべて整ったら、進む/戻るボタンと更新ボタンはすべて期待どおりに機能します。


1
方法使用する必要があります。上記のSessionServiceの例では、私は、アクセサを考える@setStorage@getStorage、むしろ`@ GET / setSession」より。
Peter Whitfield

3
この例で使用されている言語。私が慣れ親しんでいるような角張っていないようです。
ディーパック、2016年

言語はjavascript、構文はcoffeescriptでした。
バイオフラクタル2016年

1
@jlguenego機能的なブラウザの履歴/クッパの前後のボタンとブックマークできるURLがあります。
Torsten Barthel

3
@jlguenego-のバリエーションを使用することを提案する回答$window.history.back()は、質問の重要な部分を逃しています。重要なのは、履歴がジャンプ(戻る/進む/更新)したときに、アプリケーションの状態を正しい状態の場所に復元することでした。これは通常、URIを介して状態データを提供することで実現されます。この質問では、(明示的な)URI状態データのない状態の場所間をジャンプする方法を尋ねました。この制約がある場合、「戻る」ボタンを複製するだけでは十分ではありません。これは、状態の場所を再確立するためにURI状態データに依存しているためです。
バイオフラクタル2016年

46
app.run(['$window', '$rootScope', 
function ($window ,  $rootScope) {
  $rootScope.goBack = function(){
    $window.history.back();
  }
}]);

<a href="#" ng-click="goBack()">Back</a>

2
これが大好き!... lol ...明確$window.history.backにするために魔法ではありません$rootScope...なので、必要に応じてnavbarディレクティブスコープに戻ることができます。
Benjamin Conant 2016年

@BenjaminConantこれを実装する方法を知らない人のために、あなたは単に$window.history.back();呼び出される関数に入れますng-click
chakeda

正しいrootScopeは、任意のテンプレートで関数にアクセスできるようにすることだけです
GuillaumeMassé'20

チキンディナー。
Cody

23

さまざまな提案をテストした後、最も簡単な方法がしばしば最善であることがわかりました。

角度のあるui-routerを使用していて、戻るためのボタンが必要な場合は、次のようになります。

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

または

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

//警告はhrefを設定しないでください。設定するとパスが壊れます。

説明:標準の管理アプリケーションを想定します。オブジェクトの検索->オブジェクトの表示->オブジェクトの編集

角度解の使用 この状態から:

検索->表示->編集

へ:

検索->表示

まあそれは私たちが望んでいたことですが、ブラウザの戻るボタンをクリックした場合は、再びそこに戻ります:

検索->表示->編集

そしてそれは論理的ではありません

ただし、シンプルなソリューションを使用して

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

から:

検索->表示->編集

ボタンをクリックした後:

検索->表示

ブラウザの戻るボタンをクリックした後:

探す

一貫性が尊重されます。:-)


7

最も単純な「戻る」ボタンを探している場合は、次のようなディレクティブを設定できます。

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

これはブラウザの履歴を使用しているため、これはかなりインテリジェントではない「戻る」ボタンです。ランディングページにそれを含めると、ユーザーをランディング前に取得したURLにユーザーを送り返します。


3

ブラウザーの戻る/進むボタンソリューション
同じ問題が発生popstate eventし、$ windowオブジェクトとを使用して解決しましたui-router's $state object。アクティブな履歴エントリが変更されるたびに、popstateイベントがウィンドウに送出されます。そしてイベントは、アドレスバーには新しい場所を示していても、ブラウザのボタンクリックで起動されません。 したがって、州から州へとナビゲートした場合、ブラウザーを押すとルートに戻るはずです。パスは更新されますが、ビューは更新されていません。これを試して:
$stateChangeSuccess$locationChangeSuccess
mainfoldermainbackfoldermain

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

その後、戻る/進むボタンはスムーズに機能します。

注:window.onpopstate()のブラウザの互換性を確認してください


3

単純なディレクティブ「go-back-history」を使用して解決できます。これは、以前の履歴がない場合にもウィンドウを閉じます。

ディレクティブの使用

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

角度指令宣言

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

注:ui-routerを使用するかどうか。


0

[戻る]ボタンも機能しませんでしたが、問題はhtml、メインページ内のui-view要素にコンテンツが含まれていることであることがわかりました。

すなわち

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

そのため、コンテンツを新しい.htmlファイルに移動.jsし、ルートを含むファイルでテンプレートとしてマークしました。

すなわち

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

-1

history.back()以前の状態に切り替えると、多くの場合、意図した効果が得られません。たとえば、タブのあるフォームがあり、各タブに独自の状態がある場合、これはフォームから戻るのではなく、直前のタブを切り替えただけです。ネストされた状態の場合、通常はロールバックする親状態の魔女について考える必要があります。

このディレクティブは問題を解決します

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

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

ボタンが表示されるにアクティブだった状態に戻ります。オプションdefaultStateは、メモリに以前の状態がないときに使用される状態名です。また、スクロール位置を復元します

コード

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;
                });
        }]);
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.