js-model (0.11.0)

js-model is a library that allows you to work with models in your JavaScript.

Download
js-model-0.11.0.min.js (8.6Kb)
js-model-0.11.0.js (16.9Kb)
Source
github.com/benpickles/js-model

Getting started

The first thing to do is to create a model class using the factory Model().

var Project = Model("project")

This allows you to create instances of “project” models and also contains an internal collection of all “projects” which can be used for querying.

Manipulating objects

Now you can create and manipulate instances of your new model. Attributes are read and set with the attr() method which works in a similar way to jQuery on the DOM.

var project = new Project({ id: 1, title: "stuff" })
project.attr("title", "nonsense")
project.save()

Finding objects

After calling save() on a model it is added to the class’s “collection” and can be retrieved again by calling find() (or first() as it is the first model in the collection).

Project.find(1)
// => project

Project.first()
// => project

You can retrieve all models from the collection with all().

Project.all()
// => [project]

For more ways to query the collection check out detect() and select().

Custom properties

You might need to give your model custom methods and properties. There are two parts to a model which can be extended, and these are akin to class and instance methods on an ORM such as ActiveRecord.

Class properties

When creating a model you can pass a function as the optional second argument and “extend” the class by adding methods to it.

var Project = Model("project", function() {
  this.extend({
    find_by_title: function(title) {
      return this.detect(function() {
        return this.attr("title") == title
      })
    }
  })
})

Project.find_by_title("stuff")
// => "stuff" project model

Instance properties

You can also “include” instance methods on the model’s prototype. These are often used to link objects together in a way that mimics the relationships the data might have in the remote database (“has many” etc). However, they can be pretty much anything and can overwrite the defaults.

var Project = Model("project", function() {
  this.include({
    markAsDone: function() {
      this.attr("done", true)
    }
  })
})

Project.find(1).markAsDone()
// "stuff" project marked as done

Associations

Simple associations can be mimicked by adding a couple of instance methods. Here a Cat “belongs to” a Mat and a Mat “has many” Cats.

var Cat = Model("cat", function() {
  this.include({
    mat: function() {
      var mat_id = this.attr("mat_id")

      return Mat.detect(function() {
        return this.id() == mat_id
      })
    }
  })
})

var Mat = Model("mat", function() {
  this.include({
    cats: function() {
      var id = this.id()

      return Cat.select(function() {
        return this.attr("mat_id") == id
      })
    }
  })
})

Events

js-model allows you to listen to the lifecycle of objects based on the events they trigger at different points. Typically you’ll use this to link your data objects to UI elements.

Class events

It is possible to bind to an event occurring when adding and removing an object to a collection.

Project.bind("add", function(new_object) {
  add_object_to_ui(new_object)
})

Project.bind("remove", function(removed_object) {
  remove_object_from_ui(removed_object)
})

Instance events

Parts of your application can be bound to changes which happen to a specific instance:

var project = Project.first()

project.bind("update", function() {
  my_ui_elem.text(this.attr("name"))
})

Including when the instance is destroyed:

project.bind("destroy", function() {
  my_ui_elem.remove()
})

Custom events

You might also want to have custom events on objects which might be linked up to a UI element.

project.bind("turn_blue", function() {
  my_ui_elem.css("background", "blue")
})

project.trigger("turn_blue")

Validations

To add your own validations you should define a custom validate() method on your model that adds error messages to the errors object. valid() is called on save() and checks that there are no errors. Validations are useful when using localStorage persistence but can also help you avoid hitting your server unnecessarily if you’re using REST persistence.

Persistence

js-model is different to several other solutions, it’s not a REST-based proxy for the objects on your server and doesn’t rely on constant HTTP requests to gather information. Instead, it looks up objects in its own cache which can be populated via a persistence adapter — think of it as maintaining the state of your objects in the browser.

Persistence is defined as a class property and comes in two flavours: REST and localStorage. Both adapters encode/decode your attributes with JSON and so require the browser to be JSON-aware (or to include the JSON JavaScript library). Persistence is defined using the persistence() method.

REST

