-
-
Notifications
You must be signed in to change notification settings - Fork 141
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Added option to allow merging invalid changesets #74
Changes from 12 commits
aa3c60c
6a3ac98
78ec424
2996eb0
49149c9
f8ee915
50e067f
7515a4a
507433d
ebfbc9a
e60de10
fb031f2
f3e7a72
864c378
bbb6e6d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -380,20 +380,24 @@ 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); | ||
let changesetB = new Changeset(user, validatorFn); | ||
changesetA.set('firstName', 'Jim'); | ||
changesetA.get('errors') // [{key: 'firstName', value: 'Jim', validations: ['Firstname cannot be Jim, sorry Jim']}] | ||
changesetB.set('firstName', 'Jimmy'); | ||
changesetB.get('errors') // [] | ||
changesetB.set('lastName', 'Fallon'); | ||
let changesetC = changesetA.merge(changesetB); | ||
changesetC.execute(); | ||
user.get('firstName'); // "Jimmy" | ||
user.get('lastName'); // "Fallon" | ||
``` | ||
|
||
When merging, the changeset being merged in takes precedence. For example: if `B` contains errors, the errors from `B` will be merged, and the invalid attributes on `B` will not be merged from `A` even if they're valid on `A`. Likewise, if `A` contains errors on attributes which are valid on `B`, the value from `B` will be used and the errors for those keys will not be merged from `A`. This is to prevent the new changeset from containing `changes` and `errors` on the same keys, which is a guarantee of a normal changeset. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is still confusing, in fact I think it would be better to just remove all the changes to the readme and only keep the change on L383. I feel like this is just implementation detail that the end user doesn't need to know about. |
||
|
||
Note that both changesets `A` and `B` are not destroyed by the merge, so you might want to call `destroy()` on them to avoid memory leaks. | ||
|
||
**[⬆️ back to top](#api)** | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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,18 +212,28 @@ 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; | ||
} | ||
|
||
let changesA = get(this, CHANGES); | ||
let changesB = get(changeset, CHANGES); | ||
let mergedChanges = pureAssign(changesA, changesB); | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No whitelines between the variable declarations please! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you think you could add a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah I will at some point |
||
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; | ||
}, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
}, {}); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { default } from 'ember-changeset/utils/object-without'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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'); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nitpick: prefer having spaces within
{}
, e.g.[{ key: ... }]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure this addition helps with understanding how merge works, feels like it could be confusing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cleaned that up