sebpiq / backbone.statemachine

Simple finite-state machine for Backbone. View states made easy. Synchronizing your application's parts with events made easy.
MIT License
177 stars 16 forks source link

Enumerating states #19

Open afeld opened 11 years ago

afeld commented 11 years ago

Thinking on using this to wrap a Router (#16), I'm considering how dynamic routes would work. For example, are /posts/1 and /posts/2 different states? You don't know all the possible states (in this case, all available the post IDs) up front, and even if you did, you wouldn't want to enumerate them. How would a transition between posts be defined?

This question applies to the StateMachine in general but becomes really apparent when dealing with routes.

sebpiq commented 11 years ago

Well ... I'd say, the obvious solution to this is defining one state for "postDetail" (with transition to itself, when navigating from /posts/1 to /posts/2), one for "postList", ... and simply taking the postId as a parameter. e.g. :

routes = Backbone.StatefulRouter({
    transitions: {
        postList: {
            detail: {enterState: 'postDetail', callbacks: ['showDetail']},
        },
        postDetail: {
            detail: {enterState: 'postDetail', callbacks: ['showDetail']},
            list: {enterState: 'postList', callbacks: ['showList']},
        }
    },

    showDetail: function(postId) {
        // do stuff
    }
});

Or something like that ... what do you think ?

afeld commented 11 years ago

Ah, so you can define transitions to the same state? Theoretically this works in the StateMachine as-is?

showDetail: function(postId){
  console.log('visiting ' + postId);
}

...

routes.trigger('detail', 1); // "visiting 1"
routes.currentState; // "showDetail"
routes.trigger('detail', 2); // "visiting 2"

Don't think I saw an example in the tests, so I wasn't sure. Guess I could always write one...

afeld commented 11 years ago

The part in the README that says

event 'hide' while in state 'hidden' -> no transition

made it seem like it was not possible.

sebpiq commented 11 years ago

Theres no difference in a transition between 2 different states and one state to itself.

event 'hide' while in state 'hidden' -> no transition

The documentation says this, because in that case theres no transition defined. It doesnt say that you cant define it ! Ill make a note for precising this.

afeld commented 11 years ago

Gotcha. Not sure if it's a huge deal, but that means

showDetail: function(postId){
  console.log('incrementing view count');
}

...

routes.trigger('detail', 1); // "incrementing view count"
routes.trigger('detail', 1); // "incrementing view count" - but you're already there!
sebpiq commented 11 years ago

In the router, one workaround is to memoize the route call's args (should be easy since args are strings) :

    // Overriding Router's `route` method
    route: function(route, name, callback) {
      if (!_.isRegExp(route)) route = this._routeToRegExp(route);
      if (!callback) callback = this[name];
      var wrappedCb = function() {
        var key = this._argsToKey.call(this, arguments);
        if (this.key !== key) {
          this.key = key;
          callback.apply(this, arguments);
        } else {
          return; // We are already on that route with same arguments
        }
      }
      Backbone.Router.prototype.route(route, name, wrappedCb)
    },

    _argsToKey: function(args) { /* Just turn args into one string */ }

Of course one could also do :

showDetail: function(postId){
  if (postId !== this.currentPostId) {
    console.log('incrementing view count');
  }
}
afeld commented 11 years ago

Sure - just considering if that should be covered by the StateMachine in a more generalized way.

sebpiq commented 11 years ago

Hmm ... it might make sense. Though that sounds a bit complicated in practice, and I can't really think of a nice way how to declare this for any state. One solution could be to make a state option, something like :

states: {
    postDetail: {
        subStates: : function(postId) { return 'postDetail:' + postId; }
    }
},

transitions: {
    postDetail: {
        showDetail: {
            enterState: postDetail
        }
    }
}

But I am not sure how obvious this is ... probably not very obvious. Any better idea ?

afeld commented 11 years ago

Well, when you declare routes in Backbone (or Rails) you can make something static ('/posts') or something with a known variable ('/posts/:id'). What if the state definitions supported a similar syntax, where it could handle variations of a particular state?

var ImageViewer = _.extend({}, Backbone.StateMachine, Backbone.Events, {
  states: {
    'imageList': {},
    'imageDetail/:id': {} // <-- special syntax, maybe in the transition definition too?
  },

  transitions: {
    imageDetail: { enterState: 'imageDetail', callbacks: ['doCrossFade'] },
  },

  // ...
});

var viewer = ImageViewer();
viewer.trigger('imageDetail/1'); // cross-fades
viewer.trigger('imageDetail/2'); // cross-fades
viewer.trigger('imageDetail/2'); // (no change)
sebpiq commented 11 years ago

hmm ... this would make the API much more complicated (you forgot the events in your transitions definiton). How would you then make a good API for mapping variables with event, arg and states ?

Something like this maybe ?

var ImageViewer = _.extend({}, Backbone.StateMachine, Backbone.Events, {
  states: {
    'imageList': {},
    'imageDetail/:id': {}
  },

  transitions: {
    imageDetail: { 
      'show/:id': { enterState: 'imageDetail/:id', callbacks: ['doCrossFade'] }
    }
  },

  doCrossFade: function(id) { /* cross-fade here */ }

  // ...
});

var viewer = ImageViewer();
viewer.toState('imageDetail/0');
viewer.trigger('show/1'); // cross-fades, state before : 'imageDetail/0', after : 'imageDetail/1'
viewer.trigger('show/2'); // cross-fades, state before : 'imageDetail/1', after : 'imageDetail/2'
viewer.trigger('show/2'); // (no change)

Also, I had in mind that events could be regexp (#10) ... and I am not sure how all this can work together !

On the other hand, I like how this would solve a problem that I have very often : "tabs", picture viewers, etc ...

Maybe we can just try implementing this API, and then let's see how it feels. I am not sure when I will have time to do this, but probably next week ; or if you want to try your hand at it, just go ahead !