Jump To …

model.coffee

this.Model = (name, options, func) ->

  model = (attributes) ->
    self = this
    this._tagId = null
    this._refresh attributes
    this._errors = {}
    this._parent = {}
    this._name = name
    this._json = {}
    this._uid = _.uniqueId("#{this._name}_")
    this.bind "change", this._change
    this.bind "data:format", this.toData
    _.each model.listAssociations(), (associationName) -> self[associationName]._parent = self
    this.initialize(attributes)
    this

  model._name = name

  _.extend model.prototype, Model.Events,

Callback that can be used when a record is initialized

    initialize: -> # Do nothing

Returns the model name of the record

    modelName: -> "#{_.classify this._name}"

Returns the attributes associated with the record

    attributes: -> this._attributes

    _change: (record) ->
      _.each record._changes, (value, key) ->
        if $(name).val() != value
          $(name).val value

    _refresh: (attributes) ->
      this._attributes = attributes or= {}
      this._parseAssociations attributes
      this._prevAttributes = _.clone(this._attributes)
      this._changes = {}
      this.attr(attributes)
      this._changes = {}

    _parseAssociations: (attributes) ->
      _.each model._manyAssociations, (assoc) ->
        val = attributes[assoc]
        delete attributes[assoc]
        _.each val, (attrs) ->
          instance = eval("new #{_.classify assoc}")
          instance._refresh(attrs)
          instance._parent = this
          this[assoc].add instance
        , this
      , this

Get the attribute value. post.get("body")

    get: (key) ->
      this._attributes[key]

Set the attribute value. Can set multiple keys at once. post.set("body", "Lorem Ipsum..") post.set({body: "Lorem Ipsum", title: "Fake Latin Lesson"})

    set: (key, value) ->
      if _.isString(key) or _.isNumber(key)

Run value through sanitation if set

        if model._sanitizers[key]
          value = model._sanitizers[key].call(this, value)

Delete from changes if changed back to previous value

        if _.isEqual this._prevAttributes[key], value
          delete this._changes[key]

Create new changes

        else
          this._changes[key] = value

Set new value and trigger changes

        this._attributes[key] = value
        this.trigger("change", [this])
      else
        for k, v of key
          this.set(k, v)

Set or get an attribute value/values post.attr("body", "Lorem Ipsum") post.attr("body") #=> "Lorem Ipsum"

    attr: (key, value) ->
      argLen = arguments.length
      return false if (_.isUndefined(key) or _.isEmpty(key)) && argLen > 0
      if (argLen == 0)
        this._attributes
      else if (argLen == 2)
        this.set(key, value)
      else if _.isString(key) or _.isNumber(key)
        this.get(key)
      else
        this.set(key)

Indicates if the record has changed or not. Returns true or false

    changed: -> this._changes != {}

Changes to the attributes of the record Returns {key: "value", key2: "value2"}

    changes: -> this._changes

Unbind record from a form. See bindTo()

    unbindFrom: (form) ->
      $(form).undelegate ":input", "change", this.onBoundChange
      $(form).undelegate ":input", "keyup", this.onBoundChange

Bind record to a form. All changes to input/select/textarea fields will automatically update the attributes on a record. post.bindTo "#post_form"

    bindTo: (form) ->
      self = this
      $(form).delegate ":input", "change", {record: this}, this.onBoundChange
      $(form).delegate ":input", "keyup", {record: this}, this.onBoundChange

    onBoundChange: (e) ->
      el = $(e.target)
      record = e.data.record
      value = el.val()
      keys = record._parseNameField el
      keys.shift()
      record._parseAttributeKeys(keys, value)

    _parseAttributeKeys: (keys, value) ->

attribute

      if keys.length == 1
        key = keys[0]
        this.attr key, value

association

      else if keys.length > 1
        this._parseAssociationKeys(keys, value)

    _parseAssociationKeys: (keys, value) ->
      assoc = keys.shift().replace("_attributes", "")
      uid = keys.shift()
      key = keys[0]
      if !this[assoc]._object
        obj = this[assoc].findByUid(uid)
      else
        obj = this[assoc]._object
      obj._parseAttributeKeys(keys, value)

    _parseNameField: (el) ->
      _.map el.attr("name").split("["), (p) -> p.replace("]", "")

Gets the id of the record from the server

    id: -> this.get("id")

The UID for the record object. This is generated randomly for each record and is never equal to the id()

    uid: -> this._uid

Determines if the record is new or not. Looks at the id() and if it is set, then it's not new. Returns true or false

    isNew: -> !_.isNumber this.id()

Displays the validations errors that came from the server response

    errors: -> this._errors

Reset validation errors

    resetErrors: -> this._errors = {}

When a record has been marked hasMany or hasOne, they become a child in that association. parent() is a convenient way to access the parent object. Example, Post.hasMany "comments" comment.parent() #=> post record

    parent: -> this._parent

Deprecated

    tagId: -> this._tagId

Generates JSON for the persistence request. You never need to call this but it's useful to look at for debugging purposes. You can also override this method to generate your own toJSON logic.

    toJSON: (options) ->
      baseObj = if options and options.child
        this._json = _.clone this.attributes()
      else
        this._json[model._name] = _.clone this.attributes()

      this.trigger "data:format", [this]

Loop through hasMany associations

      _.each model._manyAssociations, (association) ->
        model.prototype[association].each (child) ->
          childKey = "#{association}_attributes"
          baseObj[childKey] = [] unless baseObj[childKey]
          baseObj[childKey].push child.toJSON({child: true})
        , this
      , this

Loop through hasOne associations

      _.each model._oneAssociations, (association) ->
        child = model.prototype[association]
        unless child._fake
          this._json[model._name]["#{association}_attributes"] = child.toJSON({child: true})
      , this

      this._json

The url used to make REST requests

    getUrl: (method) ->
      path = _.pluralize "/#{model._name}"
      path = "#{path}/#{this.id()}" unless this.isNew()
      path

Saves the record Example: post.save success: (validPost) -> console.log "SUCCESS" errors: (invalidPost) -> console.log invalidPost.errors

    save: (options) ->
      method = if this.isNew() then "create" else "update"

      record = this

      options or= {}
      success = options.success
      error = options.error

      options.success = (resp, status, xhr) ->
        record.resetErrors()
        unless _.isEmpty resp
          record.attr "id", resp["id"]
        success record, resp, xhr
        record.trigger "save:after", [record]
      options.error = (resp, status, xhr) ->
        record._errors = $.parseJSON(resp.responseText)
        error record, resp, xhr if error
        record.trigger "save:after", [record]

      record.trigger "save:before", [record]

      Model.Sync(record, method, options)

Deprecated.

    toData: -> # Do nothing

Associations

  _.extend model,
    _manyAssociations: []
    _oneAssociations: []
    _sanitizers: {}

    listAssociations: ->
      this._manyAssociations.concat this._oneAssociations

Sets a hasMany association for the model Post.hasMany "comments"

post.comments #=> [{comment1...}, {comment2...}]

You can also extend the collection methods by using extend Post.hasMany "comments", extend: popularComments: -> this.records = this.sortBy (comment) -> comment.position()

    hasMany: (name, options) ->
      this._manyAssociations.push name
      manyArray = {}
      collection = new Model.Collection(name)

      if options and options.extend
        _.extend collection, options.extend

      manyArray[name] = collection
      _.extend model.prototype, manyArray

Sets a hasOne association for the model Post.hasOne "user"

    hasOne: (name) ->
      this._oneAssociations.push name
      association = new Model.One(name)
      oneObj = {}
      oneObj[name] = association
      oneObj["build_#{name}"] = -> association.build()
      oneObj["clear_#{name}"] = -> association.clear()
      _.extend model.prototype, oneObj

Sanitize incoming record attribute data Task.sanitize "hours", (hours) -> parseInt(hours)

Then if you do this: task.set("hours", "5")

You should get: task.get("hours") #=> 5

    sanitize: (key, callback) ->
      this._sanitizers[key] = callback

    newCollection: ->
      new Model.Collection(this._name)

    fetch: (params) ->
      model.newCollection().fetch(params)

    query: (options) ->
      model.newCollection().query(options)

  model

this.Model.Events =
  bind: (event, callback) ->
    this.callbacks = this.callbacks || {}
    this.callbacks[event] = this.callbacks[event] || []
    this.callbacks[event].push(callback)
    this

  trigger: (name, data) ->
    this.callbacks = this.callbacks || {}

    callbacks = this.callbacks[name]

    if callbacks
      for callback in callbacks
        callback.apply(this, data || [])

    this

Ajax persistence

this.Model.Sync = (obj, method, options) ->
  methodVerbs = {
    "create": "POST",
    "update": "PUT",
    "delete": "DELETE",
    "read":   "GET"
  }

  params = _.extend({
    type:        methodVerbs[method],
    contentType: "application/json",
    dataType:    "json",
    processData: if method == "read" then true else false}, options)

  params.url = obj.getUrl(method)

  unless options.data
    data = JSON.stringify obj.toJSON()
    params.data = data unless data == "{}"

  $.ajax(params)

One

this.Model.One = (name) ->
  this._name = name
  this._fake = true
  this

_.extend Model.One.prototype,
  build: ->
    record = eval("new #{_.classify this._name}")
    record._parent = this.parent() if this.parent()
    this._object = record

  clear: ->
    this.parent()[this._name] = this

  parent: -> this._parent

Collection

this.Model.Collection = (name) ->
  this._name = _.singularize name
  this._options = {}
  this._reset()
  this.url = this.getUrl(name)
  this.bind "change", this._change
  this.bind "add", (collection) -> this.trigger("refresh", [collection])
  this.bind "remove", (collection) -> this.trigger("refresh", [collection])
  this

_.extend Model.Collection.prototype, Model.Events,
  getUrl: ->
    "/#{_.pluralize this._name}"

  add: (records) ->
    if _.isArray records
      _.each records, (record) ->
        this._add record
      , this
    else
      this._add records

  removeAll: ->
    this.records = []

this.remove this.records

  remove: (records) ->
    if _.isArray records
      _.each records, (record) ->
        this._remove record
      , this
    else
      this._remove records

  _change: (record) ->
    self = this
    _.each record._changes, (value, key) ->
      index_id = record.tagId()
      current_model_name = _.pluralize(record._name)
      name = '[name="' + self.parent()._name + "[#{current_model_name}_attributes][#{index_id}][#{key}]" + '"]'
      $(name).val value

  parent: -> this._parent

  get: (id) ->
    return null if _.isNull id
    return this._byId[if !_.isNull id.id then id.id else id]

  refresh: (records) ->
    this._reset()
    this.add(records)
    this.trigger("refresh", [this])
    this

  fetch: (params) ->
    this.query(data: params)
    this

  findByTagId: (tag_id) ->
    _.detect this.records, (element) -> element.tagId() == parseInt(tag_id)

  removeByTagId: (tag_id) ->
    record = _.detect this.records, (element) -> element.tagId() == parseInt(tag_id)
    this._remove(record) if record

  findByUid: (uid) ->
    _.detect this.records, (record) -> record.uid() == uid

  removeByUid: (uid) ->
    record = this.findByUid(uid)
    this._remove(record) if record

  query: (options) ->
    this._options = options or= {}
    collection = this
    success = options.success
    options.success = (resp, status, xhr) ->
      collection.refresh(resp)
      success(collection, resp) if success

    Model.Sync(this, "read", options)
    this

  toJSON: -> this._options

  pluck: (attr) ->
    _.map this.records, (record) ->
      record.get attr

  _add: (record) ->

If a json object is passed, convert to record object

    unless record._name
      attr = record
      record = eval("new #{_.classify this._name}")
      record._refresh(attr)

    this._bindRecordEvents(record)
    record._parent = this.parent() if this.parent()
    this.records.push record
    this.length++
    this.trigger("add", [record])
    record

  _remove: (record) ->
    index = this.records.indexOf(record)
    this.records.splice(index, 1)
    this.trigger("remove", [record])

Deprecate this in favor of dispatching a change event after a bind to remove

    this.trigger("change", [record])
    this.length--

  _reset: ->
    this.length = 0
    this.records = []
    this._byId = {}

  _bindRecordEvents: (record) ->
    collection = this
    record.bind "change", ->
      collection.trigger "change", [record]

Underscore methods that we want to implement on the Collection.

methods = ['forEach', 'each', 'map', 'reduce', 'reduceRight', 'find', 'detect',
  'filter', 'select', 'reject', 'every', 'all', 'some', 'any', 'include',
  'invoke', 'max', 'min', 'sortBy', 'sortedIndex', 'toArray', 'size',
  'first', 'rest', 'last', 'without', 'indexOf', 'lastIndexOf', 'isEmpty']

_.each methods, (method) ->
  Model.Collection.prototype[method] = ->
    _[method].apply(_, [this.records].concat(_.toArray(arguments)))