Uses jQuery’s ajax() method to GET, POST, PUT and DELETE model data to the server as JSON and expects JSON back.

var Project = Model("project", function() {
  this.persistence(Model.REST, "/projects")
})

Calling save() or destroy() on an object now fires a corresponding REST request:

var project = new Project({ name: "stuff" })
project.save()                            //   POST /projects
project.attr("name", "nonsense").save()   //    PUT /projects/1
project.destroy()                         // DELETE /projects/1

When responding to POST or PUT requests any JSON returned will be merged into the model’s attributes — you should also make sure to include the id in the POST response so it can be assigned to the model. 422 responses from the server will be interpreted as having failed validations, any returned JSON will be assumed to be errors and replace client-side errors.

Note: If you’re using Rails you should make sure to add the following setting in an initializer as js-model expects non-namespaced JSON:

ActiveRecord::Base.include_root_in_json = false

localStorage

localStorage is a client-side key/value store that persists between page views and browser sessions, it’s supported by Safari, Chrome, Firefox, Opera, IE8 and Safari Mobile (iPhone) — WebKit-based browsers have an excellent localStorage GUI in the Web Inspector.

var Project = Model("project", function() {
  this.persistence(Model.localStorage)
})

Loading data

If you have existing data stored in your persistence layer you’ll want to be able to have it available when you next open your app. You’ll typically call load() when your document loads and perform an action when it has completed.

// wait for the document to load
$(function() {
  Project.load(function() {
    // do something with the UI
  })
})

js-model ♥ Sammy

js-model works really well with Sammy — you are using Sammy right? Your routes might look something like this:

$.sammy(function() {
  this.get("#/projects", function() {
    var projects = Project.all()
    // display list of projects
  })

  this.post("#/projects", function(context) {
    var project = new Project(this.params.project)
    project.save(function(success) {
      if (success) {
        context.redirect("#/projects/" + project.id())
      } else {
        // display errors...
      }
    })
  })

  this.get("#/projects/:id", function() {
    var project = Project.find(this.params.id)
    // display project
  })

  this.put("#/projects/:id", function(context) {
    var project = Project.find(this.params.id)
    project.attr(this.param.project)
      .save(function(success) {
        if (success) {
          context.redirect("#/projects/" + project.id())
        } else {
          // display errors...
        }
      })
  })

  this.route("delete", "#/projects/:id", function(context) {
    Project.find(this.params.id)
      .destroy(function() {
        context.redirect("#/projects")
      })
  })
})

API

Model(name, func)

Model() is a factory method that is used to generate model classes. At its simplest it can be used like so:

var Project = Model("project")

The optional function is used to define custom methods and properties on your newly defined class and its prototype using extend() and include().

var Project = Model("project", function() {
  this.extend({
    find_by_title: function(title) {
      return this.detect(function() {
        return this.attr("title") == title
      })
    }
  })

  this.include({
    markAsDone: function() {
      this.attr("done", true)
    }
  })
})

Project.find_by_title("stuff").markAsDone()
// "stuff" project marked as done

The function is also called with two arguments: the class itself and its prototype. The above could be written as:

var Project = Model("project", function(klass, proto) {
  klass.find_by_title = function(title) {
    return this.detect(function() {
      return this.attr("title") == title
    })
  }

  proto.markAsDone = function() {
    this.attr("done", true)
  }
})

Project.find_by_title("stuff").markAsDone()
// "stuff" project marked as done

Class properties

add(model)

Adds a model to a collection and is what save() calls internally if it is successful. add() won’t allow you to add a model to the collection if one already exists with the same id or uid.

Food.all()
// => []

var egg = new Food({ id: 1, name: "egg" })
var ham = new Food({ id: 2, name: "ham" })
var cheese = new Food({ id: 3, name: "cheese" })

Food.add(egg)
Food.all()
// => [egg]

Food.add(ham).add(cheese)
Food.all()
// => [egg, ham, cheese]

var not_egg = new Food({ id: 1, name: "not egg" })

Food.add(not_egg)
Food.all()
// => [egg, ham, cheese]

all()

