Как заставить кнопку «Назад» работать с конечным автоматом пользовательского интерфейса AngularJS?

87

Я реализовал одностраничное приложение angularjs с использованием ui-router .

Первоначально я идентифицировал каждое состояние, используя отдельный URL-адрес, однако это было сделано для недружественных URL-адресов, упакованных с помощью GUID.

Итак, я определил свой сайт как гораздо более простой конечный автомат. Состояния не идентифицируются 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" - и я подозреваю, что в этом ваша проблема. Кнопка возврата довольно защищена от кода (иначе люди переопределят ее, что вызовет проблемы). Почему бы просто не позволить angular создавать лучшие URL-адреса, как это делает SO (хорошо, они могут не использовать angular, но их схема URL-адреса является иллюстративной)?
jcollum
Также может помочь этот вопрос: stackoverflow.com/questions/13499040/…
jcollum
Кроме того, поскольку вы не используете URL-адреса, не означает ли это, что для перехода в состояние Z люди должны будут щелкнуть через состояние X и Y, чтобы добраться до него? Это может раздражать.
jcollum
он будет идти с состоянием с разными параметрами? @jcollum
VijayVishnu
Понятия

Ответы:

78

Заметка

Все ответы, предлагающие использовать варианты $window.history.back(), упускают из виду важную часть вопроса: как восстановить состояние приложения до правильного местоположения состояния при переходе истории (назад / вперед / обновить). С этим в мыслях; пожалуйста, читайте дальше.


Да, можно переключать браузер назад / вперед (история) и обновлять во время работы чистого ui-routerконечного автомата, но это требует некоторых усилий.

Вам понадобится несколько компонентов:

  • Уникальные URL-адреса . Браузер включает кнопки «назад / вперед» только при изменении URL-адресов, поэтому вы должны создать уникальный URL-адрес для каждого посещенного состояния. Однако эти URL-адреса не должны содержать никакой информации о состоянии.

  • Сессионная служба . Каждый сгенерированный URL-адрес соотносится с определенным состоянием, поэтому вам нужен способ хранения ваших пар URL-состояние, чтобы вы могли получать информацию о состоянии после перезапуска приложения angular с помощью щелчков назад / вперед или обновления.

  • Государственная история . Простой словарь состояний ui-router с ключом по уникальному URL-адресу. Если вы можете положиться на HTML5, вы можете использовать API истории HTML5 , но если вы, как я, не можете, вы можете реализовать его самостоятельно в нескольких строках кода (см. Ниже).

  • Служба определения местоположения . Наконец, вам необходимо иметь возможность управлять как изменениями состояния ui-router, инициируемыми внутри вашего кода, так и обычными изменениями URL-адресов браузера, которые обычно запускаются пользователем, нажимая кнопки браузера или вводя данные в строку браузера. Все это может быть немного сложно, потому что легко запутаться в том, что что вызвало.

Вот моя реализация каждого из этих требований. Я объединил все в три службы:

Сессионная служба

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

Это оболочка для sessionStorageобъекта javascript . Я сократил это здесь для ясности. Полное объяснение этого вопроса см. В разделе: Как обрабатывать обновление страницы с помощью одностраничного приложения 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внешнем виде после хранения и извлечения исторических состояний введенных пользователя с помощью сгенерированных уникальных адресов. На самом деле это просто удобная оболочка для объекта словарного стиля.

Государственная служба геолокации

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Обрабатывает два события:

  • locationChange . Это вызывается при изменении местоположения браузеров, обычно при нажатии кнопки назад / вперед / обновления, при первом запуске приложения или при вводе пользователем URL-адреса. Если состояние для текущего location.url существует в, StateHistoryServiceто оно используется для восстановления состояния через ui-router $state.go.

  • stateChange . Это вызывается, когда вы перемещаете состояние внутри. Имя и параметры текущего состояния хранятся в StateHistoryServiceсгенерированном URL-адресе с ключом. Этот сгенерированный URL-адрес может быть любым, он может идентифицировать или не определять состояние в удобочитаемой форме. В моем случае я использую параметр состояния плюс случайно сгенерированную последовательность цифр, полученных из guid (фрагмент генератора guid см. В Foot). Сгенерированный URL-адрес отображается в строке браузера и, что особенно важно, добавляется во внутренний стек истории браузера с помощью @location.url url. Он добавляет URL-адрес в стек истории браузера, который включает кнопки вперед / назад.

