1 /** 2 use this as a more convienient (sometimes) method instead of .prototype.blah.prototype chaining. It tends 3 to be a real javascript way of sub-classing 4 5 @parm {Object} o the original object 6 @returns a new object with the original object as a prototype 7 */ 8 MBX.Constructor = function (o) { 9 function F() {} 10 F.prototype = o; 11 return new F(); 12 }; 13 14 /** 15 Use this to create instances of models and extend all models (and instances of all models) 16 @class 17 */ 18 MBX.JsModel = (function () { 19 /** 20 @memberof MBX.JsModel 21 @namespace 22 */ 23 var publicObj = {}; 24 var currentGUID = 0; 25 26 /** used internally to prevent name collision 27 @private 28 */ 29 var modelCache = {}; 30 31 /** 32 Instances of a Model 33 @name JsModel#instance 34 @class A single instance of a Model 35 @see MBX.Constructor 36 */ 37 var oneJsModelInstance = 38 /** @lends JsModel#instance */ 39 { 40 /** 41 Use this to set attributes of an instance (rather than set them directly). 42 It will automatically create events that will be relevant to controllers 43 @param {String} key the key of the attribute 44 @param value the value to be assigned to the attribute 45 @example 46 modelInstance.set('myAttr', 'foo'); 47 modelInstance.get('myAttr', 'foo'); 48 */ 49 set: function (key, value) { 50 var changed = false; 51 if (this.attributes[key] != value) { 52 changed = true; 53 } 54 var oldValue = this.attributes[key]; 55 this.attributes[key] = value; 56 if (changed) { 57 if (key == this.parentClass.primaryKey) { 58 this._handlePrimaryKeyChange(oldValue, value); 59 } 60 this._fireChangeEvent(key); 61 } 62 return this; 63 }, 64 65 /** 66 Use to manually fire a change event on an attribute. 67 @param {String} key the key of the attribute you want to fire the enent on 68 @example 69 modelInstance.touch("myAttr"); 70 */ 71 touch: function (key) { 72 this._fireChangeEvent(key); 73 }, 74 75 /** 76 Use this to retreive attributes on an object (rather than accessing them directly); 77 @param {String} key 78 @example 79 modelInstance.set('myAttr', 'foo'); 80 modelInstance.get('myAttr', 'foo'); 81 */ 82 get: function (key) { 83 return this.attributes[key]; 84 }, 85 86 /** 87 Take an Object literal and update all attributes on this instance 88 @param {Object} obj the attributes to update as key, value 89 */ 90 updateAttributes: function (obj) { 91 obj = obj || {}; 92 for (k in obj) { 93 if (obj.hasOwnProperty(k)) { 94 this.set(k, obj[k]); 95 } 96 } 97 }, 98 99 /** 100 You should always use this to refer to instances. 101 Model.find uses this to grab objects from the instances 102 @returns returns the primaryKey of the instance 103 @see JsModel.find 104 */ 105 primaryKey: function () { 106 if (this.parentClass.primaryKey) { 107 return this.get(this.parentClass.primaryKey); 108 } else { 109 return this.GUID; 110 } 111 }, 112 113 /** 114 destroy this instance - works just like rails #destroy will fire off the destroy event as well 115 controllers will receive this event by default 116 */ 117 destroy: function () { 118 delete this.parentClass.instanceCache[this.primaryKey()]; 119 this.__MBXJsModelWasDestroyed = true; 120 MBX.emit(this.parentClass.Event.destroyInstance, { object: this }); 121 }, 122 123 124 /** has this instance been destroyed? 125 basically - things can keep a reference to objects that have actually been destroyed 126 this method will let you know if the instance still exists in the model 127 */ 128 isDestroyed: function () { 129 return this.__MBXJsModelWasDestroyed; 130 }, 131 132 /** 133 listen to an attribute of a model. The event passed to your listener will look like 134 { 135 object: //the instance that change, 136 key: 'the key that changed' 137 } 138 @params key {String} the key to listen to 139 @params func {Function} the function to pass to the EventHandler 140 @returns an EventHandler subscription object 141 @see MBX.EventHandler 142 */ 143 observe: function (key, func) { 144 return this.on(key + "_changed", func); 145 }, 146 147 /** @private */ 148 _createGUID: function () { 149 this.GUID = this.parentClass.modelName + "_" + MBX.JsModel.nextGUID(); 150 }, 151 152 _fireChangeEvent: function (key) { 153 if (!this.isDestroyed()) { 154 var changeObject = { 155 object: this, 156 key: key 157 }; 158 MBX.emit(this.parentClass.Event.changeInstance, changeObject); 159 this.emit(key + "_changed", changeObject); 160 } 161 }, 162 163 _handlePrimaryKeyChange: function (oldValue, newValue) { 164 var instanceCache = this.parentClass.instanceCache; 165 if (instanceCache[oldValue]) { 166 delete instanceCache[oldValue] 167 instanceCache[newValue] = this; 168 } 169 } 170 171 }; 172 173 _(oneJsModelInstance).extend(EventEmitter.prototype); 174 175 /** 176 @class A single instance of MBX.JsModel 177 @constructor 178 @throws an error if there's no name, a name already exists or you specified a primaryKey and it wasn't a string 179 */ 180 var JsModel = function (name, opts) { 181 opts = opts || {}; 182 if (!name) { 183 throw new Error("A name must be specified"); 184 } 185 if (modelCache[name]) { 186 throw new Error("The model: " + name + " already exists"); 187 } 188 if (opts.primaryKey && (typeof opts.primaryKey != "string")) { 189 throw new Error("primaryKey specified was not a string"); 190 } 191 _(this).extend(opts); 192 193 /** the model name of this model 194 @type String 195 */ 196 this.modelName = name; 197 198 /** the instances of this model 199 @private 200 */ 201 this.instanceCache = {}; 202 203 /** class level attributes */ 204 this.attributes = {}; 205 206 this.prototypeObject = MBX.Constructor(oneJsModelInstance); 207 208 /** 209 instances get their parentClass assigned this model 210 @name JsModel#instance.parentClass 211 @type JsModel 212 */ 213 this.prototypeObject.parentClass = this; 214 215 /** events that this model will fire. Use this to hook into (at a very low level) events 216 @example 217 MBX.on(MyModel.Event.newInstance, function (instance) { // dostuff } ); 218 */ 219 this.Event = { 220 newInstance: this.modelName + "_new_instance", 221 changeInstance: this.modelName + "_change_instance", 222 destroyInstance: this.modelName + "_destroy_instance", 223 changeAttribute: this.modelName + "_change_attribute" 224 }; 225 226 /** add an instanceMethods attribute to the passed in attributes in order to extend 227 all instances of this model. You can also specify default attributes by adding 228 a defaults attribute to this attribute. 229 @type Object 230 @name JsModel.instanceMethods 231 @example 232 MyModel = MBX.JsModel.create("MyModel", { 233 instanceMethods: { 234 defaults: { 235 myAttribute: "myDefault" 236 }, 237 myMethod: function (method) { 238 return this.get('myAttribute'); 239 } 240 } 241 }); 242 MyModel.create().myMethod() == "myDefault"; 243 */ 244 if (opts.instanceMethods) { 245 _(this.prototypeObject).extend(opts.instanceMethods); 246 } 247 248 modelCache[name] = this; 249 250 if (typeof this.initialize == "function") { 251 this.initialize(); 252 } 253 254 MBX.emit("new_model", { 255 object: this 256 }); 257 }; 258 259 JsModel.prototype = { 260 /** 261 Create an instance of the model 262 @param {Object} attrs attributes you want the new instance to have 263 @returns JsModel#instance 264 @throws "trying to create an instance with the same primary key as another instance" 265 if you are trying to create an instance that already exists 266 @example 267 MyModel = MBX.JsModel.create("MyModel"); 268 var instance = MyModel.create({ 269 myAttr: 'boo' 270 }); 271 instance.get('myAttr') == 'boo'; 272 */ 273 create: function (attrs) { 274 attrs = attrs || {}; 275 var obj = MBX.Constructor(this.prototypeObject); 276 obj.errors = null; 277 obj.attributes = {}; 278 if (obj.defaults) { 279 _(obj.attributes).extend(obj.defaults); 280 _(obj.attributes).each(function (value, key) { 281 if (_(value).isArray()) { 282 obj.defaults[key] = _(value).clone(); 283 } else { 284 if (value != null && typeof value == "object") { 285 obj.defaults[key] = _.clone(value); 286 } 287 } 288 }); 289 } 290 _(obj.attributes).extend(attrs); 291 if (typeof obj.beforeCreate == 'function') { 292 obj.beforeCreate(); 293 } 294 295 if (!obj.errors) { 296 if (this.validateObject(obj)) { 297 obj._createGUID(); 298 this.cacheInstance(obj); 299 MBX.emit(this.Event.newInstance, { 300 object: obj 301 }); 302 if (typeof obj.afterCreate == "function") { 303 obj.afterCreate(); 304 } 305 return obj; 306 } else { 307 throw new Error("trying to create an instance of " + this.modelName + " with the same primary key: '" + obj.get(this.primaryKey) + "' as another instance. Caller was: " + arguments.callee.caller.toString()); 308 } 309 } else { 310 MBX.emit(this.Event.newInstance, { 311 object: obj 312 }); 313 return obj; 314 } 315 }, 316 317 /** this method to get extended later. Used mostly internally. Right now it only verifies 318 that a primaryKey is allowed to be used 319 @param {JsModel#instance} instance the instance that's being validated 320 */ 321 validateObject: function (instance) { 322 // temporarily - this only will validate primary keys 323 if (this.primaryKey) { 324 if (!instance.get(this.primaryKey)) { 325 return false; 326 } 327 if (this.find(instance.get(this.primaryKey))) { 328 return false; 329 } 330 } 331 332 return true; 333 }, 334 335 /** use this to extend all instances of a single model 336 @param {Object} attrs methods and attributes that you want to extend all instances with 337 */ 338 extendInstances: function (attrs) { 339 attrs = attrs || {}; 340 _(this.prototypeObject).extend(attrs); 341 }, 342 343 /** store the instance into the cache. this is mostly used internally 344 @private 345 */ 346 cacheInstance: function (instance) { 347 if (this.primaryKey) { 348 this.instanceCache[instance.get(this.primaryKey)] = instance; 349 } else { 350 this.instanceCache[instance.GUID] = instance; 351 } 352 }, 353 354 /** find a single instance 355 @param {String} primaryKey a JsModel#instance primaryKey 356 @returns an instance of this element 357 @see JsModel#instance.primaryKey 358 */ 359 find: function (primaryKey) { 360 return this.instanceCache[primaryKey]; 361 }, 362 363 /** @returns all instances of this model */ 364 findAll: function () { 365 return _(this.instanceCache).values(); 366 }, 367 368 /** destroy all instances in the instance cache */ 369 flush: function () { 370 this.instanceCache = {}; 371 }, 372 373 /** Gives back the number of cached instances stored in this model 374 @returns {number} number of instances */ 375 count: function () { 376 return this.findAll().length; 377 }, 378 379 /** 380 Use this to set attributes of the model itself (rather than set them directly). 381 It will automatically create events that will be relevant to controllers 382 @param {String} key the key of the attribute 383 @param value the value to be assigned to the attribute 384 @see MBX.JsModel.get 385 @example 386 Model.set('myAttr', 'foo'); 387 Model.get('myAttr', 'foo'); 388 */ 389 set: function (key, value) { 390 var changed = false; 391 if (this.attributes[key] != value) { 392 changed = true; 393 } 394 this.attributes[key] = value; 395 if (changed) { 396 this._fireChangeEvent(key); 397 } 398 }, 399 400 401 /** 402 Use to manually fire a change event on an attribute of a model. 403 @param {String} key the key of the attribute you want to fire the enent on 404 @example 405 Model.touch("myAttr"); 406 */ 407 touch: function (key) { 408 this._fireChangeEvent(key); 409 }, 410 411 /** 412 Use this to retreive attributes on a Model (rather than accessing them directly); 413 @param {String} key 414 @see MBX.JsModel.set 415 @example 416 Model.set('myAttr', 'foo'); 417 Model.get('myAttr', 'foo'); 418 */ 419 get: function (key) { 420 return this.attributes[key]; 421 }, 422 /** 423 A convenience method to subscribe to new model instances 424 @example 425 AModel.onInstanceCreate(function (evt) { console.log(evt) }); 426 */ 427 onInstanceCreate: function (func) { 428 return MBX.on(this.Event.newInstance, func); 429 }, 430 431 /** 432 A convenience method to subscribe to destroying model instances 433 @example 434 AModel.onInstanceDestroy(function (evt) { console.log(evt); }); 435 */ 436 onInstanceDestroy: function (func) { 437 return MBX.on(this.Event.destroyInstance, func); 438 }, 439 440 /** 441 A convenience method to subscribe to changing model instances 442 @example 443 AModel.onInstanceChange(function (evt) { console.log(evt); }); 444 */ 445 onInstanceChange: function (func) { 446 return MBX.on(this.Event.changeInstance, func); 447 }, 448 449 450 /** 451 A convenience method to subscribe to changing model attributes 452 @example 453 AModel.onAttributeChange(function (evt) { console.dir(evt); }); 454 */ 455 onAttributeChange: function (func) { 456 return MBX.on(this.Event.changeAttribute, func); 457 }, 458 459 _fireChangeEvent: function (key) { 460 MBX.emit(this.Event.changeAttribute, { 461 object: this, 462 key: key 463 }); 464 //console.log("fire change event on ", key, " on obj ", this); 465 this.emit(key + "_changed", {object: this}); 466 } 467 }; 468 469 _(JsModel.prototype).extend(EventEmitter.prototype); 470 471 publicObj.Event = { 472 newModel: "new_model" 473 }; 474 475 /** 476 Used for creating a new JsModel 477 @name MBX.JsModel.create 478 @function 479 480 @param {String} name model name used to prevent name collision 481 @param {Object} opts defaults to {} 482 483 @constructs 484 @example 485 var MyModel = MBX.JsModel.create("MyModel"); 486 var instance = MyModel.create(); 487 */ 488 publicObj.create = function (name, opts) { 489 return new JsModel(name, opts); 490 }; 491 492 /** 493 Used internally to find the next GUID 494 @private 495 */ 496 publicObj.nextGUID = function () { 497 return currentGUID++; 498 }; 499 500 /** 501 Extends all JsModels 502 @name MBX.JsModel.extend 503 @function 504 @param {Object} methsAndAttrs the methods and attributes to extend all models with 505 */ 506 publicObj.extend = function (methsAndAttrs) { 507 methsAndAttrs = methsAndAttrs || {}; 508 _(JsModel.prototype).extend(methsAndAttrs); 509 }; 510 511 /** 512 Extend all instances of all models 513 @name MBX.JsModel.extendInstancePrototype 514 @function 515 @param {Object} methsAndAttrs the methods and attributes to extend all models' instances with 516 */ 517 publicObj.extendInstancePrototype = function (methsAndAttrs) { 518 _(oneJsModelInstance).extend(methsAndAttrs); 519 }; 520 521 /** 522 Destroy a model 523 @param {String} name the name of the model 524 @name MBX.JsModel.destroyModel 525 @function 526 */ 527 publicObj.destroyModel = function (name) { 528 delete modelCache[name]; 529 }; 530 531 return publicObj; 532 })(); 533