Як змусити кнопку Назад працювати для роботи з автоматом користувальницького інтерфейсу AngularJS?


87

Я реалізував односторінковий додаток angularjs за допомогою ui-маршрутизатора .

Спочатку я ідентифікував кожен штат за допомогою окремої 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-адреси, кнопка "Назад" не працює.

Як зберегти акуратний автомат користувальницького інтерфейсу, але ввімкнути функцію кнопки повернення?


1
"штати не ідентифікуються за URL-адресами" - і є ваша проблема, я підозрюю. Кнопка "Назад" досить захищена від коду (інакше люди перевизначать його, спричиняючи проблеми). Чому б просто не дозволити angular робити кращі URL-адреси, як це робить SO (ОК, можливо, вони не використовують angular, але їх URL-схема є наочною)?
jcollum

Крім того , це питання може допомогти: stackoverflow.com/questions/13499040 / ...
jcollum

Крім того, оскільки ви не використовуєте URL-адреси, це не означає, що, щоб дістатися до стану Z людям доведеться натискати через стан X та Y, щоб дістатися до нього? Це може дратувати.
jcollum

це піде з державою з різними параметрами? @jcollum
VijayVishnu

Я не маю уявлення, це було надто давно
jcollum

Відповіді:


78

Примітка

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


Так, можна отримати браузер назад / вперед (історія) та оновити під час запуску чистого ui-routerавтомата, але для цього потрібно трохи зробити.

Вам потрібно кілька компонентів:

  • Унікальні URL-адреси . Браузер дозволяє кнопки перемотування назад / вперед лише при зміні URL-адрес, тому вам потрібно створити унікальну URL-адресу для кожного відвіданого стану. Ці URL-адреси не повинні містити жодної інформації про стан.

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

  • Державна історія . Простий словник станів інтерфейсу маршрутизатора, введений унікальною URL-адресою. Якщо ви можете покластися на HTML5, ви можете використовувати API історії HTML5 , але якщо, як і я, ви не можете, ви можете реалізувати його самостійно в декількох рядках коду (див. Нижче).

  • Служба локації . Нарешті, вам потрібно вміти керувати як змінами стану користувальницького інтерфейсу, що ініціюються внутрішнім вашим кодом, так і звичайними змінами 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 Single Page?

Державна служба історії

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-маршрутизатора $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). Кожного разу, коли один із методів викликається, це заважає іншому методу називатися одноразовим. Цей прийом не заважає правильному запуску $-подій і тримає все прямо.

Тепер все, що нам потрібно зробити, - це викликати HistoryServiceметоди у відповідний час життєвого циклу переходу держави. Це робиться за допомогою .runметоду AngularJS Apps , наприклад:

Кутовий додаток.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 вище, я думаю , що збруя: метод слід використовувати @setStorageі @getStorageзамість `@ прибуде / 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 - лише зробити функцію доступною в будь-якому шаблоні
Гійом Массе

Куряча вечеря.
Коді,

23

Перевіривши різні пропозиції, я виявив, що найпростіший спосіб часто є найкращим.

Якщо ви використовуєте кутовий користувальницький маршрутизатор і вам потрібна кнопка для повернення, найкраще це:

<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не запускаються при натисканні кнопки браузера, навіть якщо в адресному рядку вказано нове місце.
Таким чином, припускаючи , що ви переходите з станів 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

Це можна вирішити за допомогою простої директиви "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();  
                }
            });
        }
    };
}])

Примітка: Робота з користувальницьким інтерфейсом або ні.


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;
                });
        }]);
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.