forked from jashkenas/backbone
-
Notifications
You must be signed in to change notification settings - Fork 1
/
nextbone.js
2428 lines (2095 loc) · 72 KB
/
nextbone.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
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import {
uniqueId,
extend,
once,
result as getResult,
defaults as getDefaults,
escape,
iteratee as createIteratee,
isEqual,
has,
invert,
omit,
pick,
isString,
isFunction,
isRegExp,
isObject,
negate,
max,
min,
first,
initial,
last,
drop,
without,
difference,
findLastIndex,
shuffle,
sample,
partition,
sortBy,
countBy,
groupBy,
take
} from 'lodash-es';
import { cloneObject, deepCloneLite } from './utils.js';
// Initial Setup
// -------------
// Underscore like functions
var keys = function(obj) {
return obj ? Object.keys(obj) : [];
};
var isArray = Array.isArray;
// Returns whether an object has a given set of `key:value` pairs.
var isMatch = function(object, attrs) {
var objKeys = keys(attrs),
length = objKeys.length;
if (object == null) return !length;
var obj = Object(object);
for (var i = 0; i < length; i++) {
var key = objKeys[i];
if (attrs[key] !== obj[key] || !(key in obj)) return false;
}
return true;
};
var isObjectEmpty = function(obj) {
return obj == null || Object.keys(obj).length === 0;
};
var matches = function(attrs) {
attrs = Object.assign({}, attrs);
return function(obj) {
return isMatch(obj, attrs);
};
};
class ValidationError extends Error {}
// try to get a prop from instance, with fallback to constructor (class property)
var getClassProp = function(obj, prop) {
var value = obj[prop];
return typeof value === 'function' ? value.call(obj) : value ? value : obj.constructor[prop];
};
var ensureClassProperty = function(ctor, prop) {
if (!ctor.hasOwnProperty(prop)) {
var superProperties = Object.getPrototypeOf(ctor)[prop];
ctor[prop] = superProperties ? [...superProperties] : [];
}
return ctor[prop];
};
// Events
// ---------------
// A class to provide a custom event channel. You may bind a callback to an event with `on` or
// remove with `off`; `trigger`-ing an event fires all callbacks in succession.
// It can be be also mixed in to *any object* in order
// var object = {};
// Events.extend(object);
// object.on('expand', function(){ alert('expanded'); });
// object.trigger('expand');
//
// Regular expression used to split event strings.
var eventSplitter = /\s+/;
// A private global variable to share between listeners and listenees.
var _listening;
// Iterates over the standard `event, callback` (as well as the fancy multiple
// space-separated events `"change blur", callback` and jQuery-style event
// maps `{event: callback}`).
var eventsApi = function(iteratee, events, name, callback, opts) {
var i = 0,
names;
if (name && typeof name === 'object') {
// Handle event maps.
if (callback !== void 0 && 'context' in opts && opts.context === void 0)
opts.context = callback;
for (names = keys(name); i < names.length; i++) {
events = eventsApi(iteratee, events, names[i], name[names[i]], opts);
}
} else if (name && eventSplitter.test(name)) {
// Handle space-separated event names by delegating them individually.
for (names = name.split(eventSplitter); i < names.length; i++) {
events = iteratee(events, names[i], callback, opts);
}
} else {
// Finally, standard events.
events = iteratee(events, name, callback, opts);
}
return events;
};
// The reducing API that adds a callback to the `events` object.
var onApi = function(events, name, callback, options) {
if (callback) {
var handlers = events[name] || (events[name] = []);
var context = options.context,
ctx = options.ctx,
listening = options.listening;
if (listening) listening.count++;
handlers.push({
callback: callback,
context: context,
ctx: context || ctx,
listening: listening
});
}
return events;
};
// An try-catch guarded #on function, to prevent poisoning the global
// `_listening` variable.
var tryCatchOn = function(obj, name, callback, context) {
try {
obj.on(name, callback, context);
} catch (e) {
return e;
}
};
// The reducing API that removes a callback from the `events` object.
var offApi = function(events, name, callback, options) {
if (!events) return;
var context = options.context,
listeners = options.listeners;
var i = 0,
names;
// Delete all event listeners and "drop" events.
if (!name && !context && !callback) {
for (names = keys(listeners); i < names.length; i++) {
listeners[names[i]].cleanup();
}
return;
}
names = name ? [name] : keys(events);
for (; i < names.length; i++) {
name = names[i];
var handlers = events[name];
// Bail out if there are no events stored.
if (!handlers) break;
// Find any remaining events.
var remaining = [];
for (var j = 0; j < handlers.length; j++) {
var handler = handlers[j];
if (
(callback && callback !== handler.callback && callback !== handler.callback._callback) ||
(context && context !== handler.context)
) {
remaining.push(handler);
} else {
var listening = handler.listening;
if (listening) listening.off(name, callback);
}
}
// Replace events if there are any remaining. Otherwise, clean up.
if (remaining.length) {
events[name] = remaining;
} else {
delete events[name];
}
}
return events;
};
// Reduces the event callbacks into a map of `{event: onceWrapper}`.
// `offer` unbinds the `onceWrapper` after it has been called.
var onceMap = function(map, name, callback, offer) {
if (callback) {
var fn = (map[name] = once(function(...args) {
offer(name, fn);
callback.apply(this, args);
}));
fn._callback = callback;
}
return map;
};
// Handles triggering the appropriate event callbacks.
var triggerApi = function(objEvents, name, callback, args) {
if (objEvents) {
var events = objEvents[name];
var allEvents = objEvents.all;
if (events && allEvents) allEvents = allEvents.slice();
if (events) triggerEvents(events, args);
if (allEvents) triggerEvents(allEvents, [name].concat(args));
}
return objEvents;
};
// A difficult-to-believe, but optimized internal dispatch function for
// triggering events. Tries to keep the usual cases speedy (most internal
// Nextbone events have 3 arguments).
var triggerEvents = function(events, args) {
var ev,
i = -1,
l = events.length,
a1 = args[0],
a2 = args[1],
a3 = args[2];
switch (args.length) {
case 0:
while (++i < l) (ev = events[i]).callback.call(ev.ctx);
return;
case 1:
while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1);
return;
case 2:
while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2);
return;
case 3:
while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3);
return;
default:
while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args);
return;
}
};
var eventsMethods = ['on', 'listenTo', 'off', 'stopListening', 'once', 'listenToOnce', 'trigger'];
class Events {
// Extend an object with Events methods
static extend(obj) {
eventsMethods.forEach(method => {
obj[method] = Events.prototype[method];
});
return obj;
}
constructor() {
const onEvents = this.constructor.__onEvents;
if (onEvents) {
onEvents.forEach(({ eventName, listener }) => this.on(eventName, listener));
}
}
// Bind an event to a `callback` function. Passing `"all"` will bind
// the callback to all events fired.
on(name, callback, context) {
this._events = eventsApi(onApi, this._events || {}, name, callback, {
context: context,
ctx: this,
listening: _listening
});
if (_listening) {
var listeners = this._listeners || (this._listeners = {});
listeners[_listening.id] = _listening;
// Allow the listening to use a counter, instead of tracking
// callbacks for library interop
_listening.interop = false;
}
return this;
}
// Inversion-of-control versions of `on`. Tell *this* object to listen to
// an event in another object... keeping track of what it's listening to
// for easier unbinding later.
listenTo(obj, name, callback) {
if (!obj) return this;
var id = obj._listenId || (obj._listenId = uniqueId('l'));
var listeningTo = this._listeningTo || (this._listeningTo = {});
var listening = (_listening = listeningTo[id]);
// This object is not listening to any other events on `obj` yet.
// Setup the necessary references to track the listening callbacks.
if (!listening) {
this._listenId || (this._listenId = uniqueId('l'));
listening = _listening = listeningTo[id] = new Listening(this, obj);
}
// Bind callbacks on obj.
var error = tryCatchOn(obj, name, callback, this);
_listening = void 0;
if (error) throw error;
// If the target obj is not an Events instance, track events manually.
if (listening.interop) listening.on(name, callback);
return this;
}
// Remove one or many callbacks. If `context` is null, removes all
// callbacks with that function. If `callback` is null, removes all
// callbacks for the event. If `name` is null, removes all bound
// callbacks for all events.
off(name, callback, context) {
if (!this._events) return this;
this._events = eventsApi(offApi, this._events, name, callback, {
context: context,
listeners: this._listeners
});
return this;
}
// Tell this object to stop listening to either specific events ... or
// to every object it's currently listening to.
stopListening(obj, name, callback) {
var listeningTo = this._listeningTo;
if (!listeningTo) return this;
var ids = obj ? [obj._listenId] : keys(listeningTo);
for (var i = 0; i < ids.length; i++) {
var listening = listeningTo[ids[i]];
// If listening doesn't exist, this object is not currently
// listening to obj. Break out early.
if (!listening) break;
listening.obj.off(name, callback, this);
if (listening.interop) listening.off(name, callback);
}
if (isObjectEmpty(listeningTo)) this._listeningTo = void 0;
return this;
}
// Bind an event to only be triggered a single time. After the first time
// the callback is invoked, its listener will be removed. If multiple events
// are passed in using the space-separated syntax, the handler will fire
// once for each event, not once for a combination of all events.
once(name, callback, context) {
// Map the event into a `{event: once}` object.
var events = eventsApi(onceMap, {}, name, callback, this.off.bind(this));
if (typeof name === 'string' && context == null) callback = void 0;
return this.on(events, callback, context);
}
// Inversion-of-control versions of `once`.
listenToOnce(obj, name, callback) {
// Map the event into a `{event: once}` object.
var events = eventsApi(onceMap, {}, name, callback, this.stopListening.bind(this, obj));
return this.listenTo(obj, events);
}
// Trigger one or many events, firing all bound callbacks. Callbacks are
// passed the same arguments as `trigger` is, apart from the event name
// (unless you're listening on `"all"`, which will cause your callback to
// receive the true name of the event as the first argument).
trigger(name, ...args) {
if (!this._events) return this;
eventsApi(triggerApi, this._events, name, void 0, args);
return this;
}
}
// A listening class that tracks and cleans up memory bindings
// when all callbacks have been offed.
class Listening {
constructor(listener, obj) {
this.id = listener._listenId;
this.listener = listener;
this.obj = obj;
this.interop = true;
this.count = 0;
this._events = void 0;
}
// Offs a callback (or several).
// Uses an optimized counter if the listenee uses Events.
// Otherwise, falls back to manual tracking to support events
// library interop.
off(name, callback) {
var cleanup;
if (this.interop) {
this._events = eventsApi(offApi, this._events, name, callback, {
context: void 0,
listeners: void 0
});
cleanup = !this._events;
} else {
this.count--;
cleanup = this.count === 0;
}
if (cleanup) this.cleanup();
}
// Cleans up memory bindings between the listener and the listenee.
cleanup() {
delete this.listener._listeningTo[this.obj._listenId];
if (!this.interop) delete this.obj._listeners[this.id];
}
}
Listening.prototype.on = Events.prototype.on;
const registerOnEvent = (ctor, eventName, listener) => {
const onEvents = ensureClassProperty(ctor, '__onEvents');
onEvents.push({ eventName, listener });
};
// Method decorator to listen to an event from same instance
const on = eventName => (protoOrDescriptor, methodName, propertyDescriptor) => {
if (typeof methodName !== 'string') {
const { kind, key, placement, descriptor, initializer } = protoOrDescriptor;
return {
kind,
placement,
descriptor,
initializer,
key,
finisher(ctor) {
registerOnEvent(ctor, eventName, descriptor.value);
}
};
}
// legacy decorator spec
registerOnEvent(protoOrDescriptor.constructor, eventName, propertyDescriptor.value);
};
const registerObservableProperty = (ctor, name, key) => {
const desc = {
get() {
return this[key];
},
set(value) {
const oldValue = this[key];
if (value === oldValue) return;
this[key] = value;
this.trigger(`change:${name}`, this, value, oldValue);
this.trigger('change', this);
},
configurable: true,
enumerable: true
};
Object.defineProperty(ctor.prototype, name, desc);
};
// Class field decorator to make it observable
const observable = (protoOrDescriptor, fieldName, propertyDescriptor) => {
const isLegacy = typeof fieldName === 'string';
const name = isLegacy ? fieldName : protoOrDescriptor.key;
const key = typeof name === 'symbol' ? Symbol() : `__${name}`;
if (!isLegacy) {
const { kind, placement, descriptor, initializer } = protoOrDescriptor;
return {
kind,
placement,
descriptor,
initializer,
key,
finisher(ctor) {
registerObservableProperty(ctor, name, key);
}
};
}
registerObservableProperty(protoOrDescriptor.constructor, name, key);
};
var wrapSync = function(model, response, options) {
model.isLoading = true;
model.trigger('request', model, response, options);
response.then(
function(data) {
model.isLoading = false;
if (options.success) options.success(data);
model.trigger('load', model);
},
function(error) {
model.isLoading = false;
if (options.error) options.error.call(options.context, error);
model.trigger('load', model);
}
);
return response;
};
// Model
// --------------
// **Models** are the basic data object in the library --
// frequently representing a row in a table in a database on your server.
// A discrete chunk of data and a bunch of useful, related methods for
// performing computations and transformations on that data.
// Create a new model with the specified attributes. A client id (`cid`)
// is automatically generated and assigned for you.
class Model extends Events {
// The default name for the JSON `id` attribute is `"id"`. MongoDB and
// CouchDB users may want to set this to `"_id"`.
// static idAttribute = 'id';
// The prefix is used to create the client id which is used to identify models locally.
// You may want to override this if you're experiencing name clashes with model ids.
// static cidPrefix = 'c';
constructor(attributes, options) {
super();
this.isLoading = false;
// The value returned during the last failed validation.
this.validationError = null;
var attrs = attributes || {};
options || (options = {});
this.preinitialize.apply(this, arguments);
this.cid = uniqueId(this.constructor.cidPrefix || 'c');
this.attributes = {};
if (options.collection) this.collection = options.collection;
if (options.parse) attrs = this.parse(attrs, options) || {};
if (options.clone) {
this.assign(options.clone);
} else {
var defaults = getClassProp(this, 'defaults');
attrs = getDefaults(extend({}, defaults, attrs), defaults);
this.set(attrs, options);
}
// A hash of attributes whose current and previous value differ.
this.changed = {};
this.initialize.apply(this, arguments);
}
// preinitialize is an empty function by default. You can override it with a function
// or object. preinitialize will run before any instantiation logic is run in the Model.
preinitialize() {}
// Initialize is an empty function by default. Override it with your own
// initialization logic.
initialize() {}
// Return a copy of the model's `attributes` object.
toJSON(options) {
return cloneObject(this.attributes);
}
// Get the Model class idAttribute. Read only.
get idAttribute() {
return this.constructor.idAttribute || 'id';
}
_sync(method, options) {
return wrapSync(this, this.sync(method, options), options);
}
// Proxy `sync.handler` by default -- but override this if you need
// custom syncing semantics for *this* particular model.
sync(method, options, requestHandler) {
return sync.handler(method, this, options, requestHandler);
}
// Get the value of an attribute.
get(attr) {
return this.attributes[attr];
}
// Get the HTML-escaped value of an attribute.
escape(attr) {
return escape(this.get(attr));
}
// Returns `true` if the attribute contains a value that is not null
// or undefined.
has(attr) {
return this.get(attr) != null;
}
// Special-cased proxy to lodash-es's `matches` method.
matches(attrs) {
return !!createIteratee(attrs, this)(this.attributes);
}
// Set a hash of model attributes on the object, firing `"change"`. This is
// the core primitive operation of a model, updating the data and notifying
// anyone who needs to know about the change in state. The heart of the beast.
set(key, val, options) {
if (key == null) return this;
// Handle both `"key", value` and `{key: value}` -style arguments.
var attrs;
if (typeof key === 'object') {
attrs = key;
options = val;
} else {
(attrs = {})[key] = val;
}
options || (options = {});
// Run validation.
this._validate(attrs, options);
// Extract attributes and options.
var unset = options.unset;
var silent = options.silent;
var reset = options.reset;
var changes = [];
var changing = this._changing;
this._changing = true;
if (!changing) {
this._previousAttributes = cloneObject(this.attributes);
this.changed = {};
}
var current = this.attributes;
var changed = this.changed;
var prev = this._previousAttributes;
// For each `set` attribute, update or delete the current value.
for (var attr in attrs) {
val = attrs[attr];
if (!isEqual(current[attr], val)) changes.push(attr);
if (!isEqual(prev[attr], val)) {
changed[attr] = val;
} else {
delete changed[attr];
}
unset ? delete current[attr] : (current[attr] = val);
}
if (reset) {
for (var currAttr in current) {
if (!(currAttr in attrs)) {
delete current[currAttr];
changes.push(currAttr);
changed[currAttr] = void 0;
}
}
}
// Update the `id`.
var idAttribute = this.constructor.idAttribute || 'id';
if (idAttribute in attrs) this.id = this.get(idAttribute);
// Trigger all relevant attribute changes.
if (!silent) {
if (changes.length) this._pending = options;
for (var i = 0; i < changes.length; i++) {
this.trigger('change:' + changes[i], this, current[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;
}
// Remove an attribute from the model, firing `"change"`. `unset` is a noop
// if the attribute doesn't exist.
unset(attr, options) {
return this.set(attr, void 0, extend({}, options, { unset: true }));
}
// Clear all attributes on the model, firing `"change"`.
clear(options) {
var attrs = {};
for (var key in this.attributes) attrs[key] = void 0;
return this.set(attrs, extend({}, options, { unset: true }));
}
// Determine if the model has changed since the last `"change"` event.
// If you specify an attribute name, determine if that attribute has changed.
hasChanged(attr) {
if (attr == null) return !isObjectEmpty(this.changed);
return has(this.changed, attr);
}
// Return an object containing all the attributes that have changed, or
// false if there are no changed attributes. Useful for determining what
// parts of a view need to be updated and/or what attributes need to be
// persisted to the server. Unset attributes will be set to undefined.
// You can also pass an attributes object to diff against the model,
// determining if there *would be* a change.
changedAttributes(diff) {
if (!diff) return this.hasChanged() ? cloneObject(this.changed) : false;
var old = this._changing ? this._previousAttributes : this.attributes;
var changed = {};
var hasChanged;
for (var attr in diff) {
var val = diff[attr];
if (isEqual(old[attr], val)) continue;
changed[attr] = val;
hasChanged = true;
}
return hasChanged ? changed : false;
}
// Get the previous value of an attribute, recorded at the time the last
// `"change"` event was fired.
previous(attr) {
if (attr == null || !this._previousAttributes) return null;
return this._previousAttributes[attr];
}
// Get all of the attributes of the model at the time of the previous
// `"change"` event.
previousAttributes() {
return cloneObject(this._previousAttributes);
}
// Fetch the model from the server, merging the response with the model's
// local attributes. Any changed attributes will trigger a "change" event.
fetch(options) {
options = extend({ parse: true }, options);
var model = this;
var success = options.success;
options.success = function(resp) {
var serverAttrs = options.parse ? model.parse(resp, options) : resp;
if (!model.set(serverAttrs, options)) return false;
if (success) success.call(options.context, model, resp, options);
model.trigger('sync', model, resp, options);
};
wrapError(this, options);
return this._sync('read', options);
}
// Set a hash of model attributes, and sync the model to the server.
// If the server returns an attributes hash that differs, the model's
// state will be `set` again.
save(key, val, options) {
// Handle both `"key", value` and `{key: value}` -style arguments.
var attrs;
if (key == null || typeof key === 'object') {
attrs = key;
options = val;
} else {
(attrs = {})[key] = val;
}
options = extend({ validate: true, parse: true }, options);
var wait = options.wait;
// 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 && !wait) {
this.set(attrs, options);
if (this.validationError) return Promise.reject(new ValidationError());
} else if (!this._validate(attrs, options)) {
return Promise.reject(new ValidationError());
}
// After a successful server-side save, the client is (optionally)
// updated with the server-side state.
var model = this;
var success = options.success;
var attributes = this.attributes;
options.success = function(resp) {
// Ensure attributes are restored during synchronous saves.
model.attributes = attributes;
var serverAttrs = options.parse ? model.parse(resp, options) : resp;
if (wait) serverAttrs = extend({}, attrs, serverAttrs);
if (serverAttrs && !model.set(serverAttrs, options)) return false;
if (success) success.call(options.context, model, resp, options);
model.trigger('sync', model, resp, options);
};
wrapError(this, options);
// Set temporary attributes if `{wait: true}` to properly find new ids.
if (attrs && wait) this.attributes = extend({}, attributes, attrs);
var method = this.isNew() ? 'create' : options.patch ? 'patch' : 'update';
if (method === 'patch' && !options.attrs) options.attrs = attrs;
var xhr = this._sync(method, options);
// Restore attributes.
this.attributes = attributes;
return xhr;
}
// Destroy this model on the server if it was already persisted.
// Optimistically removes the model from its collection, if it has one.
// If `wait: true` is passed, waits for the server to respond before removal.
destroy(options) {
options = options ? cloneObject(options) : {};
var model = this;
var success = options.success;
var wait = options.wait;
var destroy = function() {
model.stopListening();
model.trigger('destroy', model, model.collection, options);
};
options.success = function(resp) {
if (wait) destroy();
if (success) success.call(options.context, model, resp, options);
if (!model.isNew()) model.trigger('sync', model, resp, options);
};
var result;
if (this.isNew()) {
result = Promise.resolve().then(options.success);
} else {
wrapError(this, options);
result = this._sync('delete', options);
}
if (!wait) destroy();
return result;
}
// Default URL for the model's representation on the server -- if you're
// using Nextbone's restful methods, override this to change the endpoint
// that will be called.
url() {
var base = getResult(this, 'urlRoot') || getResult(this.collection, 'url') || urlError();
if (this.isNew()) return base;
var id = this.get(this.constructor.idAttribute || 'id');
return base.replace(/[^\/]$/, '$&/') + encodeURIComponent(id);
}
// **parse** converts a response into the hash of attributes to be `set` on
// the model. The default implementation is just to pass the response along.
parse(resp, options) {
return resp;
}
// Create a new model with identical attributes to this one.
clone() {
return new this.constructor(null, { clone: this });
}
// assign attributes from an object or another model
assign(source, options) {
if (source instanceof Model) {
source.assignTo(this, options);
} else if (isObject(source)) {
this.set(deepCloneLite(source), options);
}
}
// inversion of control for assign
assignTo(target, options) {
target.set(deepCloneLite(this.attributes), options);
}
// A model is new if it has never been saved to the server, and lacks an id.
isNew() {
return !this.has(this.constructor.idAttribute || 'id');
}
// Check if the model is currently in a valid state.
isValid(options) {
return this._validate({}, extend({}, options, { validate: true }));
}
// underscore methods
keys() {
return keys(this.attributes);
}
values() {
return Object.values(this.attributes);
}
pairs() {
return Object.entries(this.attributes);
}
entries() {
return Object.entries(this.attributes);
}
invert() {
return invert(this.attributes);
}
pick(...args) {
return pick(this.attributes, ...args);
}
omit(...args) {
return omit(this.attributes, ...args);
}
isEmpty() {
return isObjectEmpty(this.attributes);
}
// Run validation against the next complete set of model attributes,
// returning `true` if all is well. Otherwise, fire an `"invalid"` event.
_validate(attrs, options) {
if (!options.validate || !this.validate) return true;
attrs = extend({}, this.attributes, attrs);
var error = (this.validationError = this.validate(attrs, options) || null);
if (!error) return true;
this.trigger('invalid', this, error, options);
return false;
}
}
// Defining an @@iterator method implements JavaScript's Iterable protocol.
// This value is found at Symbol.iterator.
var $$iterator = Symbol.iterator;
// Collection
// -------------------
// If models tend to represent a single row of data, a Collection is
// more analogous to a table full of data ... or a small slice or page of that
// table, or a collection of rows that belong together for a particular reason
// -- all of the messages in this particular folder, all of the documents
// belonging to this particular author, and so on. Collections maintain
// indexes of their models, both in order, and for lookup by `id`.
// Create a new **Collection**, perhaps to contain a specific type of `model`.
// If a `comparator` is specified, the Collection will maintain
// its models in sort order, as they're added and removed.
class Collection extends Events {
// The default model for a collection is just a **Model**.
// This should be overridden in most cases.
// static model = Model;
constructor(models, options) {
super();
this.isLoading = false;
options || (options = {});
this.preinitialize.apply(this, arguments);
if (options.model) this.model = options.model;
if (options.comparator !== void 0) this.comparator = options.comparator;
this._reset();
this.initialize.apply(this, arguments);
if (models) this.reset(models, extend({ silent: true }, options));
}
// preinitialize is an empty function by default. You can override it with a function
// or object. preinitialize will run before any instantiation logic is run in the Collection.
preinitialize() {}
// Initialize is an empty function by default. Override it with your own
// initialization logic.
initialize() {}
// The JSON representation of a Collection is an array of the
// models' attributes.
toJSON(options) {
return this.map(function(model) {
return model.toJSON(options);
});
}
_sync(method, options) {
return wrapSync(this, this.sync(method, options), options);
}
// Proxy `sync.handler` by default.
sync(method, options) {
return sync.handler(method, this, options);
}