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