Skip to content

Instantly share code, notes, and snippets.

@lukemorton
Last active December 16, 2015 13:48
Show Gist options
  • Save lukemorton/5444235 to your computer and use it in GitHub Desktop.
Save lukemorton/5444235 to your computer and use it in GitHub Desktop.
# A class for controlling history state
#
# The aim of this class is to provide ways of pushing and
# replacing state by providing an action alongside a state.
# This approach differs from routing based solutions that
# provide functionality to make and detect state changes then
# handle them elsewhere.
#
# $button = $('.button')
#
# button_state = new StateController
#
# button_state.sync_push '/green', ->
# $button.css('background-color', 'green')
#
# button_state.push '/waiting', (done) ->
# setTimeout(done, 4000)
#
# button_state.sync_replace '/red', ->
# $button.css('background-color', 'red')
#
class StateController
@uid = 0
# Set up a `StateController` instance with an optional
# object. Valid keys:
#
# - **auto**, automatically listen for `onpopstate` changes
# when instance created. Defaults to `true`.
# - **id**, a unique identifier for this `StateController`
# instance. Used for only responding to `onpopstate`
# events that relate to this controller. Defaults to an
# auto incremented number.
constructor: (s = {}) ->
@id = s.id or ++StateController.uid
@stack = []
@queue = []
@listening = no
@processing = no
@listen() unless s.auto is false
# Listen to window.onpopstate events specifically targeted
# at a `StateController` instance
listen: ->
return if @listening
@listening = yes
window.onpopstate = (event) =>
# We return here if no event.state since we do not want
# to trigger any callbacks on page load. Everyone but
# Firefox triggers on page load, which means things are
# inconsistent, hence we don't bother.
return unless event.state?
{id, stack_id} = event.state
return unless id is @id
queue_handler(@, @stack[stack_id])
process_queue = (sc) ->
return if sc.processing
sc.processing = yes
unless handler = sc.queue.shift()
sc.processing = no
return
handler ->
sc.processing = no
process_queue(sc)
queue_handler = (sc, handler) ->
sc.queue.push(handler)
process_queue(sc)
# Update state with a particular method (`'push'` or
# `'replace'`) and state object. Then queues the handler
# associated with this state.
update_state: (method, state, handler) ->
state.id = @id
state.stack_id = @stack.length
history["#{method}State"](state, state.title or '', state.uri)
@stack.push(handler)
queue_handler(@, handler)
return
# Replace the current state with a new URI and handler. The
# handler will be passed one argument, a callback which must
# be triggered when the state behaviour is finished.
replace: (uri, handler) -> @update_state('replace', uri: uri, handler)
# Push a new state with a URI and handler. The handler will
# be passed one argument, a callback which must be triggered
# when the state behaviour is finished.
push: (uri, handler) -> @update_state('push', uri: uri, handler)
wrap_sync = (handler) ->
return (done) ->
handler()
done()
# A synchronous form of `replace()`. The only difference is
# the handler is not passed any arguments and is expected to
# complete synchronously.
sync_replace: (uri, handler) -> @replace(uri, wrap_sync(handler))
# A synchronous form of `push()`. The only difference is
# the handler is not passed any arguments and is expected to
# complete synchronously.
sync_push: (uri, handler) -> @push(uri, wrap_sync(handler))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment