Skip to content

Instantly share code, notes, and snippets.

@nickfargo
Last active August 29, 2015 14:05
Show Gist options
  • Select an option

  • Save nickfargo/ad76adbd1983eb40c90f to your computer and use it in GitHub Desktop.

Select an option

Save nickfargo/ad76adbd1983eb40c90f to your computer and use it in GitHub Desktop.
Re: Two-way binding between the URL and a state.js machine.

In response to https://gist.github.com/AdrianRossouw/0d6d49702a64e22b7541

application.js

Backbone = require 'backbone'
state = require 'state'
Router = require './router'


module.exports = class Application
  constructor: ->
    @router = null
    state this, require './state-machine'

state-machine.js

{ pathToUrl, urlToPath } = require './util'


module.exports = state

  methods:
    getUrl: -> pathToUrl @state().path()
    setUrl: -> @router.navigate @getUrl(), trigger: no
    urlState: (url) -> @state().go urlToPath url

  events:
    noSuchMethod: (name) ->
      # Just for fun let’s be strict about invalid method calls.
      throw new Error "No such method '#{name}' in state '#{@state()}'"

Instead of the awkward bit in router.js where we rig up listenForArrive to each state, I’ll just delegate the arrive event once at the root.

The event delegation feature is still only on master … I’ve not npm version’d in quite awhile 😐 … and I forget what I was waiting on … probably oughtta just bump it as-is.

    '***:arrive': -> do @setUrl; do @show

Here I have a special, sort-of-hidden exit-only initial state in which to contain the start method. This makes start impossible to call more than once by definition.

  _incipient: state 'initial',
    admit: no

    start: ->

Instead of '$state' I’m keying this (instance of Application) to 'app'.

      @router = new Router app: this

Instead of an explicit this.show() call here, I do an initial this.state().go('...') call. This leads to the delegated arrive handler above, which performs the initial this.show() call for us.

      @state().go 'home' # or wherever the “real” start state should be

      do Backbone.History.start
      return

Do we want to accommodate the / route? Then we should probably leave the root state concrete (i.e. as-is, not abstract) and implement a show method for the root.

  show: -> # render

Or do we want the root to redirect to home? If so then we should make the root state abstract, and make the home state default.

  home: state 'default',
    show: -> # render

  about: state
    show: ->

    team: state
      show: ->

  # etc.

router.js

module.exports = class Router extends Backbone.Router

Awkwardness begone! … all that remains of initialize is the Application instance.

  initialize: (opts) ->
    @app = opts.app

Maybe there’s a cleaner way to deal with this routing table stuff too … hmm …

  routes:
    '*state': 'goState'

  goState: (stateName) -> @app.urlState stateName

… might it help to override Backbone.Router.prototype.route?

I wonder also how evil it'd be to pull a state( Backbone.Router.prototype, {} ) or similar … since, really, the app’s “navigational state machine” is the router itself.

@AdrianRossouw

Copy link
Copy Markdown

Yeah, this is cool!

Let's take a quick look at what features we needed for this.

  1. the ability to attach to any object
  2. separate definition of state machine
  3. you only used substate-superstate relations
  4. initial, abstract and concrete state types. (default too?)
  5. an 'exit-only state', which is something I started doing all the time too btw.
  6. guards, although only to buffer the exit-only state.
  7. events defined in SM (although that was a design choice)
  8. event delegation, which is something i've needed half-a-dozen times this year... =)

@nickfargo

Copy link
Copy Markdown
Author

Neat … the inclusion set is useful. I’m less clear on what this suggests for an exclusion set, however.

Reflecting a bit on your numbered points above:

(1,2) — The distinction between “static definition” vs “live entities” — i.e., StateExpression vs State; i.e., state.define({}) -> StateExpression vs state.implement(owner, StateExpression) -> State — is an important one.

  • Incidentally, I note the React folks recently appreciated this very distinction in their “virtual DOM” implementation (there they went with the perfectly suitable term Descriptors).

(3) — Here we did only superstate–substate. Does this suggest anything about the other relations?

  • This exercise dealt with singletons Application, or possibly Router, so this doesn’t seem to provide insight as to the value of protostate–epistate.
  • In theory, the parastate/“mixin” concept is one whose utility becomes clear only once the “machine” grows appreciably large, to where reliance on single-parent inheritance alone begins to hurt. (Cue the Java-esque ten-deep-hierarchy boogeyman.)
    • Does that “appreciably large” quality suggest an opportunity to relegate parastates to a plugin or similar? Maybe.
    • Cognitively, I see a clean separation as one of parastates’ best features: a user who has no use for parastates can happily ignore (or be completely unaware of) them.
    • The only practical benefit I see to exclusion here is the byte-payload reduction (almost certainly <1kB gz). Maybe this is significant enough to pursue, though I’d guess that to be a hard case to make, and that there are greater gains to be made elsewhere in this regard.

(4) — Again the question appears to be, what does this suggest about attributes other than {initial abstract concrete default}? … As the utility of final is clear, and conclusive is simple enough, that seems to implicate the mutability attributes {mutable finite immutable} as candidates for exclusion:

  • Certainly, the benefit of these are more theoretical, experimental, and perhaps cognitively heavy.
  • Some of the theory behind (im)mutability attributes for the State indirection to “strictly immutable states-as-values” is, in my view, sound, but not yet fully fleshed out.
    • Here we get into the impetus for deploying immutable data structures everywhere, etc., and the attendant benefits of easy history/versioning, concurrency, etc. (you know — aka the stuff Hickey has already got licked).

(5,6,7) mostly explain themselves …

(8) — By the way, one of the later commits was to discontinue .gitgnore-ing the compiled js, so, to gain access to edge features — including event delegation — now (in lieu of awaiting a certain numbskull to type npm version), we could do

{
  "dependencies": {
    "state": "nickfargo/state"
  }
}

or thereabouts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment