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” Cat
s.
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:
-
attr(name)
— Get the value of the named attribute. -
attr(name, value)
— Set the value of the named attribute. -
attr()
— Get an object containing all name/value attribute pairs. -
attr(object)
— Set multiple name/value attribute pairs.
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:
- Check whether the model is
valid()
, if not then halt here passing the callbackfalse
. - If the model is new then call
create()
on the persistence adapter otherwise callupdate()
. - If the persistence call is successful then
merge()
anychanges
intoattributes
andadd()
the model to the collection if it’s new. - Finally the supplied callback is called with a boolean to indicate success/failure and any further arguments the persistence adapter supplies.
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...
})