-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathbackbone.intactmodel.js
409 lines (334 loc) · 12.2 KB
/
backbone.intactmodel.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
/**
* Intact Model
* (c) 2014 Carl Törnqvist <calle.tornqvist@gmail.com>
* MIT Licensed
* https://github.com/daytona/backbone.intactmodel
*/
(function (root, factory) {
'use strict';
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['underscore', 'backbone'], function (_, Backbone) {
return factory(_, Backbone);
});
} else if (typeof exports === 'object') {
// Node.
module.exports = factory(require('underscore'), require('backbone'));
} else {
// Browser globals
factory(root._, root.Backbone);
}
}(this, function (_, Backbone) {
'use strict';
/**
* IntactModel constructor
*/
var IntactModel = function (attributes, options) {
// Ensure "properties" and "derived"
this.properties = _.clone(this.properties || {});
this.derived = _.clone(this.derived || {});
// Ensure a blank session state
this.session = {};
// Run super constructor
return Backbone.Model.call(this, attributes, options);
};
_.extend(IntactModel.prototype, Backbone.Model.prototype, {
/**
* Defaults to using the property type validator.
*/
validate: function (attrs, options) {
var keys = _.keys(attrs);
var result = _.bind(testAttributes, this)(attrs);
if (keys.length !== _.keys(result.attributes).length) {
return (new Error('One or more attributes are not valid.'));
}
},
/**
* Compile plain object from "native", session and derived attributes.
*/
compile: function (options) {
var self = this;
var derived = {};
// Compile derived attributes
_(this.derived).each(function (fn, key, list) {
if (!_.isFunction(fn)) return;
derived[key] = fn.call(self, options);
});
// Compile all model attributes
return _.extend(_.clone(this.attributes), _.clone(this.session), derived);
},
/**
* Get value from either native, session or derived property.
*/
get: function (attr) {
var val;
var groups = [this.attributes, this.session, this.derived];
for (var i = 0, l = groups.length; i < l; i += 1) {
if (groups[i].hasOwnProperty(attr)) {
val = groups[i][attr];
break;
}
}
return _.isFunction(val) ? val.call(this) : val;
},
/**
* See that a model has all it's properties assigned
*/
isComplete: function () {
var attrs = this.attributes;
var props = _.keys(this.properties);
return _.every(props, function (prop) {
return attrs.hasOwnProperty(prop);
});
},
/**
* Graceful clear
* Default clear method unset both "native" and session attributes.
* Passing the option `graceful` unsets only the session attributes.
*/
clear: function(options) {
var UNDEFINED;
var attrs = {};
var list = _.clone(this.session);
// Ensure options
options = options || {};
// Optionally clear only session attributes
if (!options.graceful) _.extend(list, this.attributes);
_(list).each(function (val, key, list) {
attrs[key] = UNDEFINED;
});
return this.set(attrs, _.extend({}, options, {unset: true}));
},
/**
* Get hash of model's "native" and session attributes
* that have changes since the last `set`.
*/
changedAttributes: function(diff) {
if (!diff) return this.hasChanged() ? _.clone(this.changed) : false;
var val, changed = false;
var old = this._changing ? this._previousAttributes : _.extend({}, this.attributes, this.session);
for (var attr in diff) {
if (_.isEqual(old[attr], (val = diff[attr]))) continue;
(changed || (changed = {}))[attr] = val;
}
return changed;
},
/**
* Smart set assumption
* Set attributes that pass type validation.
* Attributes that does not pass are ignored.
* Attributes that are not accounted for in `properties` are set to session.
*/
set: function (key, val, options) {
var attrs, attributes, session, handleAttr, unset, silent, changes, changing, prev;
if (!key) return this;
// Wrapper for looping through attributes and checking with/assigning to
// proper model properties
handleAttr = _.bind(function (type, val, key, attrs) {
if (!_.isEqual(this[type][key], val)) changes.push(key);
if (!_.isEqual(prev[key], val)) {
this.changed[key] = val;
} else {
delete this.changed[key];
}
if (unset) {
delete this[type][key];
} else {
this[type][key] = val;
}
}, this);
// Handle both `"key", value` and `{key: value}` -style arguments.
if (_.isObject(key)) {
attrs = key;
options = val;
} else {
(attrs = {})[key] = val;
}
// Ensure options
options = options || {};
// Run validation.
if (!this._validate(attrs, options)) return false;
// Filter out attributes unaccounted for as "session" attributes
attrs = _.bind(testAttributes, this)(attrs);
attributes = attrs.attributes;
session = attrs.session;
// Extract attributes and options.
unset = options.unset;
silent = options.silent;
changes = [];
changing = this._changing;
this._changing = true;
if (!changing) {
this._previousAttributes = _.extend({}, this.attributes, this.session);
this.changed = {};
}
prev = this._previousAttributes;
// Check for changes of `id`.
if (_.has(attributes, this.idAttribute)) this.id = attributes[this.idAttribute];
// Test both "native" and session attributes
_(attrs).each(function (attrType, type, list) {
// Update or delete their respective value
_(attrType).each(_.partial(handleAttr, type));
});
// Trigger all relevant attribute changes.
if (!silent) {
if (changes.length) this._pending = options;
for (var i = 0, l = changes.length; i < l; i += 1) {
this.trigger('change:' + changes[i], this, this.get(changes[i]), options);
}
}
// You might be wondering why there's a `while` loop here. Changes can
// be recursively nested within `"change"` events.
if (changing) return this;
if (!silent) {
while (this._pending) {
options = this._pending;
this._pending = false;
this.trigger('change', this, options);
}
}
this._pending = false;
this._changing = false;
return this;
},
save: function(key, val, options) {
var attrs, method, xhr;
var attributes = this.attributes;
var session = this.session;
// Handle both `"key", value` and `{key: value}` -style arguments.
if (_.isObject(key)) {
attrs = key;
options = val;
} else {
(attrs = {})[key] = val;
}
options = _.extend({validate: true}, options);
// If we're not waiting and attributes exist, save acts as
// `set(attr).save(null, opts)` with validation. Otherwise, check if
// the model will be valid when the attributes, if any, are set.
if (attrs && !options.wait) {
if (!this.set(attrs, options)) return false;
} else {
if (!this._validate(attrs, options)) return false;
}
attrs = _.bind(testAttributes, this)(attrs);
// Set temporary attributes if `{wait: true}`.
if (attrs && options.wait) {
this.attributes = _.extend({}, attributes, attrs.attributes);
this.session = _.extend({}, session, attrs.session);
}
// After a successful server-side save, the client is (optionally)
// updated with the server-side state.
if (options.parse === void 0) options.parse = true;
var model = this;
var success = options.success;
options.success = function(resp) {
// Ensure attributes are restored during synchronous saves.
model.attributes = attributes;
var serverAttrs = model.parse(resp, options);
if (options.wait) serverAttrs = _.extend(attrs.attributes || {}, serverAttrs);
if (_.isObject(serverAttrs) && !model.set(serverAttrs, options)) {
return false;
}
if (success) success(model, resp, options);
model.trigger('sync', model, resp, options);
};
wrapError(this, options);
method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update');
if (method === 'patch') options.attrs = attrs.attributes;
xhr = this.sync(method, this, options);
// Restore attributes.
if (attrs.attributes && options.wait) this.attributes = attributes;
return xhr;
}
});
// Underscore methods that we want to implement on the Model.
var modelMethods = ['keys', 'values', 'pairs', 'invert', 'pick', 'omit'];
// Mix in each Underscore method as a proxy to `Model#attributes`.
_.each(modelMethods, function(method) {
IntactModel.prototype[method] = function() {
var args = Array.prototype.slice.call(arguments);
args.unshift(this.compile());
return _[method].apply(_, args);
};
});
// Custom extend function that extends on some of the model's own properties
var extend = function(protoProps, staticProps) {
var child = Backbone.Model.extend.call(this, protoProps, staticProps);
_.defaults(child.prototype.defaults, this.prototype.defaults);
_.defaults(child.prototype.properties, this.prototype.properties);
_.defaults(child.prototype.derived, this.prototype.derived);
return child;
};
// Attach custom extend method
IntactModel.extend = extend;
// Add to Backbone
Backbone.IntactModel = IntactModel;
// Yanked this part from Henrik Joretegs' Human model:
// https://github.com/HenrikJoreteg/human-model
//
// In backbone, when you add an already instantiated model to a collection
// the collection checks to see if what you're adding is already a model
// the problem is, it does this witn an instanceof check. We're wanting to
// use completely different models so the instanceof will fail even if they
// are "real" models. So we work around this by overwriting this method from
// backbone 1.0.0. The only difference is it looks for an initialize method
// (which both Backbone and HumanModel will always have) to determine whether
// an instantiated model or a simple object is being passed in.
Backbone.Collection.prototype._prepareModel = function (attrs, options) {
if (_.isFunction(attrs.initialize)) {
if (!attrs.collection) attrs.collection = this;
return attrs;
}
options = options || {};
options.collection = this;
var model = new this.model(attrs, options);
if (!model._validate(attrs, options)) {
this.trigger('invalid', this, attrs, options);
return false;
}
return model;
};
/**
* Validate attributes and seperate them as attributes/session
* depending on wether the property is accounted for and of the correct type.
*/
var testAttributes = function (attrs) {
var session = {};
var props = this.properties;
var idAttr = this.idAttribute;
attrs = _.clone(attrs);
_(attrs).each(function (value, key, list) {
var type, length;
var prop = props[key];
// Don't try and handle the id
if (key === idAttr) return;
// Don't allow attributes unaccounted for
if (_.isUndefined(prop)) {
session[key] = value;
delete attrs[key];
return;
}
// Capitalize type to match with underscore method
type = prop.type.replace('integer', 'number');
type = type.slice(0, 1).toUpperCase() + type.slice(1, type.length);
// Test type using Underscore utility fn
if (!_.isUndefined(value) && !_['is' + type](value)) delete attrs[key];
});
// Return attributes sparated as attributes/session
return {
attributes: attrs,
session: session
};
};
// Wrap an optional error callback with a fallback error event.
var wrapError = function(model, options) {
var error = options.error;
options.error = function(resp) {
if (error) error(model, resp, options);
model.trigger('error', model, resp, options);
};
};
// Return class
return IntactModel;
}));