3vot-model
Version:
3VOT Model based on SpineJS
393 lines (306 loc) • 8.96 kB
text/coffeescript
Events = require("./events")
Module = require("./module")
class Model extends Module
@extend Events
@records : []
@irecords : {}
@attributes : []
@configure: (name, attributes...) ->
@className = name
@deleteAll()
@attributes = attributes if attributes.length
@attributes and= makeArray(@attributes)
@attributes or= []
@unbind()
this
@toString: -> "#{@className}(#{@attributes.join(", ")})"
@find: (id) ->
record = @exists(id)
throw new Error("\"#{@className}\" model could not find a record for the ID \"#{id}\"") unless record
return record
@exists: (id) ->
@irecords[id]?.clone()
@addRecord: (record) ->
if record.id and @irecords[record.id]
@irecords[record.id].remove()
record.id or= record.cid
@records.push(record)
@irecords[record.id] = record
@irecords[record.cid] = record
@refresh: (values, options = {}) ->
@deleteAll() if options.clear
records = @fromJSON(values)
records = [records] unless isArray(records)
@addRecord(record) for record in records
@sort()
result = @cloneArray(records)
@trigger('refresh', result, options)
result
@select: (callback) ->
(record.clone() for record in @records when callback(record))
@findByAttribute: (name, value) ->
for record in @records
if record[name] is value
return record.clone()
null
@findAllByAttribute: (name, value) ->
@select (item) ->
item[name] is value
@each: (callback) ->
callback(record.clone()) for record in @records
@all: ->
@cloneArray(@records)
@first: ->
@records[0]?.clone()
@last: ->
@records[@records.length - 1]?.clone()
@count: ->
@records.length
@deleteAll: ->
@records = []
@irecords = {}
@destroyAll: (options) ->
record.destroy(options) for record in @records
@update: (id, atts, options) ->
@find(id).updateAttributes(atts, options)
@create: (atts, options) ->
record = new @(atts)
record.save(options)
@destroy: (id, options) ->
@find(id).destroy(options)
@change: (callbackOrParams) ->
if typeof callbackOrParams is 'function'
@bind('change', callbackOrParams)
else
@trigger('change', arguments...)
@fetch: (callbackOrParams) ->
if typeof callbackOrParams is 'function'
@bind('fetch', callbackOrParams)
else
@trigger('fetch', arguments...)
@toJSON: ->
@records
@fromJSON: (objects) ->
return unless objects
if typeof objects is 'string'
objects = JSON.parse(objects)
if isArray(objects)
(new @(value) for value in objects)
else
new @(objects)
@fromForm: ->
(new this).fromForm(arguments...)
@sort: ->
if @comparator
@records.sort @comparator
this
# Private
@cloneArray: (array) ->
(value.clone() for value in array)
@idCounter: 0
@uid: (prefix = '') ->
uid = prefix + @idCounter++
uid = @uid(prefix) if @exists(uid)
uid
# Instance
constructor: (atts) ->
super
@load atts if atts
@cid = atts?.cid or @constructor.uid('c-')
isNew: ->
not @exists()
isValid: ->
not @validate()
validate: ->
load: (atts) ->
if atts.id then @id = atts.id
for key, value of atts
if atts.hasOwnProperty(key) and typeof @[key] is 'function'
@[key](value)
else
@[key] = value
this
get: (attr) ->
return @[attr];
set: (attr, value) ->
@[attr] = value;
attributes: ->
result = {}
for key in @constructor.attributes when key of this
if typeof @[key] is 'function'
result[key] = @[key]()
else
result[key] = @[key]
result.id = @id if @id
result
eql: (rec) ->
!!(rec and rec.constructor is @constructor and
(rec.cid is @cid) or (rec.id and rec.id is @id))
save: (options = {}) ->
unless options.validate is false
error = @validate()
if error
@trigger('error', error)
return false
@trigger('beforeSave', options)
record = if @isNew() then @create(options) else @update(options)
@stripCloneAttrs()
@trigger('save', options)
record
stripCloneAttrs: ->
return if @hasOwnProperty 'cid' # Make sure it's not the raw object
for own key, value of @
delete @[key] if @constructor.attributes.indexOf(key) > -1
this
updateAttribute: (name, value, options) ->
atts = {}
atts[name] = value
@updateAttributes(atts, options)
updateAttributes: (atts, options) ->
@load(atts)
@save(options)
changeID: (id) ->
return if id is @id
records = @constructor.irecords
records[id] = records[@id]
delete records[@id]
@id = id
@save()
remove: ->
# Remove record from model
records = @constructor.records.slice(0)
for record, i in records when @eql(record)
records.splice(i, 1)
break
@constructor.records = records
# Remove the ID and CID
delete @constructor.irecords[@id]
delete @constructor.irecords[@cid]
destroy: (options = {}) ->
@trigger('beforeDestroy', options)
@remove()
@destroyed = true
# handle events
@trigger('destroy', options)
@trigger('change', 'destroy', options)
if @listeningTo
@stopListening()
@unbind()
this
dup: (newRecord = true) ->
atts = @attributes()
if newRecord
delete atts.id
else
atts.cid = @cid
new @constructor(atts)
clone: ->
createObject(this)
reload: ->
return this if @isNew()
original = @constructor.find(@id)
@load(original.attributes())
original
refresh: (data) ->
# go to the source and load attributes
root = @constructor.irecords[@id]
root.load(data)
@trigger('refresh')
@
toJSON: ->
@attributes()
toString: ->
"<#{@constructor.className} (#{JSON.stringify(this)})>"
fromForm: (form) ->
result = {}
###
for checkbox in $(form).find('[type=checkbox]:not([value])')
result[checkbox.name] = $(checkbox).prop('checked')
for checkbox in $(form).find('[type=checkbox][name$="[]"]')
name = checkbox.name.replace(/\[\]$/, '')
result[name] or= []
result[name].push checkbox.value if $(checkbox).prop('checked')
for key in $(form).serializeArray()
result[key.name] or= key.value
###
@load(result)
exists: ->
@constructor.exists(@id)
# Private
update: (options) ->
@trigger('beforeUpdate', options)
records = @constructor.irecords
records[@id].load @attributes()
@constructor.sort()
clone = records[@id].clone()
clone.trigger('update', options)
clone.trigger('change', 'update', options)
clone
create: (options) ->
@trigger('beforeCreate', options)
@id or= @cid
record = @dup(false)
@constructor.addRecord(record)
@constructor.sort()
clone = record.clone()
clone.trigger('create', options)
clone.trigger('change', 'create', options)
clone
bind: (events, callback) ->
@constructor.bind events, binder = (record) =>
if record && @eql(record)
callback.apply(this, arguments)
# create a wrapper function to be called with 'unbind' for each event
for singleEvent in events.split(' ')
do (singleEvent) =>
@constructor.bind "unbind", unbinder = (record, event, cb) =>
if record && @eql(record)
return if event and event isnt singleEvent
return if cb and cb isnt callback
@constructor.unbind(singleEvent, binder)
@constructor.unbind("unbind", unbinder)
this
one: (events, callback) ->
@bind events, handler = =>
@unbind(events, handler)
callback.apply(this, arguments)
trigger: (args...) ->
args.splice(1, 0, this)
@constructor.trigger(args...)
listenTo: -> Events.listenTo.apply @, arguments
listenToOnce: -> Events.listenToOnce.apply @, arguments
stopListening: -> Events.stopListening.apply @, arguments
unbind: (events, callback) ->
if arguments.length is 0
@trigger('unbind')
else if events
for event in events.split(' ')
@trigger('unbind', event, callback)
Model::on = Model::bind
Model::off = Model::unbind
# Utilities & Shims
createObject = Object.create or (o) ->
Func = ->
Func.prototype = o
new Func()
isArray = (value) ->
Object::toString.call(value) is '[object Array]'
isBlank = (value) ->
return true unless value
return false for key of value
true
makeArray = (args) ->
Array::slice.call(args, 0)
Model.isBlank = isBlank;
Model.sub = (instances, statics) ->
class Result extends this
Result.include(instances) if instances
Result.extend(statics) if statics
Result.unbind?()
Result
Model.setup = (name, attributes = []) ->
class Instance extends this
Instance.configure(name, attributes...)
Instance
Model.host = ""
module?.exports = Model