web-dev-qa-db-ja.com

AngularJS UIルーターステートマシンで[戻る]ボタンを機能させるにはどうすればよいですか?

i-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ルーターステートマシンを維持しながら、戻るボタンの機能を有効にするにはどうすればよいですか?

84
biofractal

注意

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


はい、純粋なui-router状態マシンを実行しながら、ブラウザを前後に移動(履歴)して更新することは可能ですが、少し時間がかかります。

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

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

  • Aセッションサービス。生成された各URLは特定の状態に関連付けられているため、angularアプリが戻る/進むまたは更新クリックによって再起動された後に状態情報を取得できるように、url-stateペアを保存する方法が必要です。

  • 状態履歴。一意の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シングルページアプリケーションでページの更新を処理する方法

State History Service

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をキーとする履歴状態の保存と取得を管理します。実際には、辞書スタイルのオブジェクトの単なる便利なラッパーです。

State Location Service

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

StateLocationServiceは2つのイベントを処理します。

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

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

この手法の大きな問題は、stateChangeメソッドで@location.url urlを呼び出すと$locationChangeSuccessイベントがトリガーされるため、locationChangeメソッドが呼び出されることです。同様に、locationChangeから@state.goを呼び出すと、$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()}"

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

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

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

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

angular ui-routerを使用し、最適な状態に戻るためにボタンが必要な場合は次のとおりです。

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

または

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

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

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

angularソリューションを使用この状態から:

検索->表示->編集

に:

検索->表示

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

検索->表示->編集

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

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

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

から:

検索->表示->編集

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

検索->表示

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

サーチ

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

23
bArraxas

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

    .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に戻ります。

7
dgrant069

ブラウザの戻る/進むボタンソリューション
同じ問題が発生し、$ windowオブジェクトのpopstate eventui-router's $state objectを使用して解決しました。アクティブな履歴エントリが変更されるたびに、popstateイベントがウィンドウに送出されます。
アドレスバーに新しい場所が示されていても、$stateChangeSuccessおよび$locationChangeSuccessイベントはブラウザーのボタンクリックでトリガーされません。
したがって、状態mainからfolderからmainに再度移動したと仮定すると、ブラウザーでbackをヒットすると、folderルートに戻るはずです。パスは更新されますが、ビューは更新されず、mainにあるものはすべて表示されます。これを試して:

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
jfab fab

単純なディレクティブ "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を使用して使用するかどうか。

3
hthetiot

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

つまり.

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

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

つまり.

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