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