diff --git a/README.markdown b/README.markdown index 72684c3..48d153f 100644 --- a/README.markdown +++ b/README.markdown @@ -1,27 +1,27 @@ -# js-model - -js-model is a library that allows you to work with models in your JavaScript. - -## In brief - -Declare a model class: - - var Todo = Model("todo") - -Now create and manipulate model instances: - - var todo = new Todo({ text: "do it" }) - todo.attr("when", "now") - todo.save() - -## Documentation / download / more - -For notes on [getting started](http://benpickles.github.com/js-model/#getting-started), [persisting via REST or localStorage](http://benpickles.github.com/js-model/#persistence), [using with Sammy](http://benpickles.github.com/js-model/#js-model-hearts-sammy), [API documentation](http://benpickles.github.com/js-model/#api) and download links visit the [js-model docs site](http://benpickles.github.com/js-model/). - -## Suggestions / questions / issues / comments - -Feel free to use [Github issues](http://github.com/benpickles/js-model/issues) for any of the above. - -## Copyright - -Copyright 2010-2012 [Ben Pickles](http://benpickles.com/). See [LICENSE](http://github.com/benpickles/js-model/blob/master/LICENSE) for details. +# js-model + +js-model is a library that allows you to work with models in your JavaScript. + +## In brief + +Declare a model class: + + var Todo = Model("todo") + +Now create and manipulate model instances: + + var todo = new Todo({ text: "do it" }) + todo.set("when", "now") + todo.save() + +## Documentation / download / more + +For notes on [getting started](http://benpickles.github.com/js-model/#getting-started), [persisting via REST or localStorage](http://benpickles.github.com/js-model/#persistence), [using with Sammy](http://benpickles.github.com/js-model/#js-model-hearts-sammy), [API documentation](http://benpickles.github.com/js-model/#api) and download links visit the [js-model docs site](http://benpickles.github.com/js-model/). + +## Suggestions / questions / issues / comments + +Feel free to use [Github issues](http://github.com/benpickles/js-model/issues) for any of the above. + +## Copyright + +Copyright 2010-2012 [Ben Pickles](http://benpickles.com/). See [LICENSE](http://github.com/benpickles/js-model/blob/master/LICENSE) for details. diff --git a/docs/class_properties.md b/docs/class_properties.md index 52c9017..665519e 100644 --- a/docs/class_properties.md +++ b/docs/class_properties.md @@ -1,101 +1,10 @@ -### Class properties +### Static properties -#### `add(model)` - -Adds a model to a collection and is what [`save()`](#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`](#id) or [`uid`](#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()`](#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] +When you factory a new model using [`Model()`](#model) the returned constructor includes the following static properties. #### `find(id)` -Returns the model with the corresponding id. +Convenience method to return the model with the corresponding id. Food.find(2) // => ham @@ -103,86 +12,6 @@ Returns the model with the corresponding id. 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](#initialize) 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()`](#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. @@ -191,22 +20,6 @@ Calls [`read()`](#read) on the persistence adapter and adds the returned 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`](#attributes). Custom initialization behaviour can be added by defining an [`initialize()`](#initialize) instance method. @@ -219,70 +32,6 @@ Instantiates a model, the supplied attributes get assigned directly to the model 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](#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()`](#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. diff --git a/docs/collection.md b/docs/collection.md new file mode 100644 index 0000000..5eb7958 --- /dev/null +++ b/docs/collection.md @@ -0,0 +1,120 @@ +### Collection + +A Collection is a decorated array that includes the following standard array methods: + +`every()`, `filter()`, `forEach()`, `indexOf()`, `lastIndexOf()`, `map()`, `pop()`, `push()`, `reverse()`, `shift()`, `slice()`, `some()`, `sort()`, `splice()`, `unshift()` + +These methods are assumed to be present on the `Array.prototype` and therefore need to be polyfilled if support older browsers is required – in which case I suggest [Augment.js](http://augmentjs.com/). + +#### `add(model)` + +Adds a model to a collection and won’t allow you to add the same model to a collection more than once. An “add” event is emitted by the collection when a model is successfully added to it. + + Food.collection + // => [] + + 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.collection.add(egg) + Food.collection + // => [egg] + + Food.collection.add(ham) + Food.collection.add(cheese) + Food.collection + // => [egg, ham, cheese] + + Food.collection.add(egg) + Food.collection + // => [egg, ham, cheese] + +#### `at(index)` + +Returns the model at the specified index. + + Food.collection.at(1) + // => ham + +#### `detect(func[, context])` + +Returns the first model that matches the supplied function. + + Food.collection.detect(function(model) { + return model.get("name") == "ham" + }) + // => ham + +#### `first()` + +Returns the first model in the collection. + + Food.collection.first() + // => egg + + Food.collection.filter(function(model) { + return model.get("name").indexOf("h") > -1 + }).first() + // => ham + +#### `last()` + +Returns the last model in the collection. + + Food.collection.last() + // => cheese + +#### `length` + +Returns the size of the collection, just like an array. + + Food.collection.length + // => 3 + + Food.collection.filter(function(model) { + return ~model.get("name").indexOf("e") + }).length + +#### `pluck(attribute)` + +Returns an array of values for the specified attribute. + + Food.collection.pluck("name") + // => ["egg", "ham", "cheese"] + + Food.collection.filter(function(model) { + return model.get("name").indexOf("e") > -1 + }).pluck("name") + // => ["egg", "cheese"] + +#### `remove(model)` + +Removes a model from a collection. A “remove” event is emitted by the collection when a model is removed from it. + + Food.collection + // => [egg, ham, cheese] + + Food.collection.remove(egg) + Food.collection + // => [ham, cheese] + +#### `sortBy(attribute || func)` + +Returns the collection sorted by either an attribute or a custom function. + + Food.collection.sortBy("name") + // => [cheese, egg, ham] + + Food.collection.sortBy(function(model) { + return model.get("name").length + }) + // => [egg, ham, cheese] + +#### `toArray()` + +Returns an array + +#### `toJSON()` + +Returns an array of its models' attributes by calling [`toJSON()`](#tojson) on each one. diff --git a/docs/getting_started.md b/docs/getting_started.md index 0311d5c..e5d2bd8 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -8,10 +8,10 @@ This allows you to create instances of "project" models and also contains an int ### Manipulating objects -Now you can create and manipulate instances of your new model. Attributes are read and set with the [`attr()`](#attr) method which works in a similar way to jQuery on the DOM. +Now you can create and manipulate instances of your new model. Attributes are read and set with the [`get()`](#get) and [`set()`](#set) methods. var project = new Project({ id: 1, title: "stuff" }) - project.attr("title", "nonsense") + project.set("title", "nonsense") project.save() ### Finding objects @@ -41,15 +41,15 @@ When [creating a model](#model) you can pass a function as the optional second a var Project = Model("project", function() { this.extend({ - find_by_title: function(title) { + findByTitle: function(title) { return this.detect(function() { - return this.attr("title") == title + return this.get("title") == title }) } }) }) - Project.find_by_title("stuff") + Project.findByTitle("stuff") // => "stuff" project model #### Instance properties @@ -59,7 +59,7 @@ You can also "[include](#include)" instance methods on the model's prototype. Th var Project = Model("project", function() { this.include({ markAsDone: function() { - this.attr("done", true) + this.set("done", true) } }) }) @@ -74,10 +74,10 @@ Simple associations can be mimicked by adding a couple of instance methods. Here var Cat = Model("cat", function() { this.include({ mat: function() { - var mat_id = this.attr("mat_id") + var matID = this.get("mat_id") return Mat.detect(function() { - return this.id() == mat_id + return this.id() == matID }) } }) @@ -89,7 +89,7 @@ Simple associations can be mimicked by adding a couple of instance methods. Here var id = this.id() return Cat.select(function() { - return this.attr("mat_id") == id + return this.get("mat_id") == id }) } }) @@ -104,11 +104,11 @@ js-model allows you to listen to the lifecycle of objects based on the events th 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) + addObjectToUI(new_object) }) Project.bind("remove", function(removed_object) { - remove_object_from_ui(removed_object) + removeObjectFromUI(removed_object) }) #### Instance events @@ -118,13 +118,13 @@ Parts of your application can be bound to changes which happen to a specific ins var project = Project.first() project.bind("update", function() { - my_ui_elem.text(this.attr("name")) + myUIElement.text(this.get("name")) }) Including when the instance is destroyed: project.bind("destroy", function() { - my_ui_elem.remove() + myUIElement.remove() }) #### Custom events @@ -132,7 +132,7 @@ Including when the instance is destroyed: 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") + myUIElement.css("background", "blue") }) project.trigger("turn_blue") diff --git a/docs/instance_properties.md b/docs/instance_properties.md index eb0f79b..bf2ab29 100644 --- a/docs/instance_properties.md +++ b/docs/instance_properties.md @@ -1,45 +1,8 @@ -### 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`](#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" } +### Model #### `attributes` -Direct access to a model's attributes object. Most of the time you won't need to use this and should use [`attr()`](#attr) instead. +Direct access to a model's attributes object. Most of the time you won't need to use this and should use [`get()`](#get) and [`set()`](#set) instead. var project = new Project({ title: "Foo" }) project.attributes @@ -47,25 +10,25 @@ Direct access to a model's attributes object. Most of the time you won't need to #### `changes` -Attributes set with the [`attr()`](#attr) method are written to the `changes` intermediary object rather than directly to the [`attributes`](#attributes) object. This allows you to see any previous attribute values and enables validations -- see [`validate()`](#validate) for more on validations. `changes` are committed to [`attributes`](#attributes) on successful [`save()`](#save). +Attributes set with the [`set()`](#set) method are written to the `changes` intermediary object rather than directly to the [`attributes`](#attributes) object. This allows you to see any previous attribute values and enables validations -- see [`validate()`](#validate) for more on validations. `changes` are committed to [`attributes`](#attributes) on successful [`save()`](#save). var project = new Project({ title: "Foo" }) project.attributes // => { title: "Foo" } project.changes // => {} // Change title - project.attr("title", "Bar") + project.set("title", "Bar") project.attributes // => { title: "Foo" } project.changes // => { title: "Bar" } - project.attr("title") // => "Bar" + project.get("title") // => "Bar" // Change it back to what it was - project.attr("title", "Foo") + project.set("title", "Foo") project.attributes // => { title: "Foo" } project.changes // => {} // Change title again and reset changes - project.attr("title", "Bar") + project.set("title", "Bar") project.attributes // => { title: "Foo" } project.changes // => { title: "Bar" } project.reset() @@ -87,9 +50,23 @@ Removes the model from the collection and calls [`destroy()`](#destroy) on the p Returns an [`Errors`](#api-errors) object containing information about any failed validations -- similar to ActiveRecord's Errors object. See [`Errors`](#api-errors) for more information. +#### `get([name])` + +Returns a model's attribute(s). Can be used to fetch either a specific attribute by passing the attribute name or all name/value attribute pairs by passing nothing. + + var project = new Project({ title: "Foo", category: "Stuff" }) + + // Get attribute + project.get("title") + // => "Foo" + + // Get all attributes + project.get() + // => { title: "Foo", category: "Stuff" } + #### `id()` -Convenience method, equivalent of calling `attr("id")`. +Convenience method, equivalent of calling `get("id")`. #### `initialize()` @@ -98,29 +75,15 @@ If an `initialize()` instance method is defined on a class it is called at the e var User = Model("user", function() { this.include({ initialize: function() { - this.attr("state", "initialized") + this.set("state", "initialized") } }) }) var user = new User() - user.attr("state") + user.get("state") // => "initialized" -#### `merge(object)` - -Destructivly merges the given object into the [`attributes`](#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()`](#create) or [`update()`](#update) on the persistence adapter. @@ -165,6 +128,36 @@ If your persistence layer returns any data this will also be [merged](#merge) in } }) +#### `set(name, value | attributes)` + +Set a model's attribute(s) passing either a name/value pair or an object containing multiple. + +Attributes modified using `set()` can be reverted -- see [`changes`](#changes) for more information. + + var project = new Project({ title: "Foo", category: "Stuff" }) + + // Set attribute + project.set("title", "Bar") + + // Chain setters + project + .set("title", "Baz") + .set("category", "Nonsense") + + // Set multiple attributes + project.set({ + title: "Foo again", + tags: "stuff nonsense" + }) + + // Get all attributes + project.get() + // => { title: "Foo again", category: "Nonsense", tags: "stuff nonsense" } + +#### `toJSON()` + +Hook to allow `JSON.stringify(myModel)` to work, returns a clone of the model's attributes. + #### `uid` Automatically assigned on instantiation, this is a per-page-load-unique id -- used by the [localStorage persistence adapter](#localstorage). @@ -180,7 +173,7 @@ Overwrite this method to add client-side validations to your model. This method var Project = Model("project", function() { this.include({ validate: function() { - if (this.attr("title") != "Bar") { + if (this.get("title") != "Bar") { this.errors.add("title", "should be Bar") } } @@ -191,6 +184,6 @@ Overwrite this method to add client-side validations to your model. This method project.valid() // => false - project.attr("title", "Bar") + project.set("title", "Bar") project.valid() // => true diff --git a/docs/lib/docs.rb b/docs/lib/docs.rb index b7270f3..0c8aaed 100644 --- a/docs/lib/docs.rb +++ b/docs/lib/docs.rb @@ -18,6 +18,7 @@ def build markdown('model') + markdown('class_properties') + markdown('instance_properties') + + markdown('collection') + markdown('errors') + markdown('persistence_interface') ] diff --git a/docs/model.md b/docs/model.md index e9f89b9..c248e52 100644 --- a/docs/model.md +++ b/docs/model.md @@ -8,36 +8,36 @@ The optional function is used to define custom methods and properties on your ne var Project = Model("project", function() { this.extend({ - find_by_title: function(title) { + findByTitle: function(title) { return this.detect(function() { - return this.attr("title") == title + return this.get("title") == title }) } }) this.include({ markAsDone: function() { - this.attr("done", true) + this.set("done", true) } }) }) - Project.find_by_title("stuff").markAsDone() + Project.findByTitle("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) { + klass.findByTitle = function(title) { return this.detect(function() { - return this.attr("title") == title + return this.get("title") == title }) } proto.markAsDone = function() { - this.attr("done", true) + this.set("done", true) } }) - Project.find_by_title("stuff").markAsDone() + Project.findByTitle("stuff").markAsDone() // "stuff" project marked as done diff --git a/docs/persistence.md b/docs/persistence.md index 27c0d4c..fb36b55 100644 --- a/docs/persistence.md +++ b/docs/persistence.md @@ -16,7 +16,7 @@ Calling [`save()`](#save) or [`destroy()`](#destroy) on an object now fires a co var project = new Project({ name: "stuff" }) project.save() // POST /projects - project.attr("name", "nonsense").save() // PUT /projects/1 + project.set("name", "nonsense").save() // PUT /projects/1 project.destroy() // DELETE /projects/1 When responding to POST or PUT requests any JSON returned will be [merged](#merge) into the model's [`attributes`](#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`](#api-errors). diff --git a/docs/persistence_interface.md b/docs/persistence_interface.md index deaa381..8b91a34 100644 --- a/docs/persistence_interface.md +++ b/docs/persistence_interface.md @@ -2,11 +2,11 @@ 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)` +#### `save(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) { + Project.persistence().save(project, function(success) { // do something... }) @@ -25,11 +25,3 @@ Calls the supplied callback with an array of models -- models are **not** automa 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... - }) diff --git a/docs/sammy.md b/docs/sammy.md index d636bae..47c528b 100644 --- a/docs/sammy.md +++ b/docs/sammy.md @@ -26,7 +26,7 @@ js-model works really well with Sammy -- you *are* using [Sammy](http://code.qui this.put("#/projects/:id", function(context) { var project = Project.find(this.params.id) - project.attr(this.param.project) + project.set(this.param.project) .save(function(success) { if (success) { context.redirect("#/projects/" + project.id()) diff --git a/index.html b/index.html index a5c3f49..6fd9fd4 100644 --- a/index.html +++ b/index.html @@ -25,9 +25,9 @@
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.
Now you can create and manipulate instances of your new model. Attributes are read and set with the get() and set() methods.
var project = new Project({ id: 1, title: "stuff" })
-project.attr("title", "nonsense")
+project.set("title", "nonsense")
project.save()
var Project = Model("project", function() {
this.include({
markAsDone: function() {
- this.attr("done", true)
+ this.set("done", true)
}
})
})
@@ -206,7 +203,7 @@ Associations
var Cat = Model("cat", function() {
this.include({
mat: function() {
- var mat_id = this.attr("mat_id")
+ var mat_id = this.get("mat_id")
return Mat.detect(function() {
return this.id() == mat_id
@@ -221,7 +218,7 @@ Associations
var id = this.id()
return Cat.select(function() {
- return this.attr("mat_id") == id
+ return this.get("mat_id") == id
})
}
})
@@ -250,7 +247,7 @@ Instance events
var project = Project.first()
project.bind("update", function() {
- my_ui_elem.text(this.attr("name"))
+ my_ui_elem.text(this.get("name"))
})
Including when the instance is destroyed:
@@ -290,7 +287,7 @@ REST
var project = new Project({ name: "stuff" })
project.save() // POST /projects
-project.attr("name", "nonsense").save() // PUT /projects/1
+project.set("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.
@@ -345,7 +342,7 @@ Loading data
this.put("#/projects/:id", function(context) {
var project = Project.find(this.params.id)
- project.attr(this.param.project)
+ project.set(this.param.project)
.save(function(success) {
if (success) {
context.redirect("#/projects/" + project.id())
@@ -375,14 +372,14 @@ Model(name, func)
this.extend({
find_by_title: function(title) {
return this.detect(function() {
- return this.attr("title") == title
+ return this.get("title") == title
})
}
})
this.include({
markAsDone: function() {
- this.attr("done", true)
+ this.set("done", true)
}
})
})
@@ -395,115 +392,24 @@ Model(name, func)
var Project = Model("project", function(klass, proto) {
klass.find_by_title = function(title) {
return this.detect(function() {
- return this.attr("title") == title
+ return this.get("title") == title
})
}
proto.markAsDone = function() {
- this.attr("done", true)
+ this.set("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" })
+Static properties
-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]
+When you factory a new model using Model() the returned constructor includes the following static properties.
find(id)
-Returns the model with the corresponding id.
+Convenience method to return the model with the corresponding id.
Food.find(2)
// => ham
@@ -511,86 +417,6 @@ find(id)
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.
@@ -599,22 +425,6 @@ load(callback)
// 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.
@@ -627,72 +437,6 @@ new(attributes)
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_keyunique_key refers to the attribute that holds the “primary key” and defaults to "id". It’s useful when using with something like MongoDB.
use(Plugin, ...)use can also be called outside of a model’s declaration as it’s simply a class method.
Project.use(MyPlugin, "with", { extra: "arguments" })
-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" }
+attributesDirect access to a model’s attributes object. Most of the time you won’t need to use this and should use attr() instead.
Direct access to a model’s attributes object. Most of the time you won’t need to use this and should use get() and set() instead.
var project = new Project({ title: "Foo" })
project.attributes
@@ -770,25 +472,25 @@ attributes
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().
+Attributes set with the set() 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.set("title", "Bar")
project.attributes // => { title: "Foo" }
project.changes // => { title: "Bar" }
-project.attr("title") // => "Bar"
+project.get("title") // => "Bar"
// Change it back to what it was
-project.attr("title", "Foo")
+project.set("title", "Foo")
project.attributes // => { title: "Foo" }
project.changes // => {}
// Change title again and reset changes
-project.attr("title", "Bar")
+project.set("title", "Bar")
project.attributes // => { title: "Foo" }
project.changes // => { title: "Bar" }
project.reset()
@@ -810,9 +512,23 @@ errors
Returns an Errors object containing information about any failed validations — similar to ActiveRecord’s Errors object. See Errors for more information.
+get([name])
+
+Returns a model’s attribute(s). Can be used to fetch either a specific attribute by passing the attribute name or all name/value attribute pairs by passing nothing.
+
+var project = new Project({ title: "Foo", category: "Stuff" })
+
+// Get attribute
+project.get("title")
+// => "Foo"
+
+// Get all attributes
+project.get()
+// => { title: "Foo", category: "Stuff" }
+
id()
-Convenience method, equivalent of calling attr("id").
+Convenience method, equivalent of calling get("id").
initialize()
@@ -821,29 +537,15 @@ initialize()
var User = Model("user", function() {
this.include({
initialize: function() {
- this.attr("state", "initialized")
+ this.set("state", "initialized")
}
})
})
var user = new User()
-user.attr("state")
+user.get("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.
@@ -889,6 +591,36 @@ save(callback)
}
})
+set(name, value | attributes)
+
+Set a model’s attribute(s) passing either a name/value pair or an object containing multiple.
+
+Attributes modified using set() can be reverted — see changes for more information.
+
+var project = new Project({ title: "Foo", category: "Stuff" })
+
+// Set attribute
+project.set("title", "Bar")
+
+// Chain setters
+project
+ .set("title", "Baz")
+ .set("category", "Nonsense")
+
+// Set multiple attributes
+project.set({
+ title: "Foo again",
+ tags: "stuff nonsense"
+})
+
+// Get all attributes
+project.get()
+// => { title: "Foo again", category: "Nonsense", tags: "stuff nonsense" }
+
+toJSON()
+
+Hook to allow JSON.stringify(myModel) to work, returns a clone of the model’s attributes.
+
uid
Automatically assigned on instantiation, this is a per-page-load-unique id — used by the localStorage persistence adapter.
@@ -904,7 +636,7 @@ validate()
var Project = Model("project", function() {
this.include({
validate: function() {
- if (this.attr("title") != "Bar") {
+ if (this.get("title") != "Bar") {
this.errors.add("title", "should be Bar")
}
}
@@ -915,9 +647,128 @@ validate()
project.valid()
// => false
-project.attr("title", "Bar")
+project.set("title", "Bar")
project.valid()
// => true
+Collection
+
+A Collection is a decorated array that includes the following standard array methods:
+
+every(), filter(), forEach(), indexOf(), lastIndexOf(), map(), pop(), push(), reverse(), shift(), slice(), some(), sort(), splice(), unshift()
+
+These methods are assumed to be present on the Array.prototype and therefore need to be polyfilled if support older browsers is required – in which case I suggest Augment.js.
+
+add(model)
+
+Adds a model to a collection and won’t allow you to add the same model to a collection more than once. An “add” event is emitted by the collection when a model is successfully added to it.
+
+Food.collection
+// => []
+
+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.collection.add(egg)
+Food.collection
+// => [egg]
+
+Food.collection.add(ham).add(cheese)
+Food.collection
+// => [egg, ham, cheese]
+
+Food.collection.add(egg)
+Food.collection
+// => [egg, ham, cheese]
+
+at(index)
+
+Returns the model at the specified index.
+
+Food.collection.at(1)
+// => ham
+
+detect(func[, context])
+
+Returns the first model that matches the supplied function.
+
+Food.collection.detect(function(model) {
+ return model.get("name") == "ham"
+})
+// => ham
+
+first()
+
+Returns the first model in the collection.
+
+Food.collection.first()
+// => egg
+
+Food.collection.filter(function(model) {
+ return model.get("name").indexOf("h") > -1
+}).first()
+// => ham
+
+last()
+
+Returns the last model in the collection.
+
+Food.collection.last()
+// => cheese
+
+length
+
+Returns the size of the collection, just like an array.
+
+Food.collection.length
+// => 3
+
+Food.collection.filter(function(model) {
+ return ~model.get("name").indexOf("e")
+}).length
+
+pluck(attribute)
+
+Returns an array of values for the specified attribute.
+
+Food.collection.pluck("name")
+// => ["egg", "ham", "cheese"]
+
+Food.collection.filter(function(model) {
+ return model.get("name").indexOf("e") > -1
+}).pluck("name")
+// => ["egg", "cheese"]
+
+remove(model)
+
+Removes a model from a collection. A “remove” event is emitted by the collection when a model is removed from it.
+
+Food.collection
+// => [egg, ham, cheese]
+
+Food.collection.remove(egg)
+Food.collection
+// => [ham, cheese]
+
+sortBy(attribute || func)
+
+Returns the collection sorted by either an attribute or a custom function.
+
+Food.collection.sortBy("name")
+// => [cheese, egg, ham]
+
+Food.collection.sortBy(function(model) {
+ return model.get("name").length
+})
+// => [egg, ham, cheese]
+
+toArray()
+
+Returns an array
+
+toJSON()
+
+Returns an array of its models' attributes by calling toJSON() on each one.
Errors
Errors are used in conjunction with validate() and are modelled after ActiveModel’s errors.
@@ -952,7 +803,7 @@ add(attributeName, errorMessage)
project.errors.on("title")
// => ["should not be blank", "should be Bar"]
-all()all()Returns an object containing all the errors.
@@ -971,7 +822,7 @@clear()each(func)each(func)Iterate over all error messages.
@@ -999,11 +850,11 @@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)save(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) {
+Project.persistence().save(project, function(success) {
// do something...
})
@@ -1022,14 +873,6 @@ read(callback)
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...
-})
diff --git a/src/collection.js b/src/collection.js
index fcc1d25..e935ce0 100644
--- a/src/collection.js
+++ b/src/collection.js
@@ -41,9 +41,10 @@
, value
if (enumerable) {
- // Ensure enumerable method callbacks are passed this collection as
- // as the third argument instead of the `this.models` array.
value = models[name](function() {
+ // Ensure enumerable method callbacks are passed this collection
+ // as the third argument instead of the collections internal
+ // `models` array.
arguments[2] = self
return callback.apply(this, arguments)
}, context)
diff --git a/src/model_model.js b/src/model_model.js
index 0c75387..80aa15e 100644
--- a/src/model_model.js
+++ b/src/model_model.js
@@ -32,17 +32,8 @@
}
Model.Model.prototype = {
- destroy: function(callback) {
- var self = this
-
- this.constructor.persistence.destroy(this, function(success) {
- if (success) {
- self.emit("destroy", self)
- }
-
- if (callback) callback.apply(this, arguments)
- })
-
+ destroy: function() {
+ this.emit("destroy", this)
return this
},
diff --git a/src/null_persistence.js b/src/null_persistence.js
index 5a6f88b..9f48774 100644
--- a/src/null_persistence.js
+++ b/src/null_persistence.js
@@ -1,17 +1,17 @@
-Model.NullPersistence = {
- destroy: function(model, callback) {
- callback(true)
- },
-
- newRecord: function(model) {
- return false
- },
-
- read: function(callback) {
- callback([])
- },
-
- save: function(model, callback) {
- callback(true)
- }
-}
+Model.NullPersistence = {
+ destroy: function(model, callback) {
+ callback(true)
+ },
+
+ newRecord: function(model) {
+ return false
+ },
+
+ read: function(callback) {
+ callback([])
+ },
+
+ save: function(model, callback) {
+ callback(true)
+ }
+}