Получение имени состояния в своем лотке `onEnter`

Я создаю приложение, в котором я хочу переключить свойство в службе, как только пользователь войдет и покинет маршрут. Для этого мне нужно знать о имени состояния в onEnter и onExit. Это относительно легко для onExit hook, так как я могу просто ввести службу $state и прочитать имя текущего состояния. Но поскольку текущее состояние еще не установлено, когда вызван крюк onEnter, нет способа узнать, к какому состоянию мы переходим.

Мне все еще нужно иметь прекрасный контроль над другими частями штата, поэтому я бы предпочел не иметь никаких циклов. Я ищу способ передать функцию onEnter в состояние, сохраняя при этом имя состояния внутри самой функции.

Вот код, который я написал:

function onEnter($state, Steps) {
  var stateName = $state.current.name; // Not possible. The current state has not been set yet! 
  var step = Steps.getByStateName(stateName);

  step.activate();
}

function onExit($state, Steps) {
  var stateName = $state.current.name; // No problem. We know about the state. 
  var step = Steps.getByStateName(stateName);

  step.deactivate();
}

$stateProvider
  .state('step1', {
    url: '/step1',
    templateUrl: 'templates/step1.html',
    controller: 'StepOneController',
    onEnter: onEnter,
    onExit: onExit
  });

Мое решение, которое я использую сейчас, это использовать factory для создания контекста для функции onEnter, переданной в состояние. Это далеко не идеально, потому что мне все равно нужно передать ему название состояния.

Вот пример упомянутого обходного пути:

function onEnterFactory(stateName) {
  return function onEnter(Steps) {
    var step = Steps.getByStateName(stateName);

    step.activate();
  }
}

$stateProvider
  .state('step1', {
    url: '/step1',
    templateUrl: 'templates/step1.html',
    controller: 'StepOneController',
    onEnter: onEnterFactory('step1')
  });

Ответы

Ответ 1

Используйте этот в onEnter onExit hooks. onEnter вызывается следующей командой:

$injector.invoke(entering.self.onEnter, entering.self, entering.locals.globals);

Второй параметр $injector.invoke - это значение этого для вызываемой функции. Поэтому ваш код должен выглядеть следующим образом:

function onEnter(Steps) {
  var stateName = this.name; 
  var step = Steps.getByStateName(stateName);

  step.activate();
}

function onExit(Steps) {
  var stateName = this.name;
  var step = Steps.getByStateName(stateName);

  step.deactivate();
}

$stateProvider
  .state('step1', {
    url: '/step1',
    templateUrl: 'templates/step1.html',
    controller: 'StepOneController',
    onEnter: onEnter,
    onExit: onExit
  });

Ниже приведен рабочий пример доступа к имени состояния в onEnter и onExit:

angular.module('myApp', ['ui.router'])

