@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/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';
@@ -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
+ }