Skip to content

Instantly share code, notes, and snippets.

@jashkenas
Forked from maccman/spine.coffee
Created June 3, 2011 01:56
Show Gist options
  • Star 17 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save jashkenas/1005723 to your computer and use it in GitHub Desktop.
Save jashkenas/1005723 to your computer and use it in GitHub Desktop.
# In general, there are many places in the file where you can leave off the parens in function calls, if you like.
unless typeof exports is "undefined" # `unless ... else` reads poorly in English. Better to stick to `if ... else`.
Spine = exports
else
Spine = @Spine = {}
Spine.version = "0.0.4"
$ = Spine.$ = @jQuery || @Zepto || -> arguments[0] # In Coffee, `or` is preferred over `||`.
makeArray = Spine.makeArray = (args) ->
Array.prototype.slice.call(args, 0) # A shortcut for prototype accessors is: `::` `Array::slice` will do.
isArray = Spine.isArray = (value) ->
Object.prototype.toString.call(value) == "[object Array]" # Ditto. `Object::toString`
Events = Spine.Events =
bind: (ev, callback) ->
evs = ev.split(" ")
calls = @_callbacks || @_callbacks = {}
for name in evs
@_callbacks[name] ?= [] # `or=` here would be more efficient, if it matters.
@_callbacks[name].push(callback)
@
trigger: (args...) ->
ev = args.shift()
list = @_callbacks?[ev]
return false unless list
for callback in list
if callback.apply(this, args) is false
break
true
unbind: (ev, callback) ->
unless ev
@_callbacks = {}
return @
list = @_callbacks?[ev]
return @ unless list
unless callback
delete @_callbacks[ev]
return @
for cb, i in list # You can use `when` to filter a comp. `for cb, i in list when cb is callback`
if callback == cb
list.splice(i, 1)
break
@
Log = Spine.Log =
trace: true
logPrefix: "(App)"
log: (args...) ->
return unless @trace
return if typeof console == "undefined" # In Coffee, prefer `is` to `==`.
if @logPrefix then args.unshift(@logPrefix)
console.log.apply(console, args)
@
# Classes (or prototypial inheritors)
unless typeof Object.create is "function" # This faux Object.create is not API compatible with the real thing, so be careful.
Object.create = (o) ->
F = ->
F.prototype = o
new F()
moduleKeywords = ["included", "extended"]
Class = Spine.Class =
inherited: ->
created: ->
prototype:
initialize: ->
init: ->
create: (include, extend) ->
object = Object.create(@)
object.parent = @
object:: = object.fn = Object.create(@::)
object.include(include) if include
object.extend(extend) if extend
object.created()
@inherited(object)
object
init: ->
instance = Object.create(@::)
instance.parent = @
instance.initialize.apply(instance, arguments)
instance.init.apply(instance, arguments)
instance
proxy: (func) ->
=> func.apply(this, arguments)
proxyAll: (names...) ->
@[name] = @proxy(@[name]) for name in names
include: (obj) ->
for key, value of obj # Again, you can use a `when` if you like.
unless key in moduleKeywords
@::[key] = value
included = obj.included
included.apply(this) if included
@
extend: (obj) ->
for key, value of obj
unless key in moduleKeywords
@[key] = value
extended = obj.extended
extended.apply(this) if extended
@
Class::proxy = Class.proxy
Class::proxyAll = Class.proxyAll
Class.inst = Class.init
Class.sub = Class.create
Spine.guid = `function(){
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
return v.toString(16);
}).toUpperCase();
}` # This shouldn't need to be backtick'd. No?
Model = Spine.Model = Class.create()
Model.extend(Events)
Model.extend
setup: (name, atts) ->
model = Model.sub()
model.name = name if name
model.attributes = atts if atts
model
created: (sub) ->
@records = {}
if @attributes then makeArray(@attributes) # Multi-line if/elses should probably use indentation.
else @attributes = []
find: (id) ->
record = @records[id]
throw("Unknown record") unless record
record.clone()
exists: (id) ->
try
return @find(id)
catch e
return false
refresh: (values) ->
values = @fromJSON(values)
@records = {}
for record in values
record.newRecord = false
@records[record.id] = record
@trigger("refresh")
@
select: (callback) ->
result = (record for id, record of @records when callback(record))
@cloneArray(result)
findByAttribute: (name, value) ->
for id, record of @records
if record[name] == value
return record.clone()
null
findAllByAttribute: (name, value) ->
@select (item) ->
item[name] == value
each: (callback) ->
for key, value of @records
callback(value)
all: ->
@cloneArray(@recordsValues())
first: ->
record = @recordsValues()[0]
record?.clone()
last: ->
values = @recordsValues()
record = values[values.length - 1]
record?.clone()
count: ->
@recordsValues().length
deleteAll: ->
for key, value of @records
delete @records[key]
destroyAll: () -> # Functions that take no arguments require no parens before the arrow.
for key, value of @records
@records[key].destroy()
update: (id, atts) ->
@find(id).updateAttributes(atts)
create: (atts) ->
record = @init(atts)
record.save()
destroy: (id) ->
@find(id).destroy()
sync: (callback) ->
@bind("change", callback)
fetch: (callbackOrParams) ->
if typeof(callbackOrParams) == "function"
@bind("fetch", callbackOrParams)
else
@trigger("fetch", callbackOrParams)
toJSON: ->
@recordsValues()
fromJSON: (objects) ->
return unless objects
if typeof objects == "string"
objects = JSON.parse(objects)
if isArray(objects)
return (@init(value) for value in objects)
else
@init(objects)
# Private
recordsValues: ->
result = []
for key, value of @records
result.push(value)
result
cloneArray: (array) ->
result = []
result.push value.clone() for value in array
result
Model.include
model: true
newRecord: true
init: (atts) ->
@load atts if atts
@trigger("init", this)
isNew: () ->
@newRecord
isValid: () ->
not @validate()
validate: ->
load: (atts) ->
for key, value of atts
@[key] = value
attributes: ->
result = {}
result[key] = @[key] for key in @parent.attributes
result.id = @id
result
eql: (rec) ->
rec && rec.id == @id && rec.parent == @parent
save: ->
error = @validate()
if error
@trigger("error", @, error)
return false
@trigger("beforeSave", @)
if @newRecord then @create() else @update()
@trigger("save", @)
return @
updateAttribute: (name, value) ->
@[name] = value
@save()
updateAttributes: (atts) ->
@load(atts)
@save()
destroy: ->
@trigger("beforeDestroy", @)
delete @parent.records[@id]
@destroyed = true
@trigger("destroy", @)
@trigger("change", @, "destroy")
dup: ->
result = @parent.init(@attributes())
result.newRecord = @newRecord
result
clone: ->
Object.create(@)
reload: ->
return @ if @newRecord
original = @parent.find(@id)
@load(original.attributes())
return original
toJSON: ->
@attributes()
exists: ->
@id && @id of @parent.records # Prefer `and` to `&&`.
# Private
update: ->
@trigger("beforeUpdate", @)
records = @parent.records
records[@id].load @attributes()
clone = records[@id].clone()
@trigger("update", clone)
@trigger("change", clone, "update")
create: ->
@trigger("beforeCreate", @)
@id = Spine.guid() unless @id
@newRecord = false
records = @parent.records
records[@id] = @dup()
clone = records[@id].clone()
@trigger("create", clone)
@trigger("change", clone, "create")
bind: (events, callback) ->
@parent.bind events, (record) =>
if record && @eql(record)
callback.apply(@, arguments)
trigger: ->
@parent.trigger.apply(@parent, arguments)
# Controllers
eventSplitter = /^(\w+)\s*(.*)$/
Controller = Spine.Controller = Class.create
tag: "div"
initialize: (options) ->
@options = options
for key, value of @options
@[key] = value
@el = document.createElement(@tag) unless @el
@el = $(@el)
@events = @parent.events unless @events
@elements = @parent.elements unless @elements
@delegateEvents() if @events
@refreshElements() if @elements
@proxyAll.apply(@, @proxied) if @proxied
$: (selector) ->
$(selector, @el)
delegateEvents: ->
for key of @events
methodName = @events[key]
method = @proxy(@[methodName])
match = key.match(eventSplitter)
eventName = match[1]
selector = match[2]
if selector == ''
@el.bind(eventName, method)
else
@el.delegate(selector, eventName, method)
refreshElements: ->
for key, value of @elements
@[value] = @$(key)
delay: (func, timeout) ->
setTimeout(@proxy(func), timeout || 0)
Controller.include(Events)
Controller.include(Log)
Spine.App = Class.create()
Spine.App.extend(Events)
Controller.fn.App = Spine.App
@alexanderdickson
Copy link

I believe the reason that code is backticked is due to this Stack Overflow answer.

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