.config(function($stateProvider) {
  function onEnter() {
    console.log('Entering state', this.name);
  }

  function onExit() {
    console.log('Exiting state', this.name);
  }

  $stateProvider.state('state-1', {
    url: '/state-1',
    template: '<p>State 1</p>',
    onEnter: onEnter,
    onExit: onExit
  }).state('state-2', {
    url: '/state-2',
    template: '<p>State 2</p>',
    onEnter: onEnter,
    onExit: onExit
  });
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.6/angular.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.3.1/angular-ui-router.js"></script>

<div ng-app="myApp">
  <nav>
    <a ui-sref="state-1">State 1</a>
    <a ui-sref="state-2">State 2</a>
  </nav>

  <div ui-view></div>
</div>

Ответ 2

В одном из моих проектов мы использовали что-то вроде этого

app.run(function($rootScope, $state, $location) {
    $rootScope.$on('$stateChangeSuccess', function(event, toState, toParams,
    fromState) {
    $state.previous = fromState;
  });

чтобы запомнить предыдущее состояние. Но вы могли бы также запомнить новое состояние и хранить информацию где-то.

Ответ 3

Вы уже знаете, в каком состоянии оно будет, потому что вы определяете его в .state('statename',. Чтобы не писать одно и то же имя дважды, вы можете заранее определить переменные состояния:

var steps = ["step1", "step2", "step3"]; // or, something like Steps.getSteps()

$stateProvider
    .state(steps[0], {
        url: '/step1',
        templateUrl: 'templates/step1.html',
        controller: 'StepOneController',
        onEnter: function(Steps) {
            var stateName = steps[0];
            var step = Steps.getByStateName(stateName);

            step.activate();
        },
        onExit: function($state, Steps) {
            var stateName = $state.current.name; // No problem. We know about the state. 
            var step = Steps.getByStateName(stateName);
            step.deactivate();
        }
    });

Вы можете сделать так динамично:

var steps = [
    { name: "step1", url: "/step1" },
    { name: "step2", url: "/step2" },
    { name: "step3", url: "/step3" }
]; // Or something like Steps.getSteps();

for (var i = 0; i < states.length; i++) {
    var state = steps[i];
    $stateProvider.state(state.name,
        url: state.url,
        templateUrl: "templates/" + state.name + ".html",
        onEnter: function(Steps) {
            var step = Steps.getByStateName(state.name);
            step.activate();

            // or just: state.activate();
        },
        onExit: function($state, Steps) {
            var stateName = $state.current.name; // No problem. We know about the state. 
            var step = Steps.getByStateName(stateName);

            step.deactivate();
        }
}

Ответ 4

Вы можете немного расширить свое решение factory и сделать его более гибким.

Может быть, есть provider, который реагирует на изменения состояния.
Затем вы можете просто ввести этот поставщик/услугу в функцию onEnter или где-нибудь, когда вам это может понадобиться.

Связанный plunker здесь http://plnkr.co/edit/6Ri2hE

angular.module('app', ['ui.router'])
  .provider('myState', function myStateProvider() {
    var state;

    this.onEnter = function() {
      console.log('provider.onEnter', state);
    };

    this.$get = function($rootScope) {
      var myState = {};

      myState.initialize = function() {
        $rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams) {
          state = toState;
        });
      };

      myState.getState = function() {
        return state;
      };

      return myState;      
    };
  }) 

  .config(function($stateProvider, myStateProvider) {
    $stateProvider
      .state('step1', {
        url: '/step1',
        template: '<div>step1 template</div>',
        controller: function() {},
        onEnter: myStateProvider.onEnter  // usage example
      })
      .state('step2', {
        url: '/step2',
        template: '<div>step2 template</div>',
        controller: function() {},
        onEnter: function(myState) {     // other usage example 
          console.log('state.onEnter', myState.getState());
        }
      });
  })

  .run(function(myState) {
    myState.initialize();
  });

<a ui-sref="step1">state:step1</a>
<a ui-sref="step2">state:step2</a>
<div ui-view></div>

Это будет console.log() следующее, если ссылки будут последовательно нажиматься.

imgur

Ответ 5

Добавить свойство 'name', обозначающее состояние:

$stateProvider
  .state('step1', {
    name: 'step1' // <- property naming the state
    url: '/step1',
    templateUrl: 'templates/step1.html',
    controller: 'StepOneController',
    onEnter: onEnter
  });

Тогда имя маршрута доступно this.name в обратном вызове onEnter:

function onEnter() {
  var stateName = this.name; // <- Retrieve the state name from its configuration
  var step = Steps.getByStateName(stateName);
  step.activate();
}

Чтобы не записывать одно и то же имя состояния дважды, вы можете начать с определения состояний в отдельном объекте и обогащения состояний их именем перед добавлением их в $stateProvider:

var routes = {
  "step1": {
    url: '/step1',
    templateUrl: 'templates/step1.html',
    controller: 'StepOneController',
    onEnter: onEnter
  }
};

for(var routeName in routes) {
      var route = routes[routeName];
      // Enrich route with its name before feeding 
      // it to the $stateProvider
      route.name = routeName;
      $stateProvider.state(route.name, route);
}

Ответ 6

Возможно, вы можете использовать решение,

$stateProvider
  .state('step1', {
   url: '/step1',
   templateUrl: 'templates/step1.html',
   controller: 'StepOneController',
   resolve: {
       onenter : function( Steps ) {
          // use this.self.name to access state name
          var step = Steps.getByStateName(this.self.name);
          step.activate();
       }
   } 
} );

Если приведенный выше подход this кажется нечистым, возможно, можно использовать декоратор для заполнения текущего состояния.

angular.config(function($provide) {
  $provide.decorator('$state', function($delegate, $rootScope) {
    $rootScope.$on('$stateChangeStart', function(event, state) {
      $delegate.next = state;
    });
    return $delegate;
  });
});

Состояние будет доступно в $state.next внутри функции разрешения.