Большая проблема с этой техникой, что вызов @location.url urlв stateChangeметоде вызовет $locationChangeSuccessсобытие и так вызовите locationChangeметод. В равной степени вызов @state.gofrom locationChangeвызовет $stateChangeSuccessсобытие, а значит, и stateChangeметод. Это очень сбивает с толку и бесконечно портит историю браузера.

Решение очень простое. Вы можете видеть, что preventCallмассив используется как стек ( popи push). Каждый раз, когда вызывается один из методов, он предотвращает одноразовый вызов другого метода. Этот метод не мешает правильному запуску событий $ и сохраняет все в порядке.

Теперь все, что нам нужно сделать, это вызвать HistoryServiceметоды в подходящее время в жизненном цикле перехода между состояниями. Это делается в .runметоде AngularJS Apps , например:

Angular app.run

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

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

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

Создать гид

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

После всего этого кнопки вперед / назад и кнопка обновления работают должным образом.

биофрактал
источник
1
в приведенном выше примере SessionService, я думаю, что метод доступа должен использовать @setStorageand, @getStorageа не `@ get / setSession '.
Питер Уитфилд
3
Какой язык используется в этом примере. Не кажется угловатым, с которым я знаком.
deepak
Язык - javascript, синтаксис - coffeescript.
биофрактал
1
@jlguenego У вас есть функциональные кнопки просмотра истории / боуфера и URL-адреса, которые вы можете добавить в закладки.
Торстен Бартель
3
@jlguenego - ответы, предлагающие использовать варианты $window.history.back(), упускают важную часть вопроса. Дело в том, чтобы восстановить приложения состояния для правильного государственного места , как история скачки (назад / вперед / обновления). Обычно это достигается путем предоставления данных о состоянии через URI. Был задан вопрос, как переключаться между местоположениями состояний без (явных) данных состояния URI. Учитывая это ограничение, недостаточно просто реплицировать кнопку «Назад», потому что при этом используются данные состояния URI для восстановления местоположения состояния.
биофрактал
46
app.run(['$window', '$rootScope', 
function ($window ,  $rootScope) {
  $rootScope.goBack = function(){
    $window.history.back();
  }
}]);

<a href="#" ng-click="goBack()">Back</a>
Гийом Массе
источник
2
люблю это! ... лол ... для ясности $window.history.backволшебство не $rootScopeтворится ... так что возврат можно привязать к области действия вашей навигационной панели, если хотите.
Бенджамин Конант,
@BenjaminConant Для людей, которые не знают, как это реализовать, вы просто вставляете $window.history.back();функцию, которая будет вызвана ng-click.
chakeda
правильный rootScope предназначен только для того, чтобы функция была доступна в любом шаблоне
Гийом Массе
Куриный ужин.
Cody
23

После тестирования различных предложений я обнаружил, что самый простой способ зачастую оказывается лучшим.

Если вы используете угловой ui-router и вам нужна кнопка для возврата, лучше всего это:

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

или

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

// Предупреждение, не устанавливайте href, иначе путь будет нарушен.

Объяснение: Предположим, стандартное приложение для управления. Поиск объекта -> Просмотр объекта -> Редактировать объект

Использование угловых решений Из этого состояния:

Поиск -> Просмотр -> Редактировать

Кому:

Поиск -> Просмотр

Что ж, это то, что мы хотели, за исключением того, что если теперь вы нажмете кнопку возврата в браузере, вы снова будете там:

Поиск -> Просмотр -> Редактировать

И это не логично

Однако с помощью простого решения

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

из :

Поиск -> Просмотр -> Редактировать

после нажатия на кнопку:

Поиск -> Просмотр

после нажатия кнопки возврата в браузере:

Поиск

Последовательность соблюдается. :-)

bArraxas
источник
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-адрес, с которого он пришел до перехода на ваш.

dgrant069
источник
3

Решение кнопки браузера «назад / вперед».
Я столкнулся с той же проблемой и решил ее, используя popstate eventобъект from $ window и ui-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 (), чтобы быть уверенным

jfab fab
источник
3

Может быть решена с помощью простой директивы «go-back-history», она также закрывает окно в случае отсутствия предыдущей истории.

Директива использования

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

Объявление директивы 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();  
                }
            });
        }
    };
}])

Примечание: работает с ui-router или нет.

hthetiot
источник
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"
    })
CodyBugstein
источник
-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;
                });
        }]);
Sel
источник