1 /**
  2  * @fileOverview enhanced base for model with sync
  3  * @author yiminghe@gmail.com
  4  */
  5 KISSY.add("mvc/model", function (S, Base) {
  6 
  7     var blacklist = [
  8         "idAttribute",
  9         "clientId",
 10         "urlRoot",
 11         "url",
 12         "parse",
 13         "sync"
 14     ];
 15 
 16     /**
 17      * @name Model
 18      * @class
 19      * Model represent a data record.
 20      * @memberOf MVC
 21      * @extends Base
 22      */
 23     function Model() {
 24         var self = this;
 25         Model.superclass.constructor.apply(self, arguments);
 26         /*
 27          should bubble to its collections
 28          */
 29         self.publish("*Change", {
 30             bubbles:1
 31         });
 32         self.collections = {};
 33     }
 34 
 35     S.extend(Model, Base,
 36         /**
 37          * @lends MVC.Model#
 38          */
 39         {
 40 
 41             /**
 42              * Add current model instance to a specified collection.
 43              * @param {MVC.Collection} c
 44              */
 45             addToCollection:function (c) {
 46                 this.collections[S.stamp(c)] = c;
 47                 this.addTarget(c);
 48             },
 49             /**
 50              * Remove current model instance from a specified collection.
 51              * @param {MVC.Collection} c
 52              */
 53             removeFromCollection:function (c) {
 54                 delete this.collections[S.stamp(c)];
 55                 this.removeTarget(c);
 56             },
 57 
 58             /**
 59              * Get current model 's id.
 60              */
 61             getId:function () {
 62                 return this.get(this.get("idAttribute"));
 63             },
 64 
 65             /**
 66              * Set current model 's id.
 67              * @param id
 68              */
 69             setId:function (id) {
 70                 return this.set(this.get("idAttribute"), id);
 71             },
 72 
 73             __set:function () {
 74                 this.__isModified = 1;
 75                 return Model.superclass.__set.apply(this, arguments);
 76             },
 77 
 78             /**
 79              * whether it is newly created.
 80              * @return {Boolean}
 81              */
 82             isNew:function () {
 83                 return !this.getId();
 84             },
 85 
 86             /**
 87              * whether has been modified since last save.
 88              * @return {Boolean}
 89              */
 90             isModified:function () {
 91                 return !!(this.isNew() || this.__isModified);
 92             },
 93 
 94             /**
 95              * destroy this model and sync with server.
 96              * @param {Object} [opts] destroy config.
 97              * @param {Function} opts.success callback when action is done successfully.
 98              * @param {Function} opts.error callback when error occurs at action.
 99              * @param {Function} opts.complete callback when action is complete.
100              */
101             destroy:function (opts) {
102                 var self = this;
103                 opts = opts || {};
104                 var success = opts.success;
105                 /**
106                  * @ignore
107                  */
108                 opts.success = function (resp) {
109                     var lists = self.collections;
110                     if (resp) {
111                         var v = self.get("parse").call(self, resp);
112                         if (v) {
113                             self.set(v, opts);
114                         }
115                     }
116                     for (var l in lists) {
117                         lists[l].remove(self, opts);
118                         self.removeFromCollection(lists[l]);
119                     }
120                     self.fire("destroy");
121                     success && success.apply(this, arguments);
122                 };
123                 if (!self.isNew() && opts['delete']) {
124                     self.get("sync").call(self, self, 'delete', opts);
125                 } else {
126                     opts.success();
127                     if (opts.complete) {
128                         opts.complete();
129                     }
130                 }
131 
132                 return self;
133             },
134 
135             /**
136              * Load model data from server.
137              * @param {Object} opts Load config.
138              * @param {Function} opts.success callback when action is done successfully.
139              * @param {Function} opts.error callback when error occurs at action.
140              * @param {Function} opts.complete callback when action is complete.
141              */
142             load:function (opts) {
143                 var self = this;
144                 opts = opts || {};
145                 var success = opts.success;
146                 /**
147                  * @ignore
148                  */
149                 opts.success = function (resp) {
150                     if (resp) {
151                         var v = self.get("parse").call(self, resp);
152                         if (v) {
153                             self.set(v, opts);
154                         }
155                     }
156                     self.__isModified = 0;
157                     success && success.apply(this, arguments);
158                 };
159                 self.get("sync").call(self, self, 'read', opts);
160                 return self;
161             },
162 
163             /**
164              * Save current model 's data to server using sync.
165              * @param {Object} opts Save config.
166              * @param {Function} opts.success callback when action is done successfully.
167              * @param {Function} opts.error callback when error occurs at action.
168              * @param {Function} opts.complete callback when action is complete.
169              */
170             save:function (opts) {
171                 var self = this;
172                 opts = opts || {};
173                 var success = opts.success;
174                 /**
175                  * @ignore
176                  */
177                 opts.success = function (resp) {
178                     if (resp) {
179                         var v = self.get("parse").call(self, resp);
180                         if (v) {
181                             self.set(v, opts);
182                         }
183                     }
184                     self.__isModified = 0;
185                     success && success.apply(this, arguments);
186                 };
187                 self.get("sync").call(self, self, self.isNew() ? 'create' : 'update', opts);
188                 return self;
189             },
190 
191             /**
192              * Get json representation for current model.
193              * @return {Object}
194              */
195             toJSON:function () {
196                 var ret = this.getAttrVals();
197                 S.each(blacklist, function (b) {
198                     delete ret[b];
199                 });
200                 return ret;
201             }
202 
203         }, {
204             ATTRS:/**
205              * @lends MVC.Model#
206              */
207             {
208                 /**
209                  * Attribute name used to store id from server.
210                  * Default: "id".
211                  * @type String
212                  */
213                 idAttribute:{
214                     value:'id'
215                 },
216 
217                 /**
218                  * Generated client id.
219                  * Default call S.guid()
220                  * @type Function
221                  */
222                 clientId:{
223                     valueFn:function () {
224                         return S.guid("mvc-client");
225                     }
226                 },
227                 /**
228                  * Called to get url for delete/edit/new current model.
229                  * Default: collection.url+"/"+mode.id
230                  * @type Function
231                  */
232                 url:{
233                     value:url
234                 },
235                 /**
236                  * If current model does not belong to any collection.
237                  * Use this attribute value as collection.url in {@link MVC.Model#url}
238                  * @type String
239                  */
240                 urlRoot:{
241                     value:""
242                 },
243                 /**
244                  * Sync model data with server.
245                  * Default to call {@link MVC.sync}
246                  * @type Function
247                  */
248                 sync:{
249                     value:function () {
250                         S.require("mvc").sync.apply(this, arguments);
251                     }
252                 },
253                 /**
254                  * parse json from server to get attr/value pairs.
255                  * Default to return raw data from server.
256                  * @type function
257                  */
258                 parse:{
259                     value:function (resp) {
260                         return resp;
261                     }
262                 }
263             }
264         });
265 
266     function getUrl(o) {
267         var u;
268         if (o && (u = o.get("url"))) {
269             if (S.isString(u)) {
270                 return u;
271             }
272             return u.call(o);
273         }
274         return u;
275     }
276 
277     function url() {
278         var c,
279             cv,
280             collections = this.collections;
281         for (c in collections) {
282             if (collections.hasOwnProperty(c)) {
283                 cv = collections[c];
284                 break;
285             }
286         }
287         var base = getUrl(cv) || this.get("urlRoot");
288 
289         if (this.isNew()) {
290             return base;
291         }
292 
293         base = base + (base.charAt(base.length - 1) == '/' ? '' : '/');
294         return base + encodeURIComponent(this.getId()) + "/";
295     }
296 
297     return Model;
298 
299 }, {
300     requires:['base']
301 });