├── tests ├── index.js └── backbone.js ├── .travis.yml ├── .jshintrc ├── .gitignore ├── LICENSE ├── package.json ├── demo.js ├── ampersand-io-model.js └── README.md /tests/index.js: -------------------------------------------------------------------------------- 1 | require('./backbone'); -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | before_install: 5 | - "export DISPLAY=:99.0" 6 | - "sh -e /etc/init.d/xvfb start" -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true, 3 | "camelcase": true, 4 | "freeze": true, 5 | "indent": 2, 6 | "newcap": true, 7 | "noempty": true, 8 | "quotmark": "single", 9 | "undef": true, 10 | "unused": false, 11 | "maxdepth": 3, 12 | 13 | "asi": false, 14 | "expr": true, 15 | 16 | "node": true, 17 | "browser": true 18 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 João Antunes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ampersand-io-model", 3 | "version": "0.3.1", 4 | "description": "Ampersand module based on ampersand-model to be used with socket.io", 5 | "main": "ampersand-io-model.js", 6 | "scripts": { 7 | "test": "browserify tests | tape-run | tap-spec", 8 | "lint": "node_modules/.bin/jshint -c .jshintrc ampersand-io-model.js" 9 | }, 10 | "pre-commit": [ 11 | "lint", 12 | "test" 13 | ], 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/sinfo/ampersand-io-model" 17 | }, 18 | "keywords": [ 19 | "ampersand", 20 | "js", 21 | "model", 22 | "io", 23 | "socket", 24 | "realtime", 25 | "state" 26 | ], 27 | "author": "Joao Antunes (http://github.com/JGAntunes)", 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/sinfo/ampersand-io-model/issues" 31 | }, 32 | "homepage": "https://github.com/sinfo/ampersand-io-model", 33 | "dependencies": { 34 | "ampersand-io": "^0.4.2", 35 | "ampersand-state": "^4.5.3", 36 | "underscore": "^1.8.3" 37 | }, 38 | "devDependencies": { 39 | "browserify": "^9.0.8", 40 | "jshint": "^2.7.0", 41 | "pre-commit": "^1.0.6", 42 | "socket.io": "^1.3.5", 43 | "tap-spec": "^3.0.0", 44 | "tape": "^4.0.0", 45 | "tape-run": "^1.0.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /demo.js: -------------------------------------------------------------------------------- 1 | var io = require('socket.io')(); 2 | var State = require('ampersand-state'); 3 | var IOModel = require('./ampersand-io-model'); 4 | var client = require('socket.io-client'); 5 | 6 | var bar = io.of('/bar'); 7 | 8 | var foo = io.of('/foo'); 9 | 10 | io.on('connection', function(socket){ 11 | 12 | console.log('Test client connected!'); 13 | 14 | socket.on('model-create', function(data, cb){ 15 | console.log(data); 16 | cb(); 17 | }); 18 | 19 | socket.on('model-update', function(data, cb){ 20 | console.log(data); 21 | cb(); 22 | }); 23 | 24 | socket.on('model-fetch', function(data, cb){ 25 | console.log('about to emit'); 26 | socket.emit('fetch-response', {test: 'test'}, function(){console.log('done');}); 27 | console.log(data); 28 | cb(); 29 | }); 30 | 31 | socket.on('model-remove', function(data, cb){ 32 | console.log(data); 33 | cb(); 34 | }); 35 | }); 36 | io.listen(3000); 37 | 38 | bar.on('connection', function(socket){ 39 | 40 | console.log('Test bar client connected!'); 41 | 42 | socket.on('model-fetch', function(data, cb){ 43 | console.log('about to emit bar'); 44 | socket.emit('fetch-response', {test: 'test'}, function(){console.log('done bar');}); 45 | console.log(data); 46 | cb(); 47 | }); 48 | 49 | }); 50 | 51 | foo.on('connection', function(socket){ 52 | 53 | console.log('Test foo client connected!'); 54 | 55 | socket.on('model-fetch', function(data, cb){ 56 | console.log('about to emit foo'); 57 | socket.emit('fetch-response', {data: {id: 'fooModel', thread: 'test', source: 'test2', member: 'mymember'}}, function(){console.log('done foo');}); 58 | console.log(data); 59 | cb(); 60 | }); 61 | 62 | }); 63 | 64 | var mymodelFoo = new (IOModel.extend({ 65 | props: { 66 | id: ['string'], 67 | thread: ['string'], 68 | source: ['string'], 69 | member: ['string'] 70 | } 71 | }))({}, {socket: 'http://localhost:3000/foo'}); 72 | 73 | var mymodelBar = new (IOModel.extend({ 74 | props: { 75 | id: ['string'], 76 | thread: ['string'], 77 | source: ['string'], 78 | member: ['string'] 79 | } 80 | }))({}, {socket: 'http://localhost:3000/bar'}); 81 | 82 | mymodelBar.save({id: 'barModel'}); 83 | mymodelBar.fetch({callback: function(){ 84 | console.log(mymodelBar.thread, mymodelBar.source); 85 | }}); 86 | mymodelBar.destroy(); 87 | 88 | mymodelFoo.save({id: 'fooModel'}); 89 | mymodelFoo.fetch({callback: function(){ 90 | console.log(mymodelFoo.thread, mymodelFoo.source); 91 | }}); 92 | mymodelFoo.destroy(); 93 | 94 | /*var barClient = client('http://localhost:3000/bar'); 95 | var fooClient = client('http://localhost:3000/foo'); 96 | 97 | barClient.emit('model-fetch', {test: 1}, function(){console.log('stuff');}); 98 | 99 | fooClient.emit('model-fetch', {test: 2}, function(){console.log('stuffCenas');});*/ 100 | -------------------------------------------------------------------------------- /ampersand-io-model.js: -------------------------------------------------------------------------------- 1 | /*$AMPERSAND_VERSION*/ 2 | var _ = require('underscore'); 3 | var AmpersandState = require('ampersand-state'); 4 | var AmpersandIO = require('ampersand-io'); 5 | 6 | var events = { 7 | onFetch: 'fetch-response', 8 | create: 'model-create', 9 | update: 'model-update', 10 | fetch: 'model-fetch', 11 | remove: 'model-remove' 12 | }; 13 | 14 | var AmpersandIOModel = function (attrs, options){ 15 | options || (options = {}); 16 | Base.call(this, attrs, options); 17 | AmpersandIO.call(this, options.socket, options); 18 | }; 19 | 20 | var IOMixin = AmpersandIO.extend({ 21 | 22 | events: events, 23 | 24 | save: function (key, val, options) { 25 | var attrs, event; 26 | 27 | // Handle both `"key", value` and `{key: value}` -style arguments. 28 | if (key === null || typeof key === 'object') { 29 | attrs = key; 30 | options = val; 31 | } else { 32 | (attrs = {})[key] = val; 33 | } 34 | 35 | options = _.extend({validate: true}, options); 36 | 37 | // If we're not waiting and attributes exist, save acts as 38 | // `set(attr).save(null, opts)` with validation. Otherwise, check if 39 | // the model will be valid when the attributes, if any, are set. 40 | if (attrs && !options.wait) { 41 | if (!this.set(attrs, options)){ 42 | return false; 43 | } 44 | } else { 45 | if (!this._validate(attrs, options)){ 46 | return false; 47 | } 48 | } 49 | 50 | // Set the event type 51 | event = this.isNew() ? 'create' : 'update'; 52 | 53 | if (options.parse === void 0){ 54 | options.parse = true; 55 | } 56 | var model = this; 57 | options.cb = options.callback; 58 | options.callback = function cb(err, resp){ 59 | var serverAttrs = model.parse(resp, options); 60 | if (err){ 61 | return callback(err, model, resp, options); 62 | } 63 | if (options.wait){ 64 | serverAttrs = _.extend(attrs || {}, serverAttrs); 65 | } 66 | if (_.isObject(serverAttrs) && !model.set(serverAttrs, options)) { 67 | return callback(true, model, resp, options); 68 | } 69 | callback(null, model, resp, options); 70 | }; 71 | 72 | this.emit(this.events[event], this, options); 73 | 74 | return model; 75 | }, 76 | 77 | // Fetch the model from the server. If the server's representation of the 78 | // model differs from its current attributes, they will be overridden 79 | fetch: function (options) { 80 | options = options ? _.clone(options) : {}; 81 | if (options.parse === void 0){ 82 | options.parse = true; 83 | } 84 | var model = this; 85 | options.cb = options.callback; 86 | options.callback = function (err, resp){ 87 | callback(err, model, resp, options); 88 | }; 89 | 90 | options.respCallback = function cb(response, serverCb){ 91 | model.removeListeners([model.events.onFetch]); 92 | if (response.err){ 93 | return callback(response.err, model, response.data, options, serverCb); 94 | } 95 | if (!model.set(model.parse(response.data, options), options)) { 96 | return callback(true, model, response.data, options, serverCb); 97 | } 98 | callback(null, model, response.data, options, serverCb); 99 | }; 100 | 101 | var listener = {}; 102 | listener[this.events.onFetch] = { fn: options.respCallback, active: true}; 103 | this.addListeners(listener); 104 | this.emit(this.events.fetch, this, options); 105 | 106 | return model; 107 | }, 108 | 109 | // Destroy this model on the server if it was already persisted. 110 | // Optimistically removes the model from its collection, if it has one. 111 | // If `wait: true` is passed, waits for the server to respond before removal. 112 | destroy: function (options) { 113 | options = options ? _.clone(options) : {}; 114 | var model = this; 115 | 116 | var destroy = function () { 117 | model.trigger('destroy', model, model.collection, options); 118 | }; 119 | 120 | options.cb = options.callback; 121 | options.callback = function cb(err, resp){ 122 | if (err){ 123 | return callback(err, model, resp, options); 124 | } 125 | if (options.wait || model.isNew()){ 126 | destroy(); 127 | } 128 | callback(null, model, resp, options); 129 | }; 130 | 131 | if (this.isNew()) { 132 | options.callback(); 133 | return; 134 | } 135 | 136 | this.emit(this.events.remove, this, options); 137 | if (!options.wait){ 138 | destroy(); 139 | } 140 | return model; 141 | } 142 | 143 | }); 144 | 145 | // Aux func used to trigger errors if they exist, use the optional 146 | // callback function if given and call the server ack callback if exists 147 | var callback = function(err, model, resp, options, serverCb){ 148 | !serverCb || serverCb(); 149 | if (options.cb){ 150 | options.cb(err, model, resp); 151 | } 152 | if (err){ 153 | model.trigger('error', err, model, options); 154 | } 155 | }; 156 | 157 | var Base = AmpersandState.extend(); 158 | AmpersandIOModel.prototype = Object.create(Base.prototype); 159 | _.extend(AmpersandIOModel.prototype, IOMixin.prototype); 160 | AmpersandIOModel.extend = Base.extend; 161 | 162 | module.exports = AmpersandIOModel; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/sinfo/ampersand-io-model.svg?branch=master)](https://travis-ci.org/sinfo/ampersand-io-model) 2 | [![Dependency Status](https://david-dm.org/sinfo/ampersand-io-model.svg)](https://david-dm.org/sinfo/ampersand-io-model) 3 | [![devDependency Status](https://david-dm.org/sinfo/ampersand-io-model/dev-status.svg)](https://david-dm.org/sinfo/ampersand-io-model#info=devDependencies) 4 | 5 | ampersand-io-model 6 | ================== 7 | 8 | Based on [ampersand-model](https://github.com/AmpersandJS/ampersand-model) to be used with [socket.io](http://socket.io). 9 | ampersand-io-model is an extension built on [ampersand-state](http://ampersandjs.com/docs/#ampersand-state) to provide methods and properties that you'll often want when modeling data you get from an API, using a realtime based websocket app. 10 | 11 | For further explanation see the [learn ampersand-state](http://ampersandjs.com/learn/state) guide and the [ampersand-io](https://github.com/sinfo/ampersand-io) documentation. 12 | 13 | ## Installing 14 | 15 | ``` 16 | npm install ampersand-io-model 17 | ``` 18 | 19 | ## API Reference 20 | 21 | The module exports just one item, the ampersand-io-model constructor. It has a method called `extend` that works as follows: 22 | 23 | ### extend `AmpersandIOModel.extend({ })` 24 | 25 | To create a **Model** class of your own, you extend **AmpersandIOModel** and provide instance properties and options for your class. Typically here you will pass any properties (`props`, `session`, and `derived`) of your model class, and any instance methods to be attached to instances of your class, including the override of any [ampersand-io](https://github.com/sinfo/ampersand-io) default properties. 26 | 27 | **extend** correctly sets up the prototype chain, so that subclasses created with **extend** can be further extended as many times as you like. 28 | 29 | As with AmpersandState, definitions like `props`, `session`, `derived` etc will be merged with superclass definitions. 30 | 31 | ```javascript 32 | var Person = AmpersandIOModel.extend({ 33 | props: { 34 | firstName: 'string', 35 | lastName: 'string' 36 | }, 37 | session: { 38 | signedIn: ['boolean', true, false], 39 | }, 40 | derived: { 41 | fullName: { 42 | deps: ['firstName', 'lastName'], 43 | fn: function () { 44 | return this.firstName + ' ' + this.lastName; 45 | } 46 | } 47 | } 48 | events = { 49 | onFetch: 'my-fetch-response', 50 | create: 'my-model-create', 51 | update: 'my-model-update', 52 | fetch: 'my-model-fetch', 53 | remove: 'my-model-remove' 54 | } 55 | }); 56 | ``` 57 | 58 | **Note:** all the methods you're going to see here use ampersand-io [emit method](https://github.com/sinfo/ampersand-io#emit-ioemitevent-data-options-callback) to persist the state of a model to the server. Usually you won't call this directly, you'd use `save` or `destroy` instead, but it can be overriden for custom behaviour. 59 | 60 | ### constructor/initialize `new ExtendedAmpersandIOModel([attrs], [options])` 61 | 62 | Uses the [ampersand-state](http://ampersandjs.com/docs/#ampersand-state-constructorinitialize) constructor and the [ampsersand-io](https://github.com/sinfo/ampersand-io#constructorinitialize-new-ampersandiosocket-options) constructor to initalize you instance. 63 | 64 | The `options` object is accordingly passed to each of the constructors. So if you set any prop like the `socket` prop it will be rightfully set using the `ampersand-io` constructor. 65 | 66 | Also if you pass `collection` as part of options it'll be stored for reference. 67 | 68 | As with AmpersandState, if you have defined an **initialize** function for your subclass of State, it will be invoked at creation time. 69 | 70 | ```javascript 71 | var me = new Person({ 72 | firstName: 'Phil', 73 | lastName: 'Roberts' 74 | }); 75 | 76 | me.firstName //=> Phil 77 | ``` 78 | 79 | Available options: 80 | 81 | * `[parse]` {Boolean} - whether to call the class's [parse](#ampersand-state-parse) function with the initial attributes. _Defaults to `false`_. 82 | * `[parent]` {AmpersandState} - pass a reference to a model's parent to store on the model. 83 | * `[collection]` {Collection} - pass a reference to the collection the model is in. Defaults to `undefined`. 84 | * `[socket]` {Socket-io client/ string} - pass a reference to the socket-io client instance you're using or a string to be used as a namespace for a new socket.io-client instance. 85 | * `[events]` {[Events object](#events-modelevents)} - pass an `events` object as defined to override the pre-defined events used by the model. 86 | 87 | Other options are supported by the [ampsersand-io](https://github.com/sinfo/ampersand-io#constructorinitialize-new-ampersandiosocket-options) constructor, although they don't seem the most suited to this use case, you may use them if you like. 88 | 89 | ### events `model.events` 90 | 91 | Overridable property containing a key-value reference to the events to be used by the socket conection. The model uses the default props: 92 | 93 | ```javascript 94 | var events = { 95 | onFetch: 'fetch-response', 96 | create: 'model-create', 97 | update: 'model-update', 98 | fetch: 'model-fetch', 99 | remove: 'model-remove' 100 | }; 101 | ``` 102 | You may override them on construction or extend the model by passing an `events` property on [extend](#extend-ampersandiomodelextend). 103 | 104 | For more info on this property check [ampersand-io events](https://github.com/sinfo/ampersand-io#events-ioevents). 105 | 106 | ### save `model.save([attributes], [options])` 107 | 108 | Save a model to your database (or alternative persistence layer) by delegating to [ampersand-io](https://github.com/sinfo/ampersand-io). Returns `this` model object if validation is successful and false otherwise. The attributes hash (as in [set](http://ampersandjs.com/docs#ampersand-state-set)) should contain the attributes you'd like to change — keys that aren't mentioned won't be altered — but, a *complete representation* of the resource will be sent to the websocket server. As with `set`, you may pass individual keys and values instead of a hash. If the model has a validate method, and validation fails, the model will not be saved. If the model `isNew`, the save will be a "create" `event`. If the model already exists on the server, the save will be an "update" `event`. 109 | 110 | Pass `{wait: true}` if you'd like to wait for the server callback `ACK` before setting the new attributes on the model. 111 | 112 | ```javascript 113 | var book = new Backbone.Model({ 114 | title: "The Rough Riders", 115 | author: "Theodore Roosevelt" 116 | }); 117 | 118 | book.save(); 119 | //=> triggers a `create` event via ampersand-io with { "title": "The Rough Riders", "author": "Theodore Roosevelt" } 120 | 121 | book.save({author: "Teddy"}); 122 | //=> triggers a `update` via ampersand-io with { "title": "The Rough Riders", "author": "Teddy" } 123 | ``` 124 | 125 | **save** accepts a `callback` in the options hash, which will be passed the arguments `(err, model, resp)` If a server-side validation fails, return a JSON object as the first argument on the callback function describing the error. 126 | 127 | ### fetch `model.fetch([options])` 128 | 129 | Resets the model's state from the server by delegating a `fetch` event to ampersand-io. Returns `this` model. Useful if the model has yet to be populated with data, or you want to ensure you have the latest server state. 130 | 131 | The `fetch` method is comprised of two parts. A first one where a `fetch` event is emitted (containing a data object with `this` model as a `data` prop and a `options` prop containing the options passed to the model) and a `onFetch` listener is set. 132 | 133 | Then we have a second part where the server sends a `onFetch` event to which the model updates his model reference. The `onFetch` response object from the server should contain an `err` prop detailing any error ocurrences in the serverside and/or a `data` prop containing the object to update this model. 134 | 135 | Accepts a `callback` in the options hash, which is passed `(err, model, data)` as arguments. 136 | 137 | ```javascript 138 | var me = new Person({id: 123}); 139 | me.fetch(); 140 | ``` 141 | 142 | ### destroy `model.destroy([options])` 143 | 144 | Destroys the model on the server by delegating a `remove` event to ampersand-io. Returns `this` model, or `false` if the model [isNew](https://github.com/AmpersandJS/ampersand-state#isnew-stateisnew). Accepts a `callback` in the options hash, which is passed `(err, model, resp)` as arguments. 145 | 146 | Triggers: 147 | 148 | * a `"destroy"` event on the model, which will bubble up through any collections which contain it. 149 | 150 | Pass `{wait: true}` if you'd like to wait for the server to response before removing the model from the collection. 151 | 152 | ```javascript 153 | var task = new Task({id: 123}); 154 | task.destroy({ 155 | callback: function (err, model, resp) { 156 | if(err){ 157 | alert('An error ocurred'); 158 | } 159 | alert('Task destroyed!'); 160 | } 161 | }); 162 | ``` 163 | 164 | ## credits 165 | 166 | Created by [@JGAntunes](http://github.com/JGAntunes), with the support of [@SINFO](http://github.com/sinfo) and based on a series of Ampersand Modules. 167 | 168 | ## License 169 | 170 | MIT 171 | -------------------------------------------------------------------------------- /tests/backbone.js: -------------------------------------------------------------------------------- 1 | /* global console */ 2 | var _ = require('underscore'); 3 | 4 | var tape = require('tape'); 5 | var test = tape; 6 | 7 | var IOevents = { 8 | onFetch: 'fetch-response', 9 | create: 'model-create', 10 | update: 'model-update', 11 | fetch: 'model-fetch', 12 | remove: 'model-remove' 13 | }; 14 | 15 | //qunit has equal/strictEqual, we just have equal 16 | tape.Test.prototype.strictEqual = function () { 17 | this.equal.apply(this, arguments); 18 | }; 19 | 20 | //stub qunit module 21 | function module(moduleName, opts) { 22 | test = function (testName, cb) { 23 | if (opts.setup) opts.setup(); 24 | tape.call(tape, moduleName + ' - ' + testName, cb); 25 | }; 26 | 27 | test.only = function (testName, cb) { 28 | if (opts.setup) opts.setup(); 29 | tape.only.call(tape, moduleName + ' - ' + testName, cb); 30 | }; 31 | } 32 | 33 | var AmpersandModel = require('../ampersand-io-model'); 34 | //Let's fake some backbone things to minimize test changes 35 | var env = {}; 36 | var Backbone = { 37 | Model: AmpersandModel.extend({ 38 | extraProperties: 'allow', 39 | emit: function (event, model, options) { 40 | env.emitArgs = { 41 | event: event, 42 | model: model, 43 | options: options 44 | }; 45 | } 46 | }), 47 | Collection: { 48 | extend: function (o) { 49 | var Coll = function () { 50 | var k; 51 | for (k in o) { 52 | this[k] = o[k]; 53 | } 54 | }; 55 | Coll.prototype.add = function (m) { 56 | m.collection = this; 57 | }; 58 | return Coll; 59 | } 60 | } 61 | }; 62 | 63 | //ALTERED BACKBONE FUNCS BASED ON THE NEW MODEL 64 | (function newAlteredFuncs() { 65 | var proxy = Backbone.Model.extend(); 66 | var klass = Backbone.Collection.extend(); 67 | var doc, collection; 68 | 69 | module("Backbone.Model", { 70 | 71 | setup: function () { 72 | doc = new proxy({ 73 | id : '1-the-tempest', 74 | title : "The Tempest", 75 | author : "Bill Shakespeare", 76 | textLength : 123 77 | }); 78 | collection = new klass(); 79 | collection.add(doc); 80 | } 81 | 82 | }); 83 | 84 | test("validate after save", function (t) { 85 | t.plan(2); 86 | var lastError, model = new Backbone.Model(); 87 | model.validate = function (attrs) { 88 | if (attrs.admin) return "Can't change admin status."; 89 | }; 90 | model.emit = function (event, model, options) { 91 | options.callback.call(this, null, {admin: true}); 92 | }; 93 | model.on('invalid', function (model, error) { 94 | lastError = error; 95 | }); 96 | model.save(null); 97 | 98 | t.equal(lastError, "Can't change admin status."); 99 | t.equal(model.validationError, "Can't change admin status."); 100 | }); 101 | 102 | test("save", function (t) { 103 | t.plan(2); 104 | doc.save({title : "Henry V"}); 105 | t.equal(env.emitArgs.event, 'model-update'); 106 | t.ok(_.isEqual(env.emitArgs.model, doc)); 107 | }); 108 | 109 | test("save, fetch, destroy triggers error event when an error occurs", function (t) { 110 | t.plan(3); 111 | var model = new Backbone.Model(); 112 | model.on('error', function () { 113 | t.ok(true); 114 | }); 115 | model.emit = function (event, model, options) { 116 | options.callback(true); 117 | }; 118 | model.save({data: 2, id: 1}); 119 | model.fetch(); 120 | model.destroy(); 121 | }); 122 | 123 | test("non-persisted destroy", function (t) { 124 | t.plan(1); 125 | var a = new Backbone.Model({ 'foo': 1, 'bar': 2, 'baz': 3}); 126 | a.emit = function () { throw "should not be called"; }; 127 | a.destroy(); 128 | t.ok(true, "non-persisted model should not call emit"); 129 | }); 130 | 131 | test("model & result is passed to callback", function (t) { 132 | t.plan(6); 133 | var model = new Backbone.Model(); 134 | var opts = { 135 | callback: function (err, model, result) { 136 | t.ok(result); 137 | t.ok(model); 138 | } 139 | }; 140 | model.emit = function (event, model, options) { 141 | if(options.respCallback){ 142 | return options.respCallback({data: 'test'}); 143 | } 144 | options.callback(null, 'test'); 145 | }; 146 | model.save({id: 1}, opts); 147 | model.fetch(opts); 148 | model.destroy(opts); 149 | }); 150 | 151 | test("#1365 - Destroy: New models execute the callback.", function (t) { 152 | t.plan(2); 153 | new Backbone.Model() 154 | .on('emit', function () { t.ok(false); }) 155 | .on('destroy', function () { t.ok(true); }) 156 | .destroy({ callback: function () { t.ok(true); }}); 157 | }); 158 | 159 | test("#1377 - Save without attrs triggers 'error'.", function (t) { 160 | t.plan(1); 161 | var Model = Backbone.Model.extend({ 162 | emit: function (event, model, options) { options.callback(); }, 163 | validate: function () { return 'invalid'; } 164 | }); 165 | var model = new Model({id: 1}); 166 | model.on('invalid', function () { t.ok(true); }); 167 | model.save(); 168 | }); 169 | 170 | test("#1478 - Model `save` does not trigger change on unchanged attributes", function (t) { 171 | var Model = Backbone.Model.extend({ 172 | emit: function (event, model, options) { 173 | setTimeout(function () { 174 | options.callback(); 175 | t.end(); 176 | }, 0); 177 | } 178 | }); 179 | new Model({x: true}) 180 | .on('change:x', function () { t.ok(false); }) 181 | .save(null, {wait: true}); 182 | }); 183 | 184 | test("#1433 - Save: An invalid model cannot be persisted.", function (t) { 185 | t.plan(1); 186 | var model = new Backbone.Model(); 187 | model.validate = function () { return 'invalid'; }; 188 | model.emit = function () { t.ok(false); }; 189 | t.strictEqual(model.save(), false); 190 | }); 191 | })(); 192 | 193 | 194 | // PRE-EXISTING BACKBONE TEST FUNCS 195 | (function preExistingFuncs() { 196 | var proxy = Backbone.Model.extend(); 197 | var klass = Backbone.Collection.extend(); 198 | var doc, collection; 199 | 200 | module("Backbone.Model", { 201 | 202 | setup: function () { 203 | doc = new proxy({ 204 | id : '1-the-tempest', 205 | title : "The Tempest", 206 | author : "Bill Shakespeare", 207 | textLength : 123 208 | }); 209 | collection = new klass(); 210 | collection.add(doc); 211 | } 212 | 213 | }); 214 | 215 | test("initialize", function (t) { 216 | t.plan(3); 217 | var Model = Backbone.Model.extend({ 218 | initialize: function () { 219 | this.one = 1; 220 | t.equal(this.collection, collection); 221 | } 222 | }); 223 | var model = new Model({}, {collection: collection}); 224 | t.equal(model.one, 1); 225 | t.equal(model.collection, collection); 226 | }); 227 | 228 | test("initialize with attributes and options", function (t) { 229 | t.plan(1); 230 | var Model = Backbone.Model.extend({ 231 | initialize: function (attributes, options) { 232 | this.one = options.one; 233 | } 234 | }); 235 | var model = new Model({}, {one: 1}); 236 | t.equal(model.one, 1); 237 | }); 238 | 239 | test("initialize with parsed attributes", function (t) { 240 | t.plan(1); 241 | var Model = Backbone.Model.extend({ 242 | parse: function (attrs) { 243 | attrs.value += 1; 244 | return attrs; 245 | } 246 | }); 247 | var model = new Model({value: 1}, {parse: true}); 248 | t.equal(model.get('value'), 2); 249 | }); 250 | 251 | test("initialize with defaults", function (t) { 252 | t.plan(2); 253 | var Model = Backbone.Model.extend({ 254 | props: { 255 | first_name: ['string', true, 'Unknown'], 256 | last_name: ['string', true, 'Unknown'] 257 | } 258 | }); 259 | var model = new Model({'first_name': 'John'}); 260 | t.equal(model.get('first_name'), 'John'); 261 | t.equal(model.get('last_name'), 'Unknown'); 262 | }); 263 | 264 | test("parse can return null", function (t) { 265 | t.plan(1); 266 | var Model = Backbone.Model.extend({ 267 | parse: function (attrs) { 268 | attrs.value += 1; 269 | return null; 270 | } 271 | }); 272 | var model = new Model({value: 1}, {parse: true}); 273 | t.equal(JSON.stringify(model.toJSON()), "{}"); 274 | }); 275 | 276 | test("isNew", function (t) { 277 | t.plan(6); 278 | var a = new Backbone.Model({ 'foo': 1, 'bar': 2, 'baz': 3}); 279 | t.ok(a.isNew(), "it should be new"); 280 | a = new Backbone.Model({ 'foo': 1, 'bar': 2, 'baz': 3, 'id': -5 }); 281 | t.ok(!a.isNew(), "any defined ID is legal, negative or positive"); 282 | a = new Backbone.Model({ 'foo': 1, 'bar': 2, 'baz': 3, 'id': 0 }); 283 | t.ok(!a.isNew(), "any defined ID is legal, including zero"); 284 | t.ok(new Backbone.Model({ }).isNew(), "is true when there is no id"); 285 | t.ok(!new Backbone.Model({ 'id': 2 }).isNew(), "is false for a positive integer"); 286 | t.ok(!new Backbone.Model({ 'id': -5 }).isNew(), "is false for a negative integer"); 287 | }); 288 | 289 | test("get", function (t) { 290 | t.plan(2); 291 | t.equal(doc.get('title'), 'The Tempest'); 292 | t.equal(doc.get('author'), 'Bill Shakespeare'); 293 | }); 294 | 295 | test("escape", function (t) { 296 | t.plan(5); 297 | t.equal(doc.escape('title'), 'The Tempest'); 298 | doc.set({audience: 'Bill & Bob'}); 299 | t.equal(doc.escape('audience'), 'Bill & Bob'); 300 | doc.set({audience: 'Tim > Joan'}); 301 | t.equal(doc.escape('audience'), 'Tim > Joan'); 302 | doc.set({audience: 10101}); 303 | t.equal(doc.escape('audience'), '10101'); 304 | doc.unset('audience'); 305 | t.equal(doc.escape('audience'), ''); 306 | }); 307 | 308 | test("set and unset", function (t) { 309 | t.plan(8); 310 | var a = new Backbone.Model({id: 'id', foo: 1, bar: 2, baz: 3}); 311 | var changeCount = 0; 312 | a.on("change:foo", function () { changeCount += 1; }); 313 | a.set({'foo': 2}); 314 | t.ok(a.get('foo') == 2, "Foo should have changed."); 315 | t.ok(changeCount == 1, "Change count should have incremented."); 316 | a.set({'foo': 2}); // set with value that is not new shouldn't fire change event 317 | t.ok(a.get('foo') == 2, "Foo should NOT have changed, still 2"); 318 | t.ok(changeCount == 1, "Change count should NOT have incremented."); 319 | 320 | a.validate = function (attrs) { 321 | t.equal(attrs.foo, void 0, "validate:true passed while unsetting"); 322 | }; 323 | a.unset('foo', {validate: true}); 324 | t.equal(a.get('foo'), void 0, "Foo should have changed"); 325 | delete a.validate; 326 | t.ok(changeCount == 2, "Change count should have incremented for unset."); 327 | 328 | a.unset('id'); 329 | t.equal(a.id, undefined, "Unsetting the id should remove the id property."); 330 | }); 331 | 332 | test("#2030 - set with failed validate, followed by another set triggers change", function (t) { 333 | t.plan(1); 334 | var attr = 0, main = 0, error = 0; 335 | var Model = Backbone.Model.extend({ 336 | validate: function (attr) { 337 | if (attr.x > 1) { 338 | error++; 339 | return "this is an error"; 340 | } 341 | } 342 | }); 343 | var model = new Model({x: 0}); 344 | model.on('change:x', function () { attr++; }); 345 | model.on('change', function () { main++; }); 346 | model.set({x: 2}, {validate: true}); 347 | model.set({x: 1}, {validate: true}); 348 | t.deepEqual([attr, main, error], [1, 1, 1]); 349 | }); 350 | 351 | test("set triggers changes in the correct order", function (t) { 352 | t.plan(1); 353 | var value = null; 354 | var model = new Backbone.Model(); 355 | model.on('last', function () { value = 'last'; }); 356 | model.on('first', function () { value = 'first'; }); 357 | model.trigger('first'); 358 | model.trigger('last'); 359 | t.equal(value, 'last'); 360 | }); 361 | 362 | test("set falsy values in the correct order", function (t) { 363 | t.plan(2); 364 | var model = new Backbone.Model({result: 'result'}); 365 | model.on('change', function () { 366 | t.equal(model._changed.result, void 0); 367 | t.equal(model.previous('result'), false); 368 | }); 369 | model.set({result: void 0}, {silent: true}); 370 | model.set({result: null}, {silent: true}); 371 | model.set({result: false}, {silent: true}); 372 | model.set({result: void 0}); 373 | }); 374 | 375 | test("multiple unsets", function (t) { 376 | t.plan(1); 377 | var i = 0; 378 | var counter = function () { i++; }; 379 | var model = new Backbone.Model({a: 1}); 380 | model.on("change:a", counter); 381 | model.set({a: 2}); 382 | model.unset('a'); 383 | model.unset('a'); 384 | t.equal(i, 2, 'Unset does not fire an event for missing attributes.'); 385 | }); 386 | 387 | test("unset and changedAttributes", function (t) { 388 | t.plan(1); 389 | var model = new Backbone.Model({a: 1}); 390 | model.on('change', function () { 391 | t.ok('a' in model.changedAttributes(), 'changedAttributes should contain unset properties'); 392 | }); 393 | model.unset('a'); 394 | }); 395 | 396 | test("set an empty string", function (t) { 397 | t.plan(1); 398 | var model = new Backbone.Model({name : "Model"}); 399 | model.set({name : ''}); 400 | t.equal(model.get('name'), ''); 401 | }); 402 | 403 | test("setting an object", function (t) { 404 | t.plan(1); 405 | var model = new Backbone.Model({ 406 | custom: { foo: 1 } 407 | }); 408 | model.on('change', function () { 409 | t.ok(1); 410 | }); 411 | model.set({ 412 | custom: { foo: 1 } // no change should be fired 413 | }); 414 | model.set({ 415 | custom: { foo: 2 } // change event should be fired 416 | }); 417 | }); 418 | 419 | test("change, hasChanged, changedAttributes, previous, previousAttributes", function (t) { 420 | t.plan(9); 421 | var model = new Backbone.Model({name: "Tim", age: 10}); 422 | t.deepEqual(model.changedAttributes(), false); 423 | model.on('change', function () { 424 | t.ok(model.hasChanged('name'), 'name changed'); 425 | t.ok(!model.hasChanged('age'), 'age did not'); 426 | t.ok(_.isEqual(model.changedAttributes(), {name : 'Rob'}), 'changedAttributes returns the changed attrs'); 427 | t.equal(model.previous('name'), 'Tim'); 428 | t.ok(_.isEqual(model.previousAttributes(), {name : "Tim", age : 10}), 'previousAttributes is correct'); 429 | }); 430 | t.equal(model.hasChanged(), false); 431 | t.equal(model.hasChanged(undefined), false); 432 | model.set({name : 'Rob'}); 433 | t.equal(model.get('name'), 'Rob'); 434 | }); 435 | 436 | test("changedAttributes", function (t) { 437 | t.plan(3); 438 | var model = new Backbone.Model({a: 'a', b: 'b'}); 439 | t.deepEqual(model.changedAttributes(), false); 440 | t.equal(model.changedAttributes({a: 'a'}), false); 441 | t.equal(model.changedAttributes({a: 'b'}).a, 'b'); 442 | }); 443 | 444 | test("change with options", function (t) { 445 | t.plan(2); 446 | var value; 447 | var model = new Backbone.Model({name: 'Rob'}); 448 | model.on('change', function (model, options) { 449 | value = options.prefix + model.get('name'); 450 | }); 451 | model.set({name: 'Bob'}, {prefix: 'Mr. '}); 452 | t.equal(value, 'Mr. Bob'); 453 | model.set({name: 'Sue'}, {prefix: 'Ms. '}); 454 | t.equal(value, 'Ms. Sue'); 455 | }); 456 | 457 | test("change after initialize", function (t) { 458 | t.plan(1); 459 | var changed = 0; 460 | var attrs = {id: 1, label: 'c'}; 461 | var obj = new Backbone.Model(attrs); 462 | obj.on('change', function () { changed += 1; }); 463 | obj.set(attrs); 464 | t.equal(changed, 0); 465 | }); 466 | 467 | 468 | 469 | 470 | 471 | test("validate", function (t) { 472 | t.plan(7); 473 | var lastError; 474 | var model = new Backbone.Model(); 475 | model.validate = function (attrs) { 476 | if (attrs.admin != this.get('admin')) return "Can't change admin status."; 477 | }; 478 | model.on('invalid', function (model, error) { 479 | lastError = error; 480 | }); 481 | var result = model.set({a: 100}); 482 | t.equal(result, model); 483 | t.equal(model.get('a'), 100); 484 | t.equal(lastError, undefined); 485 | result = model.set({admin: true}); 486 | t.equal(model.get('admin'), true); 487 | result = model.set({a: 200, admin: false}, {validate: true}); 488 | t.equal(lastError, "Can't change admin status."); 489 | t.equal(result, false); 490 | t.equal(model.get('a'), 100); 491 | }); 492 | 493 | test("validate on unset and clear", function (t) { 494 | t.plan(6); 495 | var error; 496 | var model = new Backbone.Model({name: "One"}); 497 | model.validate = function (attrs) { 498 | if (!attrs.name) { 499 | error = true; 500 | return "No thanks."; 501 | } 502 | }; 503 | model.set({name: "Two"}); 504 | t.equal(model.get('name'), 'Two'); 505 | t.equal(error, undefined); 506 | model.unset('name', {validate: true}); 507 | t.equal(error, true); 508 | t.equal(model.get('name'), 'Two'); 509 | model.clear({validate: true}); 510 | t.equal(model.get('name'), 'Two'); 511 | delete model.validate; 512 | model.clear(); 513 | t.equal(model.get('name'), undefined); 514 | }); 515 | 516 | test("validate with error callback", function (t) { 517 | t.plan(8); 518 | var lastError, boundError; 519 | var model = new Backbone.Model(); 520 | model.validate = function (attrs) { 521 | if (attrs.admin) return "Can't change admin status."; 522 | }; 523 | model.on('invalid', function (model, error) { 524 | boundError = true; 525 | }); 526 | var result = model.set({a: 100}, {validate: true}); 527 | t.equal(result, model); 528 | t.equal(model.get('a'), 100); 529 | t.equal(model.validationError, null); 530 | t.equal(boundError, undefined); 531 | result = model.set({a: 200, admin: true}, {validate: true}); 532 | t.equal(result, false); 533 | t.equal(model.get('a'), 100); 534 | t.equal(model.validationError, "Can't change admin status."); 535 | t.equal(boundError, true); 536 | }); 537 | 538 | test("defaults always extend attrs (#459)", function (t) { 539 | t.plan(2); 540 | var Defaulted = Backbone.Model.extend({ 541 | props: { 542 | one: ['number', true, 1] 543 | }, 544 | initialize : function (attrs, opts) { 545 | t.equal(this.attributes.one, 1); 546 | } 547 | }); 548 | var providedattrs = new Defaulted({}); 549 | var emptyattrs = new Defaulted(); 550 | }); 551 | 552 | test("Nested change events don't clobber previous attributes", function (t) { 553 | t.plan(4); 554 | new Backbone.Model() 555 | .on('change:state', function (model, newState) { 556 | t.equal(model.previous('state'), undefined); 557 | t.equal(newState, 'hello'); 558 | // Fire a nested change event. 559 | model.set({other: 'whatever'}); 560 | }) 561 | .on('change:state', function (model, newState) { 562 | t.equal(model.previous('state'), undefined); 563 | t.equal(newState, 'hello'); 564 | }) 565 | .set({state: 'hello'}); 566 | }); 567 | 568 | test("hasChanged/set should use same comparison", function (t) { 569 | t.plan(2); 570 | var changed = 0, model = new Backbone.Model({a: null}); 571 | model.on('change', function () { 572 | t.ok(this.hasChanged('a')); 573 | }) 574 | .on('change:a', function () { 575 | changed++; 576 | }) 577 | .set({a: undefined}); 578 | t.equal(changed, 1); 579 | }); 580 | 581 | test("#582, #425, change:attribute callbacks should fire after all changes have occurred", function (t) { 582 | t.plan(9); 583 | var model = new Backbone.Model(); 584 | 585 | var assertion = function () { 586 | t.equal(model.get('a'), 'a'); 587 | t.equal(model.get('b'), 'b'); 588 | t.equal(model.get('c'), 'c'); 589 | }; 590 | 591 | model.on('change:a', assertion); 592 | model.on('change:b', assertion); 593 | model.on('change:c', assertion); 594 | 595 | model.set({a: 'a', b: 'b', c: 'c'}); 596 | }); 597 | 598 | test("set same value does not trigger change", function (t) { 599 | var model = new Backbone.Model({x: 1}); 600 | model.on('change change:x', function () { 601 | t.ok(false); 602 | }); 603 | model.set({x: 1}); 604 | model.set({x: 1}); 605 | t.end(); 606 | }); 607 | 608 | test("unset does not fire a change for undefined attributes", function (t) { 609 | var model = new Backbone.Model({x: undefined}); 610 | model.on('change:x', function () { t.ok(false); }); 611 | model.unset('x'); 612 | t.end(); 613 | }); 614 | 615 | test("hasChanged works outside of change events, and true within", function (t) { 616 | t.plan(6); 617 | var model = new Backbone.Model({x: 1}); 618 | model.on('change:x', function () { 619 | t.ok(model.hasChanged('x')); 620 | t.equal(model.get('x'), 1); 621 | }); 622 | model.set({x: 2}, {silent: true}); 623 | t.ok(model.hasChanged()); 624 | t.equal(model.hasChanged('x'), true); 625 | model.set({x: 1}); 626 | t.ok(model.hasChanged()); 627 | t.equal(model.hasChanged('x'), true); 628 | }); 629 | 630 | test("hasChanged gets cleared on the following set", function (t) { 631 | t.plan(4); 632 | var model = new Backbone.Model(); 633 | model.set({x: 1}); 634 | t.ok(model.hasChanged()); 635 | model.set({x: 1}); 636 | t.ok(!model.hasChanged()); 637 | model.set({x: 2}); 638 | t.ok(model.hasChanged()); 639 | model.set({}); 640 | t.ok(!model.hasChanged()); 641 | }); 642 | 643 | test("`hasChanged` for falsey keys", function (t) { 644 | t.plan(2); 645 | var model = new Backbone.Model(); 646 | model.set({x: true}, {silent: true}); 647 | t.ok(!model.hasChanged(0)); 648 | t.ok(!model.hasChanged('')); 649 | }); 650 | 651 | test("`previous` for falsey keys", function (t) { 652 | t.plan(2); 653 | var model = new Backbone.Model({0: true, '': true}); 654 | model.set({0: false, '': false}, {silent: true}); 655 | t.equal(model.previous(0), true); 656 | t.equal(model.previous(''), true); 657 | }); 658 | 659 | test("nested `set` during `'change:attr'`", function (t) { 660 | t.plan(2); 661 | var events = []; 662 | var model = new Backbone.Model(); 663 | model.on('all', function (event) { events.push(event); }); 664 | model.on('change', function () { 665 | model.set({z: true}, {silent: true}); 666 | }); 667 | model.on('change:x', function () { 668 | model.set({y: true}); 669 | }); 670 | model.set({x: true}); 671 | t.deepEqual(events, ['change:y', 'change:x', 'change']); 672 | events = []; 673 | model.set({z: true}); 674 | t.deepEqual(events, []); 675 | }); 676 | 677 | test("nested `change` only fires once", function (t) { 678 | t.plan(1); 679 | var model = new Backbone.Model(); 680 | model.on('change', function () { 681 | t.ok(true); 682 | model.set({x: true}); 683 | }); 684 | model.set({x: true}); 685 | }); 686 | 687 | test("nested `set` during `'change'`", function (t) { 688 | t.plan(6); 689 | var count = 0; 690 | var model = new Backbone.Model(); 691 | model.on('change', function () { 692 | switch (count++) { 693 | case 0: 694 | t.deepEqual(this.changedAttributes(), {x: true}); 695 | t.equal(model.previous('x'), undefined); 696 | model.set({y: true}); 697 | break; 698 | case 1: 699 | t.deepEqual(this.changedAttributes(), {x: true, y: true}); 700 | t.equal(model.previous('x'), undefined); 701 | model.set({z: true}); 702 | break; 703 | case 2: 704 | t.deepEqual(this.changedAttributes(), {x: true, y: true, z: true}); 705 | t.equal(model.previous('y'), undefined); 706 | break; 707 | default: 708 | t.ok(false); 709 | } 710 | }); 711 | model.set({x: true}); 712 | }); 713 | 714 | test("nested `change` with silent", function (t) { 715 | t.plan(3); 716 | var count = 0; 717 | var model = new Backbone.Model(); 718 | model.on('change:y', function () { t.ok(false); }); 719 | model.on('change', function () { 720 | switch (count++) { 721 | case 0: 722 | t.deepEqual(this.changedAttributes(), {x: true}); 723 | model.set({y: true}, {silent: true}); 724 | model.set({z: true}); 725 | break; 726 | case 1: 727 | t.deepEqual(this.changedAttributes(), {x: true, y: true, z: true}); 728 | break; 729 | case 2: 730 | t.deepEqual(this.changedAttributes(), {z: false}); 731 | break; 732 | default: 733 | t.ok(false); 734 | } 735 | }); 736 | model.set({x: true}); 737 | model.set({z: false}); 738 | }); 739 | 740 | test("nested `change:attr` with silent", function (t) { 741 | var model = new Backbone.Model(); 742 | model.on('change:y', function () { t.ok(false); }); 743 | model.on('change', function () { 744 | model.set({y: true}, {silent: true}); 745 | model.set({z: true}); 746 | }); 747 | model.set({x: true}); 748 | t.end(); 749 | }); 750 | 751 | test("multiple nested changes with silent", function (t) { 752 | t.plan(1); 753 | var model = new Backbone.Model(); 754 | model.on('change:x', function () { 755 | model.set({y: 1}, {silent: true}); 756 | model.set({y: 2}); 757 | }); 758 | model.on('change:y', function (model, val) { 759 | t.equal(val, 2); 760 | }); 761 | model.set({x: true}); 762 | }); 763 | 764 | test("multiple nested changes with silent", function (t) { 765 | t.plan(1); 766 | var changes = []; 767 | var model = new Backbone.Model(); 768 | model.on('change:b', function (model, val) { changes.push(val); }); 769 | model.on('change', function () { 770 | model.set({b: 1}); 771 | }); 772 | model.set({b: 0}); 773 | t.deepEqual(changes, [0, 1]); 774 | }); 775 | 776 | test("basic silent change semantics", function (t) { 777 | t.plan(1); 778 | var model = new Backbone.Model(); 779 | model.set({x: 1}); 780 | model.on('change', function () { t.ok(true); }); 781 | model.set({x: 2}, {silent: true}); 782 | model.set({x: 1}); 783 | }); 784 | 785 | test("nested set multiple times", function (t) { 786 | t.plan(1); 787 | var model = new Backbone.Model(); 788 | model.on('change:b', function () { 789 | t.ok(true); 790 | }); 791 | model.on('change:a', function () { 792 | model.set({b: true}); 793 | model.set({b: true}); 794 | }); 795 | model.set({a: true}); 796 | }); 797 | 798 | test("#1122 - clear does not alter options.", function (t) { 799 | t.plan(1); 800 | var model = new Backbone.Model(); 801 | var options = {}; 802 | model.clear(options); 803 | t.ok(!options.unset); 804 | }); 805 | 806 | test("#1122 - unset does not alter options.", function (t) { 807 | t.plan(1); 808 | var model = new Backbone.Model({x: 1}); 809 | var options = {}; 810 | model.unset('x', options); 811 | t.ok(!options.unset); 812 | }); 813 | 814 | test("#1545 - `undefined` can be passed to a model constructor without coersion", function (t) { 815 | var Model = Backbone.Model.extend({ 816 | defaults: { one: 1 }, 817 | initialize : function (attrs, opts) { 818 | t.equal(attrs, undefined); 819 | } 820 | }); 821 | var emptyattrs = new Model(); 822 | var undefinedattrs = new Model(undefined); 823 | t.end(); 824 | }); 825 | 826 | 827 | 828 | test("#1664 - Changing from one value, silently to another, back to original triggers a change.", function (t) { 829 | t.plan(1); 830 | var model = new Backbone.Model({x: 1}); 831 | model.on('change:x', function () { t.ok(true); }); 832 | model.set({x: 2}, {silent: true}); 833 | model.set({x: 3}, {silent: true}); 834 | model.set({x: 1}); 835 | }); 836 | 837 | test("#1664 - multiple silent changes nested inside a change event", function (t) { 838 | t.plan(2); 839 | var changes = []; 840 | var model = new Backbone.Model(); 841 | model.on('change', function () { 842 | model.set({a: 'c'}, {silent: true}); 843 | model.set({b: 2}, {silent: true}); 844 | model.unset('c', {silent: true}); 845 | }); 846 | model.on('change:a change:b change:c', function (model, val) { changes.push(val); }); 847 | model.set({a: 'a', b: 1, c: 'item'}); 848 | t.deepEqual(changes, ['a', 1, 'item']); 849 | t.deepEqual(model.attributes, {a: 'c', b: 2}); 850 | }); 851 | 852 | test("#1791 - `attributes` is available for `parse`", function (t) { 853 | var Model = Backbone.Model.extend({ 854 | parse: function () { this.attributes; } // shouldn't throw an error 855 | }); 856 | var model = new Model(null, {parse: true}); 857 | t.end(); 858 | }); 859 | 860 | test("silent changes in last `change` event back to original triggers change", function (t) { 861 | t.plan(2); 862 | var changes = []; 863 | var model = new Backbone.Model(); 864 | model.on('change:a change:b change:c', function (model, val) { changes.push(val); }); 865 | model.on('change', function () { 866 | model.set({a: 'c'}, {silent: true}); 867 | }); 868 | model.set({a: 'a'}); 869 | t.deepEqual(changes, ['a']); 870 | model.set({a: 'a'}); 871 | t.deepEqual(changes, ['a', 'a']); 872 | }); 873 | 874 | test("#1943 change calculations should use _.isEqual", function (t) { 875 | t.plan(1); 876 | var model = new Backbone.Model({a: {key: 'value'}}); 877 | model.set('a', {key: 'value'}, {silent: true}); 878 | t.equal(model.changedAttributes(), false); 879 | }); 880 | 881 | test("#1964 - final `change` event is always fired, regardless of interim changes", function (t) { 882 | t.plan(1); 883 | var model = new Backbone.Model(); 884 | model.on('change:property', function () { 885 | model.set('property', 'bar'); 886 | }); 887 | model.on('change', function () { 888 | t.ok(true); 889 | }); 890 | model.set('property', 'foo'); 891 | }); 892 | 893 | test("isValid", function (t) { 894 | t.plan(5); 895 | var model = new Backbone.Model({valid: true}); 896 | model.validate = function (attrs) { 897 | if (!attrs.valid) return "invalid"; 898 | }; 899 | t.equal(model.isValid(), true); 900 | t.equal(model.set({valid: false}, {validate: true}), false); 901 | t.equal(model.isValid(), true); 902 | model.set({valid: false}); 903 | t.equal(model.isValid(), false); 904 | t.ok(!model.set('valid', false, {validate: true})); 905 | }); 906 | 907 | test("#1179 - isValid returns true in the absence of validate.", function (t) { 908 | t.plan(1); 909 | var model = new Backbone.Model(); 910 | model.validate = null; 911 | t.ok(model.isValid()); 912 | }); 913 | 914 | test("#1961 - Creating a model with {validate:true} will call validate and use the error callback", function (t) { 915 | t.plan(1); 916 | var Model = Backbone.Model.extend({ 917 | validate: function (attrs) { 918 | if (attrs.id === 1) return "This shouldn't happen"; 919 | } 920 | }); 921 | var model = new Model({id: 1}, {validate: true}); 922 | t.equal(model.validationError, "This shouldn't happen"); 923 | }); 924 | 925 | test("#2034 - nested set with silent only triggers one change", function (t) { 926 | t.plan(1); 927 | var model = new Backbone.Model(); 928 | model.on('change', function () { 929 | model.set({b: true}, {silent: true}); 930 | t.ok(true); 931 | }); 932 | model.set({a: true}); 933 | }); 934 | 935 | })(); --------------------------------------------------------------------------------