Skip to content

Instantly share code, notes, and snippets.

@elliottkember
Last active August 9, 2018 14:32
Show Gist options
  • Save elliottkember/0105c2c2279b0d347707 to your computer and use it in GitHub Desktop.
Save elliottkember/0105c2c2279b0d347707 to your computer and use it in GitHub Desktop.
A simple application framework for Framer

Application State Machine

a = new AppStateUI

a.on 'firstSlide', enter: ->
	@layer = new Layer width: 100, height: 100, x: 100, y: 100, backgroundColor: "green"
	@layer.on Events.Click, a.go.bind(a, 'secondSlide')
, leave: ->
	@layer.destroy()
	
a.on 'secondSlide', enter: ->
	@layer = new Layer width: 100, height: 100, x: 100, y: 200, backgroundColor: "blue"
	@layer.on Events.Click, a.go.bind(a, 'firstSlide')
, leave: -> 
	@layer.destroy()
EventEmitter = (require?("./EventEmitter") || Framer).EventEmitter
class AppStateMachine extends EventEmitter
constructor: ->
@states = []
# Start the state machine with the document's hash, or the given route
start: (name) ->
@go if document.location.hash then document.location.hash[1..-1] else name
# Add a callback
on: (event, options) ->
@states.push event unless event.indexOf(":") > -1 or event in @states
options = enter: options if typeof options == 'function'
super event, options.enter if options.enter
super "#{event}:leave", options.leave if options.leave
@emit 'change:states' unless event == 'change:states'
# Transition to a state
go: (state, callback=false) ->
return if state == @state
@emit "#{@state}:leave"
@emit document.location.hash = @state = state
@emit "#{@state}:enter"
callback?()
@emit 'change:state'
## A UI wrapper for the above state machine.
class AppStateUI extends AppStateMachine
constructor: (options...) ->
super options
@on "change:state", @render.bind @
@on "change:states", @render.bind @
@render()
render: ->
frame = @layer?.frame || width: 150, height: 0, x: 20, y: 20
@layer?.destroy()
@layer = new Layer frame: frame, backgroundColor: 'transparent'
lineHeight = 40
for state, i in @states
stateLine = new Layer width: 150, height: 40, x: 0, y: lineHeight * i, superLayer: @layer, backgroundColor: 'transparent'
stateLine.html = state
stateLine.style['font-weight'] = if state == @state then 800 else 100
stateLine.on Events.Click, @go.bind(@, state)
@layer.height += lineHeight
exports = window if typeof exports == 'undefined'
exports.AppStateUI = AppStateUI
exports.AppStateMachine = AppStateMachine
describe "AppStateMachine", ->
describe "setup", ->
it "should be created with empty states", ->
a = new AppStateMachine()
a.states.should.have.length(0)
describe "states", ->
it "should let you create a new state", ->
j = new AppStateMachine()
l = j.states.length
j.on "asdfasdf", ->
j.success = true
j.states.should.have.length(l + 1)
it "should have unique state names", ->
sm = new AppStateMachine
sm.on "a", ->
sm.on "a", ->
sm.states.should.eql(['a'])
describe "callbacks", ->
it "should not fire change:states when on('change:states') is added", ->
sm = new AppStateMachine
sm.success = false
sm.on "change:states", ->
sm.success = "true"
sm.success.should.eql(false)
it "should fire change:states when a state is added", ->
success = false
sm = new AppStateMachine
sm.on "change:states", enter: ->
sm.success = "true"
sm.on "something", ->
true
sm.success.should.eql("true")
it "should run callbacks on state change", ->
sm = new AppStateMachine
sm.on "asdf", -> sm.success = true
sm.go "asdf"
sm.success.should.equal(true)
it "should remove a state with .off", ->
sm = new AppStateMachine()
sm.success = false
callback = (-> sm.success = true)
sm.on "fdsa", callback
sm.off "fdsa", callback
sm.go "fdsa"
sm.success.should.equal(false)
it "should run callbacks on state leave", ->
sm = new AppStateMachine
sm.on "create", enter: (-> sm.success = "false"), leave: (-> sm.success = "true")
sm.go "create"
sm.go "somethingElse" # this calls create:end
sm.success.should.equal("true")
it "should run callbacks on state leave", ->
sm = new AppStateMachine
sm.success = ""
sm.on "create", enter: (-> sm.success += "create"), leave: (-> sm.success += " leave")
sm.go "create"
sm.go "somethingElse" # this calls create:end
sm.success.should.equal("create leave")
it "should run callbacks added separately on state leave", ->
sm = new AppStateMachine
sm.on "create:enter", -> sm.success = "false"
sm.on "create:leave", -> sm.success = "true"
sm.go "create"
sm.go "somethingElse" # calls create:leave
sm.success.should.equal("true")
it "should run a .once once", ->
sm = new AppStateMachine
i = 0
sm.once "create", -> i++
sm.go "create"
sm.go "away"
sm.go "create"
i.should.eql(1)
describe "document.location.hash", ->
it "should write on go()", ->
sm = new AppStateMachine
sm.on "create", enter: (() ->), leave: (() ->)
sm.go 'create'
document.location.hash.should.eql("#create")
it "should read on setup()", ->
sm = new AppStateMachine
document.location.hash = "#create"
sm.on "default", enter: (() ->), leave: (() ->)
sm.on "create", enter: (() ->), leave: (() ->)
sm.start('default')
sm.state.should.eql("create")
document.location.hash.should.eql("#create")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment