Storing and returning to a previous state


#1

What’s the best way to capture the current state of an Ember app, that is represented by the URL, and is compatible with transitionTo?

My first thought was to store currentRouteName along with the model using modelFor(currentRouteName). e.g.

// in some-route.js
var currentRouteName = this.controllerFor('application').get('currentRouteName')
var model = this.modelFor(currentRouteName)
…
// somewhere else
this.transitionTo(currentRouteName, model)

The downsides of this approach are that it will not work with nested resources (modelFor(routeName) only returns a single model), and it will require some additional work for use with query params.

Another approach might be to simply grab the path from window.location:

var currentPath = location.href.slice(location.href.indexOf(location.pathname))
…
// somewhere else
this.transitionTo(currentPath)

The downsides are that it may not work if the Ember app does not reside at the root url, and that it may run model hooks unnecessarily.

It feels like there should be a better way to do this. Am I missing something?


#2

Or when running tests.


#3

Just discovered that routes have access to the router, so my current thinking is to go with a initializer which makes each route store its path on its controller on willTransition. On subsequent transitions, the route can then decide whether to transition to its previous path:

// app/initializers/route.js

import Ember from 'ember';

var hasRun = false;

export default {
  name: 'route',

  initialize: function () {
    // Prevents needless re-initialization
    if (hasRun) { return; }

    Ember.Route.reopen({
      // Prefix with underscore. I'm not entirely sure why!
      // see: https://github.com/emberjs/ember.js/issues/5394#issuecomment-52383989
      _actions: {
        // Stores the current path
        didTransition: function () {
          Ember.run.next(this, function () {
            this.set('path', this.get('router.url'));
          });
          return true;
        },

        // Sets the previous path on the controller.
        // Allows routes to return to a previous state.
        willTransition: function () {
          this.get('controller').set('previousPath', this.get('path'));
          return true;
        }
      }
    });
  }
};

#4

Maybe I don’t understand what you wanted to do but, why don’t simply use the window.history.back() method?


#5

It’s not a case of going back to the previous state, but storing the the current state and being able to return to it at any point in the apps lifecycle.


#6

I found that setting the previousPath in the application controller rather than the current controller made easier accessible to the post-transition controller. Great solution!


#7

Here another possible implementation for Router.transitionToPrevious. I haven’t still found a better way to manage it.

var AppRouter = Router.extend({
  previousHandlers: null,
  currentHandlers: null,

  transitionChanged: function() {
    var previousHandlers = this.router.oldState.handlerInfos;
    this.set('previousHandlers', previousHandlers);

    var currentHandlers = this.router.currentHandlerInfos;
    this.set('currentHandlers', currentHandlers);
  }.on('didTransition'),

  intermediateTransitionToPrevious: function() {

    var handlers = this.get('currentHandlers');
    if (!handlers || !handlers.length) { return; }

    var handler = handlers[handlers.length - 1];

    var args = _getModels(handlers);
    args.unshift(handler.name);
    //handlerName, model1, model2....
    return this.intermediateTransitionTo.apply(this, args);

  },

  transitionToPrevious: function(errorCallback) {

    var handlers = this.get('previousHandlers');
    if (!handlers || !handlers.length) { return; }

    var handler = handlers[handlers.length - 1];
    if (handler.name === 'loading') {
      if (errorCallback) { return errorCallback(); }
      return;
    }
    var args = _getModels(handlers);
    args.unshift(handler.name);
    //handlerName, model1, model2....
    return this.transitionTo.apply(this, args);

  }

});

AppRouter.map(Routes);

var _getModels = function(handlers) {

  var result = [],
      length,
      context,
      params,
      handler;

  for (var i = 0; i < handlers.length; i++) {
    handler = handlers[i];
    params = handler.params;
    length = Object.keys(params).length;
    context = handler.context;

    if (length && context) {
      //is isNew and 1 parameter, this could be the
      //route `new` pattern
      if (context.get('isNew') && length === 1) {
        result.push(params[Object.keys(params)[0]]);
      } else {
        result.push(handler.context);
      }
    }
  }
  return result;
};