Returns an array of the models contained in the collection.

Food.all()
// => [egg, ham, cheese]

Food.select(function() {
  return this.attr("name").indexOf("e") > -1
}).all()
// => [egg, cheese]

chain(arrayOfModels)

A utility method to enable chaining methods on a collection — used internally by select() for instance.

count()

Returns the size of the collection.

Food.count()
// => 3

Food.select(function() {
  return this.attr("name").indexOf("e") > -1
}).count()
// => 2

detect(func)

Operates on the collection returning the first model that matches the supplied function.

Food.detect(function() {
  return this.attr("name") == "ham"
})
// => ham

each(func)

Iterates over the collection calling the supplied function for each model.

Food.each(function() {
  console.log(this.attr("name"))
})
// => logs "egg", "ham" and "cheese"

Food.select(function() {
  return this.attr("name").indexOf("e") > -1
}).each(function() {
  console.log(this.attr("name"))
})
// => logs "egg" and "cheese"

extend(object)

Add class methods.

Food.extend({
  nameHasLetter: function(letter) {
    return this.select(function() {
      return this.attr("name").indexOf(letter) > -1
    })
  }
})

Food.nameHasLetter("e")
// => [egg, cheese]

find(id)

Returns the model with the corresponding id.

Food.find(2)
// => ham

Food.find(69)
// => undefined

first()

Returns the first model in the collection.

Food.first()
// => egg

Food.select(function() {
  return this.attr("name").indexOf("h") > -1
}).first()
// => ham

include(object)

Add methods to the class’s prototype.

Food.include({
  reverseName: function() {
    return this.attr("name").split("").reverse().join("")
  }
})

var carrot = new Food({ name: "Carrot" })
carrot.reverseName()
// => "torraC"

Note: Be careful when adding properties that aren’t primitives to an object’s prototype, this can result in unexpected behaviour as the prototype is shared across all instances, for example:

var Post = Model("post")
Post.include({
  comments: [],

  comment: function(text) {
    this.comments.push(text)
  }
})

var post1 = new Post({ title: "Ham" })
var post2 = new Post({ title: "Egg" })

post1.comments  // => []
post2.comments  // => []

post1.comment("Tasty")

post1.comments  // => ["Tasty"]
post2.comments  // => ["Tasty"]

In the above case an initializer would take care of things:

var Post = Model("post", function() {
  this.include({
    initialize: function() {
      this.comments = []
    },

    comment: function(text) {
      this.comments.push(text)
    }
  })
})

var post1 = new Post({ title: "Ham" })
var post2 = new Post({ title: "Egg" })

post1.comments  // => []
post2.comments  // => []

post1.comment("Tasty")

post1.comments  // => ["Tasty"]
post2.comments  // => []

last()

Returns the last model in the collection.

Food.last()
// => cheese

load(callback)

Calls read() on the persistence adapter and adds the returned models to the collection. The supplied callback is then passed an array of the newly added models.

Food.load(function(models) {
  // do something...
})

map(func)

Operates on the collection returning an array of values by calling the specified method on each instance.

Food.map(function() {
  return this.attr("name").toUpperCase()
})
// => ["EGG", "HAM", "CHEESE"]

Food.select(function() {
  return this.attr("name").indexOf("e") > -1
}).map(function() {
  return this.attr("name").toUpperCase()
})
// => ["EGG", "CHEESE"]

new(attributes)

Instantiates a model, the supplied attributes get assigned directly to the model’s attributes. Custom initialization behaviour can be added by defining an initialize() instance method.

var fish = new Food({ name: "fish" })

fish.attributes
// => { name: "fish" }

fish.changes
// => {}

persistence(adapter, ...)

Set or get the persistence adapter for a class. The first argument is a reference to the adapter which is initialised with a reference to the class and any further arguments provided. See persistence for more.

Project.persistence(Model.REST, "/projects")

Project.persistence()
// => the initialised REST persistence adapter

pluck(attributeName)

Operates on the collection returning an array of values for the specified attribute.

Food.pluck("name")
// => ["egg", "ham", "cheese"]

Food.select(function() {
  return this.attr("name").indexOf("e") > -1
}).pluck("name")
// => ["egg", "cheese"]

remove(model)

Removes a model from a collection.

Food.all()
// => [egg, ham, cheese]

Food.remove(egg)
Food.all()
// => [ham, cheese]

reverse()

Returns a collection containing the models in reverse order.

Food.reverse().all()
// => [cheese, ham, egg]

select(func)

Operates on the collection returning a collection containing all models that match the supplied function.

Food.select(function() {
  return this.attr("name").indexOf("e") > -1
}).all()
// => [egg, cheese]

sort(func)

Acts like Array#sort() on the collection. It’s more likely you’ll want to use sortBy() which is a far more convenient wrapper to sort().

sortBy(attributeName or func)

Returns the collection sorted by either an attribute or a custom function.

Food.sortBy("name").all()
// => [cheese, egg, ham]

Food.sortBy(function() {
  return this.attr("name").length
}).all()
// => [egg, ham, cheese]

unique_key

unique_key refers to the attribute that holds the “primary key” and defaults to "id". It’s useful when using with something like MongoDB.

Project = Model("project", function() {
  this.unique_key = "_id"
})

project = new Project({ _id: "qwerty" })
project.id()
// => "qwerty"

use(Plugin, ...)

Applies a plugin to the class.

Project = Model("project", function() {
  this.use(MyPlugin, "with", { extra: "arguments" })
})

use can also be called outside of a model’s declaration as it’s simply a class method.

Project.use(MyPlugin, "with", { extra: "arguments" })

Instance properties

attr()

Get and set a model’s attribute(s). attr() can be used in a few ways:

Attributes modified using attr() can be reverted — see changes for more information.

var project = new Project({ title: "Foo", category: "Stuff" })

// Get attribute
project.attr("title")
// => "Foo"

// Set attribute
project.attr("title", "Bar")

// Get attribute again
project.attr("title")
// => "Bar"

// Chain setters
project.attr("title", "Baz").attr("category", "Nonsense")

// Set multiple attributes
project.attr({
  title: "Foo again",
  tags: "stuff nonsense"
})

// Get all attributes
project.attr()
// => { title: "Foo again", category: "Nonsense", tags: "stuff nonsense" }

attributes

Direct access to a model’s attributes object. Most of the time you won’t need to use this and should use attr() instead.

var project = new Project({ title: "Foo" })
project.attributes
// => { title: "Foo" }

changes

Attributes set with the attr() method are written to the changes intermediary object rather than directly to the attributes object. This allows you to see any previous attribute values and enables validations — see validate() for more on validations. changes are committed to attributes on successful save().

var project = new Project({ title: "Foo" })
project.attributes             // => { title: "Foo" }
project.changes                // => {}

// Change title
project.attr("title", "Bar")
project.attributes             // => { title: "Foo" }
project.changes                // => { title: "Bar" }
project.attr("title")          // => "Bar"

// Change it back to what it was
project.attr("title", "Foo")
project.attributes             // => { title: "Foo" }
project.changes                // => {}

// Change title again and reset changes
project.attr("title", "Bar")
project.attributes             // => { title: "Foo" }
project.changes                // => { title: "Bar" }
project.reset()
project.changes                // => {}

destroy(callback)

Removes the model from the collection and calls destroy() on the persistence adapter if one is defined.

Food.all()
// => [egg, ham, cheese]

ham.destroy()

Food.all()
// => [egg, cheese]

errors

Returns an Errors object containing information about any failed validations — similar to ActiveRecord’s Errors object. See Errors for more information.

id()

Convenience method, equivalent of calling attr("id").

initialize()

If an initialize() instance method is defined on a class it is called at the end of the initialization process.

var User = Model("user", function() {
  this.include({
    initialize: function() {
      this.attr("state", "initialized")
    }
  })
})
var user = new User()

user.attr("state")
// => "initialized"

merge(object)

Destructivly merges the given object into the attributes object. Used internally when saving and not really required for everyday use.

var User = Model("user")
var user = new User({ name: "Bob", occupation: "Taxidermist" })

