From bf96890656e1f4f3a724137906f0a7cabceb0464 Mon Sep 17 00:00:00 2001 From: Rudi Theunissen Date: Thu, 2 Nov 2017 15:19:50 +1300 Subject: [PATCH] Add useFirstErrorOnly option to Model --- docs/_includes/models.md | 4 +++- src/Structures/Base.js | 2 +- src/Structures/Model.js | 36 ++++++++++++++++++++++++----------- test/Structures/Model.spec.js | 20 +++++++++++++++---- 4 files changed, 45 insertions(+), 17 deletions(-) diff --git a/docs/_includes/models.md b/docs/_includes/models.md index 4bb6dcf..1c7fcca 100644 --- a/docs/_includes/models.md +++ b/docs/_includes/models.md @@ -85,7 +85,9 @@ task2.getOption('editable'); // false |-------------------------+------------+-------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `patch` | `Boolean` | `false` | Whether this model should perform a "patch" on update (only send attributes that have changed). | |-------------------------+------------+-------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `patchUnchanged` | `Boolean` | `false` | Whether this model should perform a "patch" on update if no changes have been made. | +| `saveUnchanged` | `Boolean` | `true` | Whether this model should save even if no attributes have changed. If set to `false` and no changes have been made, the request will be a considered a success. | +|-------------------------+------------+-------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `useFirstErrorOnly` | `Boolean` | `false` | Whether this model should only use the first validation error it receives, rather than an array of errors. | |-------------------------+------------+-------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `validateOnChange` | `Boolean` | `false` | Whether this model should validate an attribute after it has changed. This would only affect the errors of the changed attribute and will only be applied if the value is not blank. | |-------------------------+------------+-------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| diff --git a/src/Structures/Base.js b/src/Structures/Base.js index ac23533..79ee125 100644 --- a/src/Structures/Base.js +++ b/src/Structures/Base.js @@ -143,7 +143,7 @@ class Base { // Default route parameter interpolation pattern. routeParameterPattern: this.getDefaultRouteParameterPattern(), - // + // The HTTP status code to use for indicating a validation error. validationErrorStatus: 422, } } diff --git a/src/Structures/Model.js b/src/Structures/Model.js index 2b4d7a9..5f5a130 100644 --- a/src/Structures/Model.js +++ b/src/Structures/Model.js @@ -165,9 +165,15 @@ class Model extends Base { // which will only send changed attributes in the request. patch: false, - // + // Whether this model should save even if no attributes have changed + // since the last time they were synced. If set to `false` and no + // changes have been made, the request will be a considered a success. saveUnchanged: true, + // Whether this model should only use the first validation error it + // receives, rather than an array of errors. + useFirstErrorOnly: false, + // Whether this model should validate an attribute that has changed. // This would only affect the errors of the changed attribute and // will only be applied if the value is not a blank string. @@ -462,7 +468,7 @@ class Model extends Base { Vue.set(this._attributes, attribute, value); // Only emit `change` if the value has changed. - this.emit('change', {attribute, previous, value }); + this.emit('change', {attribute, previous, value}); // If on-the-fly validation is enabled and the value is not blank, // validate the attribute. It's important to skip blank strings @@ -701,7 +707,7 @@ class Model extends Base { Vue.set(this, 'fatal', false); Vue.set(this, 'loading', false); - this.emit('fetch', {error: null }); + this.emit('fetch', {error: null}); } /** @@ -872,6 +878,13 @@ class Model extends Base { * @param {Object} errors */ setErrors(errors) { + errors = _.defaultTo(errors, {}); + + // Only pick the first error if we don't want to use an array. + if (this.getOption('useFirstErrorOnly')) { + errors = _.mapValues(errors, _.head); + } + Vue.set(this, '_errors', errors); } @@ -879,7 +892,7 @@ class Model extends Base { * @returns {Object} Validation errors on this model. */ getErrors() { - return this._errors = _.defaultTo(this._errors, {}); + return this._errors; } /** @@ -893,7 +906,7 @@ class Model extends Base { /** * Called when a save request was successful. * - * @param {Object} response + * @param {Object|null} response */ onSaveSuccess(response) { @@ -901,7 +914,9 @@ class Model extends Base { this.clearErrors(); // Update this model with the data that was returned in the response. - this.update(response.getData()); + if (response) { + this.update(response.getData()); + } Vue.set(this, 'saving', false); Vue.set(this, 'fatal', false); @@ -909,8 +924,7 @@ class Model extends Base { // Automatically add to all registered collections. this.addToAllCollections(); - // - this.emit('save', {error: null }); + this.emit('save', {error: null}); } /** @@ -972,7 +986,7 @@ class Model extends Base { Vue.set(this, 'deleting', false); Vue.set(this, 'fatal', false); - this.emit('delete', {error: null }); + this.emit('delete', {error: null}); } /** @@ -1033,9 +1047,9 @@ class Model extends Base { return false; } - // + // Don't save if no data has changed, but consider it a success. if ( ! this.getOption('saveUnchanged') && ! this.changed()) { - return false; + return true; } // Mutate attribute before we save if required to do so. diff --git a/test/Structures/Model.spec.js b/test/Structures/Model.spec.js index cb4aa28..6700c3c 100644 --- a/test/Structures/Model.spec.js +++ b/test/Structures/Model.spec.js @@ -69,8 +69,17 @@ describe('Model', () => { describe('errors', () => { it('should return errors', () => { let m = new Model(); - m.setErrors({a: 1}); - expect(m.errors).to.deep.equal({a: 1}); + m.setErrors({a: ['Invalid!']}); + expect(m.errors).to.deep.equal({a: ['Invalid!']}); + }) + + it('should only return the first error if `useFirstErrorOnly` is set', () => { + let m = new Model({}, null, {useFirstErrorOnly: true}); + m.setErrors({a: [1, 2, 3], b: [4, 5]}); + expect(m.errors).to.deep.equal({ + a: 1, + b: 4, + }); }) }) @@ -2295,14 +2304,17 @@ describe('Model', () => { }) }) - it('should skip if no attributes have changed when option is enabled', (done) => { + it('should be successful if no attributes have changed when option is enabled', (done) => { let m = new class extends Model { defaults() { return {id: 1, name: 'Fred'}} routes() { return {save: '/collection/save/{id}'}} options() { return {saveUnchanged: false} } } - expectRequestToBeSkipped(m.save(), done); + m.save().then((response) => { + expect(response).to.be.null; + done(); + }) }) it('should pass if no validation rules are configured', () => {