@converse/skeletor 0.0.8 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -92
- package/dist/skeletor.d.ts +764 -0
- package/dist/skeletor.js +10206 -6249
- package/dist/skeletor.min.js +72 -6
- package/package.json +59 -33
- package/src/collection.ts +846 -0
- package/src/drivers/sessionStorage.ts +297 -0
- package/src/eventemitter.ts +172 -0
- package/src/helpers.ts +179 -0
- package/src/index.ts +84 -0
- package/src/listening.ts +86 -0
- package/src/model.ts +520 -0
- package/src/storage.ts +293 -0
- package/src/types.ts +189 -0
- package/src/utils/events.ts +224 -0
- package/dist/skeletor.js.map +0 -1
- package/dist/skeletor.min.js.map +0 -1
- package/src/collection.js +0 -740
- package/src/drivers/sessionStorage.js +0 -306
- package/src/drivers/sessionStorage.js~ +0 -307
- package/src/element.js +0 -188
- package/src/events.js +0 -328
- package/src/helpers.js +0 -167
- package/src/history.js +0 -301
- package/src/main.js +0 -43
- package/src/model.js +0 -437
- package/src/overview.js +0 -191
- package/src/router.js +0 -119
- package/src/storage.js +0 -258
- package/src/view.js +0 -267
package/src/index.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/* global global */
|
|
2
|
+
import { EventEmitter } from './eventemitter';
|
|
3
|
+
import Storage from './storage';
|
|
4
|
+
import { Collection } from './collection';
|
|
5
|
+
import { Model } from './model';
|
|
6
|
+
import { sync } from './helpers';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @public
|
|
10
|
+
*/
|
|
11
|
+
interface SkeletorType {
|
|
12
|
+
Collection: typeof Collection;
|
|
13
|
+
EventEmitter: typeof EventEmitter;
|
|
14
|
+
Model: typeof Model;
|
|
15
|
+
sync: typeof sync;
|
|
16
|
+
VERSION?: string;
|
|
17
|
+
noConflict?: () => SkeletorType;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @public
|
|
22
|
+
*/
|
|
23
|
+
const skeletor: SkeletorType = {
|
|
24
|
+
Collection,
|
|
25
|
+
EventEmitter,
|
|
26
|
+
Model,
|
|
27
|
+
sync,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Establish the root object, `window` (`self`) in the browser, or `global` on the server.
|
|
31
|
+
// We use `self` instead of `window` for `WebWorker` support.
|
|
32
|
+
const root =
|
|
33
|
+
(typeof self == 'object' && self.self === self && self) ||
|
|
34
|
+
(typeof global == 'object' && global.global === global && global);
|
|
35
|
+
|
|
36
|
+
// Current version of the library. Keep in sync with `package.json`.
|
|
37
|
+
skeletor.VERSION = '0.0.1';
|
|
38
|
+
|
|
39
|
+
// Save the previous value of the `Skeletor` variable, so that it can be
|
|
40
|
+
// restored later on, if `noConflict` is used.
|
|
41
|
+
const previousSkeletor = root.Skeletor;
|
|
42
|
+
|
|
43
|
+
// Runs Skeletor.js in *noConflict* mode, returning the `Skeletor` variable
|
|
44
|
+
// to its previous owner. Returns a reference to this Skeletor object.
|
|
45
|
+
/**
|
|
46
|
+
* @public
|
|
47
|
+
*/
|
|
48
|
+
function noConflict() {
|
|
49
|
+
root.Skeletor = previousSkeletor;
|
|
50
|
+
return this;
|
|
51
|
+
}
|
|
52
|
+
skeletor.noConflict = noConflict;
|
|
53
|
+
|
|
54
|
+
root.Skeletor = skeletor;
|
|
55
|
+
export default skeletor;
|
|
56
|
+
|
|
57
|
+
export { noConflict, Collection, EventEmitter, Model, Storage, sync };
|
|
58
|
+
|
|
59
|
+
export type { LocalForageWithExtensions } from './storage';
|
|
60
|
+
export type {
|
|
61
|
+
ClassConstructor,
|
|
62
|
+
CollectionOptions,
|
|
63
|
+
Comparator,
|
|
64
|
+
EventCallback,
|
|
65
|
+
EventCallbackMap,
|
|
66
|
+
EventContext,
|
|
67
|
+
EventHandlersMap,
|
|
68
|
+
EventListenerMap,
|
|
69
|
+
ModelAttributes,
|
|
70
|
+
ModelOptions,
|
|
71
|
+
ObjectListenedTo,
|
|
72
|
+
ObjectWithId,
|
|
73
|
+
Options,
|
|
74
|
+
SyncOperation,
|
|
75
|
+
SyncOptions,
|
|
76
|
+
EventHandler,
|
|
77
|
+
ListeningType,
|
|
78
|
+
IEventEmitter,
|
|
79
|
+
ListeningMap,
|
|
80
|
+
} from './types';
|
|
81
|
+
|
|
82
|
+
export type { CollectionIterator } from './collection';
|
|
83
|
+
export type { SkeletorType };
|
|
84
|
+
export type { EventEmitterObject } from './eventemitter';
|
package/src/listening.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { eventsApi, onApi, offApi } from './utils/events';
|
|
2
|
+
import type {
|
|
3
|
+
EventCallback,
|
|
4
|
+
IEventEmitter,
|
|
5
|
+
EventCallbackMap,
|
|
6
|
+
EventsApiOptions,
|
|
7
|
+
ListeningType,
|
|
8
|
+
} from './types';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A listening class that tracks and cleans up memory bindings
|
|
12
|
+
* when all callbacks have been offed.
|
|
13
|
+
*/
|
|
14
|
+
class Listening implements ListeningType {
|
|
15
|
+
id: string;
|
|
16
|
+
listener: IEventEmitter;
|
|
17
|
+
obj: any;
|
|
18
|
+
interop: boolean;
|
|
19
|
+
count: number;
|
|
20
|
+
_events?: EventCallbackMap;
|
|
21
|
+
|
|
22
|
+
constructor(listener: IEventEmitter, obj: any) {
|
|
23
|
+
this.id = listener._listenId!;
|
|
24
|
+
this.listener = listener;
|
|
25
|
+
this.obj = obj;
|
|
26
|
+
this.interop = true;
|
|
27
|
+
this.count = 0;
|
|
28
|
+
this._events = undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
start(name: string | EventCallbackMap, callback: EventCallback, context: any, _listening: ListeningType): this {
|
|
32
|
+
const options: EventsApiOptions = {
|
|
33
|
+
context: this.obj,
|
|
34
|
+
ctx: context,
|
|
35
|
+
listening: _listening,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
this._events = eventsApi(onApi, this._events || {}, name, callback, options) as EventCallbackMap;
|
|
39
|
+
|
|
40
|
+
if (_listening) {
|
|
41
|
+
const listeners = this.obj._listeners || (this.obj._listeners = {});
|
|
42
|
+
listeners[this.id] = this;
|
|
43
|
+
|
|
44
|
+
// Allow the listening to use a counter, instead of tracking
|
|
45
|
+
// callbacks for library interop
|
|
46
|
+
this.interop = false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return this;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Stop's listening to a callback (or several).
|
|
54
|
+
* Uses an optimized counter if the listenee uses Backbone.Events.
|
|
55
|
+
* Otherwise, falls back to manual tracking to support events
|
|
56
|
+
* library interop.
|
|
57
|
+
*/
|
|
58
|
+
stop(name: string | EventCallbackMap, callback: EventCallback): void {
|
|
59
|
+
let cleanup: boolean;
|
|
60
|
+
if (this.interop) {
|
|
61
|
+
this._events = eventsApi(offApi, this._events, name, callback, {
|
|
62
|
+
context: undefined,
|
|
63
|
+
listeners: undefined,
|
|
64
|
+
}) as EventCallbackMap;
|
|
65
|
+
cleanup = !this._events;
|
|
66
|
+
} else {
|
|
67
|
+
this.count--;
|
|
68
|
+
cleanup = this.count === 0;
|
|
69
|
+
}
|
|
70
|
+
if (cleanup) this.cleanup();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Cleans up memory bindings between the listener and the listenee.
|
|
75
|
+
*/
|
|
76
|
+
cleanup(): void {
|
|
77
|
+
if (this.listener._listeningTo) {
|
|
78
|
+
delete this.listener._listeningTo[this.obj._listenId!];
|
|
79
|
+
}
|
|
80
|
+
if (!this.interop && this.obj._listeners) {
|
|
81
|
+
delete this.obj._listeners[this.id];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export default Listening;
|
package/src/model.ts
ADDED
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
import { getResolveablePromise, getSyncMethod, urlError, wrapError } from './helpers';
|
|
2
|
+
import clone from 'lodash-es/clone';
|
|
3
|
+
import defaults from 'lodash-es/defaults';
|
|
4
|
+
import defer from 'lodash-es/defer';
|
|
5
|
+
import has from 'lodash-es/has';
|
|
6
|
+
import invert from 'lodash-es/invert';
|
|
7
|
+
import isEmpty from 'lodash-es/isEmpty';
|
|
8
|
+
import isEqual from 'lodash-es/isEqual';
|
|
9
|
+
import iteratee from 'lodash-es/iteratee';
|
|
10
|
+
import omit from 'lodash-es/omit';
|
|
11
|
+
import pick from 'lodash-es/pick';
|
|
12
|
+
import result from 'lodash-es/result';
|
|
13
|
+
import uniqueId from 'lodash-es/uniqueId';
|
|
14
|
+
import { EventEmitterObject } from './eventemitter';
|
|
15
|
+
|
|
16
|
+
// Import types
|
|
17
|
+
import type { Collection } from './collection';
|
|
18
|
+
import type Storage from './storage';
|
|
19
|
+
import { ModelAttributes, ObjectWithId, SyncOperation, ModelOptions, Options } from './types';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @public
|
|
23
|
+
* **Models** are the basic data object in the framework --
|
|
24
|
+
* frequently representing a row in a table in a database on your server.
|
|
25
|
+
* A discrete chunk of data and a bunch of useful, related methods for
|
|
26
|
+
* performing computations and transformations on that data.
|
|
27
|
+
*/
|
|
28
|
+
export class Model<T extends ModelAttributes = ModelAttributes> extends EventEmitterObject {
|
|
29
|
+
_browserStorage?: Storage;
|
|
30
|
+
_changing = false;
|
|
31
|
+
_pending: boolean | ModelOptions = false;
|
|
32
|
+
_previousAttributes?: T;
|
|
33
|
+
_url: string = '';
|
|
34
|
+
_urlRoot: string;
|
|
35
|
+
attributes: T;
|
|
36
|
+
changed: Partial<T> = {};
|
|
37
|
+
cid: string;
|
|
38
|
+
collection?: Collection;
|
|
39
|
+
id: string | number;
|
|
40
|
+
validationError: string | number | null = null;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Create a new model with the specified attributes. A client id (`cid`)
|
|
44
|
+
* is automatically generated and assigned for you.
|
|
45
|
+
*/
|
|
46
|
+
constructor(attributes?: Partial<T>, options?: ModelOptions) {
|
|
47
|
+
super();
|
|
48
|
+
let attrs: Partial<T> = attributes || ({} as Partial<T>);
|
|
49
|
+
options = options || {};
|
|
50
|
+
|
|
51
|
+
this.preinitialize.apply(this, arguments as any);
|
|
52
|
+
this.cid = uniqueId(this.cidPrefix);
|
|
53
|
+
this.attributes = {} as T;
|
|
54
|
+
|
|
55
|
+
if (options.collection) this.collection = options.collection;
|
|
56
|
+
if (options.parse) attrs = this.parse(attrs, options) || ({} as Partial<T>);
|
|
57
|
+
|
|
58
|
+
const default_attrs = result(this, 'defaults');
|
|
59
|
+
attrs = defaults(Object.assign({}, default_attrs, attrs), default_attrs);
|
|
60
|
+
|
|
61
|
+
this.set(attrs, options);
|
|
62
|
+
|
|
63
|
+
this.initialize.apply(this, arguments as any);
|
|
64
|
+
|
|
65
|
+
// Reset changed after initial set
|
|
66
|
+
this.changed = {};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
set browserStorage(storage: Storage) {
|
|
70
|
+
this._browserStorage = storage;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
get browserStorage(): Storage | undefined {
|
|
74
|
+
return this._browserStorage;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The default name for the JSON `id` attribute is `"id"`. MongoDB and
|
|
79
|
+
* CouchDB users may want to set this to `"_id"` (by overriding this getter
|
|
80
|
+
* in a subclass).
|
|
81
|
+
*/
|
|
82
|
+
get idAttribute(): string {
|
|
83
|
+
return 'id';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The prefix is used to create the client id which is used to identify models locally.
|
|
88
|
+
* You may want to override this if you're experiencing name clashes with model ids.
|
|
89
|
+
*/
|
|
90
|
+
get cidPrefix(): string {
|
|
91
|
+
return 'c';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* preinitialize is an empty function by default. You can override it with a function
|
|
96
|
+
* or object. preinitialize will run before any instantiation logic is run in the Model.
|
|
97
|
+
*/
|
|
98
|
+
preinitialize(...args: any[]): void {}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Initialize is an empty function by default. Override it with your own
|
|
102
|
+
* initialization logic.
|
|
103
|
+
*/
|
|
104
|
+
initialize(attrs?: Partial<T>, options?: ModelOptions): void {}
|
|
105
|
+
|
|
106
|
+
validate(attrs: Partial<T> | ObjectWithId, options?: ModelOptions): string | number | null | void {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Return a hash of defaults for the model's attributes.
|
|
112
|
+
*/
|
|
113
|
+
defaults(): Partial<T> {
|
|
114
|
+
return {} as Partial<T>;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Return a copy of the model's `attributes` object.
|
|
119
|
+
*/
|
|
120
|
+
toJSON(): T {
|
|
121
|
+
return clone(this.attributes);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Override this if you need custom syncing semantics for *this* particular model.
|
|
126
|
+
*/
|
|
127
|
+
sync(method: SyncOperation, model: Model<any>, options: Options): any {
|
|
128
|
+
return getSyncMethod(model)(method, model, options);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Get the value of an attribute.
|
|
133
|
+
*/
|
|
134
|
+
get<K extends keyof T>(attr: K): T[K] {
|
|
135
|
+
return this.attributes[attr];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
keys(): string[] {
|
|
139
|
+
return Object.keys(this.attributes);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
values(): T[keyof T][] {
|
|
143
|
+
return Object.values(this.attributes);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
pairs(): [keyof T, T[keyof T]][] {
|
|
147
|
+
return this.entries();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
entries(): [keyof T, T[keyof T]][] {
|
|
151
|
+
return Object.entries(this.attributes) as [keyof T, T[keyof T]][];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
invert(): Record<string, keyof T> {
|
|
155
|
+
return invert(this.attributes);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
pick<K extends keyof T>(...args: K[]): Pick<T, K> {
|
|
159
|
+
if (args.length === 1 && Array.isArray(args[0])) {
|
|
160
|
+
args = args[0] as K[];
|
|
161
|
+
}
|
|
162
|
+
return pick(this.attributes, args) as Pick<T, K>;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
omit<K extends keyof T>(...args: K[]): Omit<T, K> {
|
|
166
|
+
if (args.length === 1 && Array.isArray(args[0])) {
|
|
167
|
+
args = args[0] as K[];
|
|
168
|
+
}
|
|
169
|
+
return omit(this.attributes, args) as Omit<T, K>;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
isEmpty(): boolean {
|
|
173
|
+
return isEmpty(this.attributes);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Returns `true` if the attribute contains a value that is not null
|
|
178
|
+
* or undefined.
|
|
179
|
+
*/
|
|
180
|
+
has(attr: keyof T): boolean {
|
|
181
|
+
return this.get(attr) != null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Special-cased proxy to lodash's `matches` method.
|
|
186
|
+
*/
|
|
187
|
+
matches(attrs: Partial<T>): boolean {
|
|
188
|
+
return !!iteratee(attrs)(this.attributes);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Set a hash of model attributes on the object, firing `"change"`. This is
|
|
193
|
+
* the core primitive operation of a model, updating the data and notifying
|
|
194
|
+
* anyone who needs to know about the change in state. The heart of the beast.
|
|
195
|
+
*/
|
|
196
|
+
set(key: string | Partial<T> | ObjectWithId, val?: any, options?: ModelOptions): boolean | this {
|
|
197
|
+
if (key == null) return this;
|
|
198
|
+
|
|
199
|
+
// Handle both `"key", value` and `{key: value}` -style arguments.
|
|
200
|
+
let attrs: Partial<T> | ObjectWithId;
|
|
201
|
+
if (typeof key === 'object') {
|
|
202
|
+
attrs = key;
|
|
203
|
+
options = val;
|
|
204
|
+
} else {
|
|
205
|
+
attrs = {} as Partial<T>;
|
|
206
|
+
attrs[key as keyof T] = val;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
options = options || {};
|
|
210
|
+
|
|
211
|
+
// Run validation.
|
|
212
|
+
if (!this._validate(attrs, options)) return false;
|
|
213
|
+
|
|
214
|
+
// Extract attributes and options.
|
|
215
|
+
const unset = options.unset;
|
|
216
|
+
const silent = options.silent;
|
|
217
|
+
const changes = [];
|
|
218
|
+
const changing = this._changing;
|
|
219
|
+
this._changing = true;
|
|
220
|
+
|
|
221
|
+
if (!changing) {
|
|
222
|
+
this._previousAttributes = clone(this.attributes);
|
|
223
|
+
this.changed = {};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const current = this.attributes;
|
|
227
|
+
const changed = this.changed;
|
|
228
|
+
const prev = this._previousAttributes;
|
|
229
|
+
|
|
230
|
+
// For each `set` attribute, update or delete the current value.
|
|
231
|
+
for (const attr in attrs) {
|
|
232
|
+
val = attrs[attr as keyof (T | ObjectWithId)];
|
|
233
|
+
if (!isEqual(current[attr as keyof T], val)) changes.push(attr);
|
|
234
|
+
if (!isEqual(prev[attr as keyof T], val)) {
|
|
235
|
+
changed[attr as keyof T] = val;
|
|
236
|
+
} else {
|
|
237
|
+
delete changed[attr as keyof T];
|
|
238
|
+
}
|
|
239
|
+
unset ? delete current[attr as keyof T] : (current[attr as keyof T] = val);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Update the `id`.
|
|
243
|
+
if (this.idAttribute in attrs) this.id = this.get(this.idAttribute);
|
|
244
|
+
|
|
245
|
+
// Trigger all relevant attribute changes.
|
|
246
|
+
if (!silent) {
|
|
247
|
+
if (changes.length) this._pending = options;
|
|
248
|
+
for (let i = 0; i < changes.length; i++) {
|
|
249
|
+
this.trigger('change:' + changes[i], this, current[changes[i]], options);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (changing) return true;
|
|
254
|
+
|
|
255
|
+
if (!silent) {
|
|
256
|
+
// You might be wondering why there's a `while` loop here. Changes can
|
|
257
|
+
// be recursively nested within `"change"` events.
|
|
258
|
+
while (this._pending) {
|
|
259
|
+
options = this._pending as ModelOptions;
|
|
260
|
+
this._pending = false;
|
|
261
|
+
this.trigger('change', this, options);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
this._pending = false;
|
|
265
|
+
this._changing = false;
|
|
266
|
+
return this;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Remove an attribute from the model, firing `"change"`. `unset` is a noop
|
|
271
|
+
* if the attribute doesn't exist.
|
|
272
|
+
*/
|
|
273
|
+
unset(attr: keyof T, options?: ModelOptions): boolean | this {
|
|
274
|
+
return this.set(attr as string, undefined, Object.assign({}, options, { unset: true }));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Clear all attributes on the model, firing `"change"`.
|
|
279
|
+
*/
|
|
280
|
+
clear(options?: ModelOptions): boolean | this {
|
|
281
|
+
const attrs: Partial<T> = {};
|
|
282
|
+
for (const key in this.attributes) attrs[key as keyof T] = undefined;
|
|
283
|
+
return this.set(attrs, Object.assign({}, options, { unset: true }));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Determine if the model has changed since the last `"change"` event.
|
|
288
|
+
* If you specify an attribute name, determine if that attribute has changed.
|
|
289
|
+
*/
|
|
290
|
+
hasChanged(attr?: keyof T): boolean {
|
|
291
|
+
if (attr == null) return !isEmpty(this.changed);
|
|
292
|
+
return has(this.changed, attr as string);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Return an object containing all the attributes that have changed, or
|
|
297
|
+
* false if there are no changed attributes. Useful for determining what
|
|
298
|
+
* parts of a view need to be updated and/or what attributes need to be
|
|
299
|
+
* persisted to the server. Unset attributes will be set to undefined.
|
|
300
|
+
* You can also pass an attributes object to diff against the model,
|
|
301
|
+
* determining if there *would be* a change.
|
|
302
|
+
*/
|
|
303
|
+
changedAttributes(diff?: Partial<T>): Partial<T> | false {
|
|
304
|
+
if (!diff) {
|
|
305
|
+
return this.hasChanged() ? clone(this.changed) : false;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const old = this._changing ? this._previousAttributes : this.attributes;
|
|
309
|
+
const changed: Partial<T> = {};
|
|
310
|
+
let hasChanged = false;
|
|
311
|
+
for (const attr in diff) {
|
|
312
|
+
const val = diff[attr as keyof T];
|
|
313
|
+
if (isEqual(old[attr as keyof T], val)) continue;
|
|
314
|
+
changed[attr as keyof T] = val;
|
|
315
|
+
hasChanged = true;
|
|
316
|
+
}
|
|
317
|
+
return hasChanged ? changed : false;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Get the previous value of an attribute, recorded at the time the last
|
|
322
|
+
* `"change"` event was fired.
|
|
323
|
+
*/
|
|
324
|
+
previous<K extends keyof T>(attr: K): T[K] | null {
|
|
325
|
+
if (attr == null || !this._previousAttributes) return null;
|
|
326
|
+
return this._previousAttributes[attr];
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Get all of the attributes of the model at the time of the previous
|
|
331
|
+
* `"change"` event.
|
|
332
|
+
*/
|
|
333
|
+
previousAttributes(): T | undefined {
|
|
334
|
+
return this._previousAttributes ? clone(this._previousAttributes) : undefined;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Fetch the model from the server, merging the response with the model's
|
|
339
|
+
* local attributes. Any changed attributes will trigger a "change" event.
|
|
340
|
+
*/
|
|
341
|
+
fetch(options: Options = {}): any {
|
|
342
|
+
options = Object.assign({ parse: true }, options);
|
|
343
|
+
|
|
344
|
+
const success = options.success;
|
|
345
|
+
|
|
346
|
+
options.success = (resp: any) => {
|
|
347
|
+
const serverAttrs = options.parse ? this.parse(resp, options) : resp;
|
|
348
|
+
if (!this.set(serverAttrs, options)) return false;
|
|
349
|
+
if (success) success.call(options.context, this, resp, options);
|
|
350
|
+
this.trigger('sync', this, resp, options);
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
wrapError(this, options);
|
|
354
|
+
return this.sync('read', this, options);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Set a hash of model attributes, and sync the model to the server.
|
|
359
|
+
* If the server returns an attributes hash that differs, the model's
|
|
360
|
+
* state will be `set` again.
|
|
361
|
+
*/
|
|
362
|
+
save(key?: string | null | Partial<T>, val?: any, options?: ModelOptions): any {
|
|
363
|
+
// Handle both `"key", value` and `{key: value}` -style arguments.
|
|
364
|
+
let attrs: Partial<T> | undefined;
|
|
365
|
+
if (key == null || typeof key === 'object') {
|
|
366
|
+
attrs = key as Partial<T>;
|
|
367
|
+
options = val as ModelOptions;
|
|
368
|
+
} else {
|
|
369
|
+
attrs = {} as Partial<T>;
|
|
370
|
+
attrs[key as keyof T] = val;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
options = Object.assign({ validate: true, parse: true }, options);
|
|
374
|
+
const wait = options.wait;
|
|
375
|
+
const return_promise = options.promise;
|
|
376
|
+
const promise = return_promise && getResolveablePromise();
|
|
377
|
+
|
|
378
|
+
// If we're not waiting and attributes exist, save acts as
|
|
379
|
+
// `set(attr).save(null, opts)` with validation. Otherwise, check if
|
|
380
|
+
// the model will be valid when the attributes, if any, are set.
|
|
381
|
+
if (attrs && !wait) {
|
|
382
|
+
if (!this.set(attrs, options)) return false;
|
|
383
|
+
} else if (!this._validate(attrs || {}, options)) {
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// After a successful server-side save, the client is (optionally)
|
|
388
|
+
// updated with the server-side state.
|
|
389
|
+
const success = options.success;
|
|
390
|
+
const error = options.error;
|
|
391
|
+
const attributes = this.attributes;
|
|
392
|
+
|
|
393
|
+
options.success = (resp: any) => {
|
|
394
|
+
// Ensure attributes are restored during synchronous saves.
|
|
395
|
+
this.attributes = attributes;
|
|
396
|
+
let serverAttrs = options.parse ? this.parse(resp, options) : resp;
|
|
397
|
+
if (wait) serverAttrs = Object.assign({}, attrs, serverAttrs);
|
|
398
|
+
if (serverAttrs && !this.set(serverAttrs, options)) return false;
|
|
399
|
+
if (success) success.call(options.context, this, resp, options);
|
|
400
|
+
this.trigger('sync', this, resp, options);
|
|
401
|
+
return_promise && promise.resolve();
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
options.error = (model: this, e: any, options: Options) => {
|
|
405
|
+
error && error.call(options.context, model, e, options);
|
|
406
|
+
return_promise && promise.reject(e);
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
wrapError(this, options);
|
|
410
|
+
|
|
411
|
+
// Set temporary attributes if `{wait: true}` to properly find new ids.
|
|
412
|
+
if (attrs && wait) this.attributes = Object.assign({}, attributes, attrs);
|
|
413
|
+
|
|
414
|
+
const method = this.isNew() ? 'create' : options.patch ? 'patch' : 'update';
|
|
415
|
+
if (method === 'patch' && !options.attrs) options.attrs = attrs;
|
|
416
|
+
const xhr = this.sync(method, this, options);
|
|
417
|
+
|
|
418
|
+
// Restore attributes.
|
|
419
|
+
this.attributes = attributes;
|
|
420
|
+
|
|
421
|
+
if (return_promise) {
|
|
422
|
+
return promise;
|
|
423
|
+
} else {
|
|
424
|
+
return xhr;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Destroy this model on the server if it was already persisted.
|
|
430
|
+
* Optimistically removes the model from its collection, if it has one.
|
|
431
|
+
* If `wait: true` is passed, waits for the server to respond before removal.
|
|
432
|
+
*/
|
|
433
|
+
destroy(options?: ModelOptions): any {
|
|
434
|
+
options = options ? clone(options) : {};
|
|
435
|
+
const success = options.success;
|
|
436
|
+
const wait = options.wait;
|
|
437
|
+
|
|
438
|
+
const destroy = () => {
|
|
439
|
+
this.stopListening();
|
|
440
|
+
this.trigger('destroy', this, this.collection, options);
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
options.success = (resp: any) => {
|
|
444
|
+
if (wait) destroy();
|
|
445
|
+
if (success) success.call(options.context, this, resp, options);
|
|
446
|
+
if (!this.isNew()) this.trigger('sync', this, resp, options);
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
let xhr: any = false;
|
|
450
|
+
if (this.isNew()) {
|
|
451
|
+
defer(options.success);
|
|
452
|
+
} else {
|
|
453
|
+
wrapError(this, options);
|
|
454
|
+
xhr = this.sync('delete', this, options);
|
|
455
|
+
}
|
|
456
|
+
if (!wait) destroy();
|
|
457
|
+
return xhr;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
get urlRoot(): string {
|
|
461
|
+
return this._urlRoot;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
set urlRoot(root: string) {
|
|
465
|
+
this._urlRoot = root;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Default URL for the model's representation on the server -- if you're
|
|
470
|
+
* using Backbone's restful methods, override this to change the endpoint
|
|
471
|
+
* that will be called.
|
|
472
|
+
*/
|
|
473
|
+
get url(): string {
|
|
474
|
+
if (this._url) return this._url;
|
|
475
|
+
|
|
476
|
+
const base = result(this, 'urlRoot') || result(this.collection, 'url') || urlError();
|
|
477
|
+
if (this.isNew()) return base as string;
|
|
478
|
+
const id = this.get(this.idAttribute);
|
|
479
|
+
return (base as string).replace(/[^\/]$/, '$&/') + encodeURIComponent(String(id));
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
set url(url: string) {
|
|
483
|
+
this._url = url;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* **parse** converts a response into the hash of attributes to be `set` on
|
|
488
|
+
* the model. The default implementation is just to pass the response along.
|
|
489
|
+
*/
|
|
490
|
+
parse(resp: any, options?: ModelOptions): Partial<T> | null | void {
|
|
491
|
+
return resp;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* A model is new if it has never been saved to the server, and lacks an id.
|
|
496
|
+
*/
|
|
497
|
+
isNew(): boolean {
|
|
498
|
+
return !this.has(this.idAttribute);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Check if the model is currently in a valid state.
|
|
503
|
+
*/
|
|
504
|
+
isValid(options?: ModelOptions): boolean {
|
|
505
|
+
return this._validate({}, Object.assign({}, options, { validate: true }));
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Run validation against the next complete set of model attributes,
|
|
510
|
+
* returning `true` if all is well. Otherwise, fire an `"invalid"` event.
|
|
511
|
+
*/
|
|
512
|
+
_validate(attrs: Partial<T> | ObjectWithId, options?: ModelOptions): boolean {
|
|
513
|
+
if (!options?.validate || !this.validate) return true;
|
|
514
|
+
attrs = Object.assign({}, this.attributes, attrs);
|
|
515
|
+
const error = (this.validationError = this.validate(attrs, options) || null);
|
|
516
|
+
if (!error) return true;
|
|
517
|
+
this.trigger('invalid', this, error, Object.assign(options, { validationError: error }));
|
|
518
|
+
return false;
|
|
519
|
+
}
|
|
520
|
+
}
|