user.attributes
// => { name: "Bob", occupation: "Taxidermist" }

user.merge({ occupation: "Stuffer" })
user.attributes
// => { name: "Bob", occupation: "Stuffer" }

newRecord()

If the model doesn’t have an id then it’s new. This is what js-model checks when saving to decide whether it should call create() or update() on the persistence adapter.

egg = new Food({ name: "egg" })
ham = new Food({ id: 2, name: "ham" })

egg.newRecord()   // => true
ham.newRecord()   // => false

reset()

Clears all changes and errors.

save(callback)

save() encapsulates quite a bit of functionality:

If your persistence layer returns any data this will also be merged into the attributes — this is how your server-assigned id gets assigned to the model when you use REST persistence.

Note: It’s important to understand that the callback passed to save() may take some time to be called as it may depend on a response from your server.

Food.all()
// => [egg, ham, cheese]

var fish = new Food({ name: "fish" })

fish.save(function(success) {
  if (success) {
    Food.all()
    // => [egg, ham, cheese, fish]

    fish.id()
    // => 4
  } else {
    // boo, something went wrong :(
  }
})

uid

Automatically assigned on instantiation, this is a per-page-load-unique id — used by the localStorage persistence adapter.

valid()

Calls validate() and checks for the existence of any errors returning true or false. Used by save() which won’t continue if valid() returns false.

validate()

Overwrite this method to add client-side validations to your model. This method is called on save() which won’t continue if the errors object is not empty.

var Project = Model("project", function() {
  this.include({
    validate: function() {
      if (this.attr("title") != "Bar") {
        this.errors.add("title", "should be Bar")
      }
    }
  })
})

var project = new Project({ title: "Foo" })
project.valid()
// => false

project.attr("title", "Bar")
project.valid()
// => true

Errors

Errors are used in conjunction with validate() and are modelled after ActiveModel’s errors.

Note: If you’re using Rails 2.x you can get Rails 3-style errors.to_json by dropping this simple monkey patch into an initializer (Gist).

module ActiveRecord
  class Errors
    def to_json
      inject({}) { |hash, error|
        attribute, message = error

        hash[attribute] ||= []
        hash[attribute] << message
        hash
      }.to_json
    end
  end
end

add(attributeName, errorMessage)

Add an error message for the specified attribute

project.errors.on("title")
// => []

project.errors
  .add("title", "should not be blank")
  .add("title", "should be Bar")

project.errors.on("title")
// => ["should not be blank", "should be Bar"]

all()

Returns an object containing all the errors.

project.errors.all()
// => { title: ["should not be blank", "should be Bar"] }

clear()

Clears all error messages (making the model valid once more).

project.valid()
// => false

project.errors.clear()

project.valid()
// => true

each(func)

Iterate over all error messages.

project.errors.each(function(attribute, message) {
  // display error messages somewhere
})

on(attributeName)

Return an array of error messages for the specified attribute.

project.errors.on("title")
// => ["should not be blank", "should be Bar"]

project.errors.on("foo")
// => []

size()

Returns a count of the total error messages on the model.

project.errors.size()
// => 2

Persistence interface

Persistence adapters implement CRUD and return an object with the following interface. You probably don’t need to know this but is documented here in case you want to implement your own.

create(model, callback)

Calls the supplied callback with a boolean indicating whether the action was a success or not and any further parameters that the persistence adapter sends.

Project.persistence().create(project, function(success) {
  // do something...
})

destroy(model, callback)

Calls the supplied callback with a boolean indicating whether the action was a success or not and any further parameters that the persistence adapter sends.

Project.persistence().destroy(project, function(success) {
  // do something...
})

read(callback)

Calls the supplied callback with an array of models — models are not automatically added to the collection when calling read(). You probably won’t need to use this much as this functionality is taken care of by load().

Project.persistence().read(function(models) {
  // do something with the models...
})

update(model, callback)

Calls the supplied callback with a boolean indicating whether the action was a success or not and any further parameters that the persistence adapter sends.

Project.persistence().update(project, function(success) {
  // do something...
})