diff --git a/README.md b/README.md index df621316..e59c6c2e 100644 --- a/README.md +++ b/README.md @@ -380,7 +380,7 @@ changeset.rollback(); // returns changeset #### `merge` -Merges 2 valid changesets and returns a new changeset with the same underlying content and validator as the origin. Both changesets must point to the same underlying object. For example: +Merges 2 changesets and returns a new changeset with the same underlying content and validator as the origin. Both changesets must point to the same underlying object. For example: ```js let changesetA = new Changeset(user, validatorFn); diff --git a/addon/index.js b/addon/index.js index 49074ef9..723a3fbf 100644 --- a/addon/index.js +++ b/addon/index.js @@ -4,6 +4,7 @@ import isEmptyObject from 'ember-changeset/utils/computed/is-empty-object'; import isPromise from 'ember-changeset/utils/is-promise'; import isObject from 'ember-changeset/utils/is-object'; import pureAssign from 'ember-changeset/utils/assign'; +import objectWithout from 'ember-changeset/utils/object-without'; import { CHANGESET, isChangeset } from 'ember-changeset/-private/internals'; const { @@ -211,7 +212,6 @@ export function changeset(obj, validateFn = defaultValidatorFn, validationMap = let content = get(this, CONTENT); assert('Cannot merge with a non-changeset', isChangeset(changeset)); assert('Cannot merge with a changeset of different content', get(changeset, CONTENT) === content); - assert('Cannot merge invalid changesets', get(this, 'isValid') && get(changeset, 'isValid')); if (get(this, 'isPristine') && get(changeset, 'isPristine')) { return this; @@ -219,10 +219,17 @@ export function changeset(obj, validateFn = defaultValidatorFn, validationMap = let changesA = get(this, CHANGES); let changesB = get(changeset, CHANGES); - let mergedChanges = pureAssign(changesA, changesB); + let errorsA = get(this, ERRORS); + let errorsB = get(changeset, ERRORS); let newChangeset = new Changeset(content, get(this, VALIDATOR)); + let newErrors = objectWithout(keys(changesB), errorsA); + let newChanges = objectWithout(keys(errorsB), changesA); + let mergedChanges = pureAssign(newChanges, changesB); + let mergedErrors = pureAssign(newErrors, errorsB); + newChangeset[CHANGES] = mergedChanges; - newChangeset.notifyPropertyChange(CHANGES); + newChangeset[ERRORS] = mergedErrors; + newChangeset._notifyVirtualProperties(); return newChangeset; }, diff --git a/addon/utils/object-without.js b/addon/utils/object-without.js new file mode 100644 index 00000000..5dd3f3fa --- /dev/null +++ b/addon/utils/object-without.js @@ -0,0 +1,18 @@ +const { keys } = Object; + +/** + * Merges all sources together, excluding keys in excludedKeys. + * + * @param {Array[String]} excludedKeys + * @param {...Object} sources + * + * @return {Object} + */ +export default function objectWithout(excludedKeys, ...sources) { + return sources.reduce((acc, source) => { + keys(source) + .filter((key) => excludedKeys.indexOf(key) === -1 || !source.hasOwnProperty(key)) + .forEach((key) => acc[key] = source[key]); + return acc; + }, {}); +} diff --git a/app/utils/object-without.js b/app/utils/object-without.js new file mode 100644 index 00000000..93aa4a50 --- /dev/null +++ b/app/utils/object-without.js @@ -0,0 +1 @@ +export { default } from 'ember-changeset/utils/object-without'; diff --git a/tests/unit/changeset-test.js b/tests/unit/changeset-test.js index d3886d27..9e0f8e0b 100644 --- a/tests/unit/changeset-test.js +++ b/tests/unit/changeset-test.js @@ -227,15 +227,27 @@ test('#merge merges 2 valid changesets', function(assert) { assert.deepEqual(get(dummyChangesetB, 'changes'), [{ key: 'lastName', value: 'Bob' }], 'should not mutate second changeset'); }); -test('#merge does not merge invalid changesets', function(assert) { +test('#merge merges invalid changesets', function(assert) { let dummyChangesetA = new Changeset(dummyModel, dummyValidator); let dummyChangesetB = new Changeset(dummyModel, dummyValidator); + let dummyChangesetC = new Changeset(dummyModel, dummyValidator); + dummyChangesetA.set('age', 21); dummyChangesetA.set('name', 'a'); - dummyChangesetB.set('name', 'b'); + dummyChangesetB.set('name', 'Tony Stark'); + dummyChangesetC.set('name', 'b'); - assert.throws(() => dummyChangesetA.merge(dummyChangesetB), ({ message }) => { - return message === 'Assertion Failed: Cannot merge invalid changesets'; - }, 'should throw error'); + let dummyChangesetD = dummyChangesetA.merge(dummyChangesetB); + dummyChangesetD = dummyChangesetD.merge(dummyChangesetC); + + let expectedChanges = [{ key: 'age', value: 21 }]; + let expectedErrors = [{ key: 'name', 'validation': 'too short', value: 'b' }]; + + assert.deepEqual(get(dummyChangesetA, 'isInvalid'), true, 'changesetA is not valid becuase of name'); + assert.deepEqual(get(dummyChangesetB, 'isValid'), true, 'changesetB should be invalid'); + assert.deepEqual(get(dummyChangesetC, 'isInvalid'), true, 'changesetC should be invalid'); + assert.deepEqual(get(dummyChangesetD, 'isInvalid'), true, 'changesetD should be invalid'); + assert.deepEqual(get(dummyChangesetD, 'changes'), expectedChanges, 'should not merge invalid changes'); + assert.deepEqual(get(dummyChangesetD, 'errors'), expectedErrors, 'should assign errors from both changesets'); }); test('#merge does not merge a changeset with a non-changeset', function(assert) { diff --git a/tests/unit/utils/object-without-test.js b/tests/unit/utils/object-without-test.js new file mode 100644 index 00000000..39f33347 --- /dev/null +++ b/tests/unit/utils/object-without-test.js @@ -0,0 +1,15 @@ +import objectWithout from 'dummy/utils/object-without'; +import { module, test } from 'qunit'; + +module('Unit | Utility | object without'); + +test('it exludes the given keys from all merged objects', function(assert) { + let objA = { name: 'Ivan' }; + let objB = { name: 'John' }; + let objC = { age: 27 }; + let objD = objectWithout([ 'age' ], objA, objB, objC); + + assert.deepEqual(objD, { name: 'John' }, 'result only contains name'); + assert.deepEqual(objA.name, 'Ivan', 'does not mutate original object'); + assert.deepEqual(objC.age, 27, 'does not mutate original object'); +});