Skip to content
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

Add option to clone objects before merge. Fixes #28 issue. #44

Merged
merged 4 commits into from
Oct 12, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ If an element at the same key is present for both `x` and `y`, the value from

The merge is immutable, so neither `x` nor `y` will be modified.

#### option: arrayMerge

The merge will also merge arrays and array values by default. However, there are nigh-infinite valid ways to merge arrays, and you may want to supply your own. You can do this by passing an `arrayMerge` function as an option.

```js
Expand All @@ -51,6 +53,9 @@ function concatMerge(destinationArray, sourceArray, mergeOptions) {
merge([1, 2, 3], [1, 2, 3], { arrayMerge: concatMerge }) // => [1, 2, 3, 1, 2, 3]
```

#### option: clone

If `clone` option is `true` then both `x` and `y` would be clonned before merge. Default value for `clone` is `false`.
install
=======

Expand Down
24 changes: 22 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,30 +16,50 @@ function isMergeableObject(val) {
&& Object.prototype.toString.call(val) !== '[object Date]'
}

function emptyTarget(val) {
return Array.isArray(val) ? [] : {}
}

function defaultArrayMerge(target, source, optionsArgument) {
var destination = target.slice()
var clone = optionsArgument && optionsArgument.clone === true
source.forEach(function(e, i) {
if (typeof destination[i] === 'undefined') {
if (clone && isMergeableObject(e)) {
e = deepmerge(emptyTarget(e), e)
}
destination[i] = e
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think e needs to be cloned in this case, too - I think a failing test for this case might be

var a = { key: 'yup' }
var target = []
var source = [a]
var output = deepmerge(target, source)
t.notEqual(output[0], a)
t.equal(output[0].key, 'yup')

But I haven't tested that to make sure it fails right now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@TehShrike Good analytical thinking.

} else if (isMergeableObject(e)) {
destination[i] = deepmerge(target[i], e, optionsArgument)
} else if (target.indexOf(e) === -1) {
if (clone && isMergeableObject(e)) {
e = deepmerge(emptyTarget(e), e)
}
destination.push(e)
}
})
return destination
}

function mergeObject(target, source, optionsArgument) {
var clone = optionsArgument && optionsArgument.clone === true
var destination = {}
if (isMergeableObject(target)) {
Object.keys(target).forEach(function (key) {
destination[key] = target[key]
var val = target[key]
if (clone && isMergeableObject(val)) {
val = deepmerge(emptyTarget(val), val)
}
destination[key] = val
})
}
Object.keys(source).forEach(function (key) {
if (!isMergeableObject(source[key]) || !target[key]) {
destination[key] = source[key]
var val = source[key]
if (clone && isMergeableObject(val)) {
val = deepmerge(emptyTarget(val), val)
}
destination[key] = val
} else {
destination[key] = deepmerge(target[key], source[key], optionsArgument)
}
Expand Down
163 changes: 163 additions & 0 deletions test/merge.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,67 @@ test('should add nested object in target', function(t) {
t.end()
})

test('should clone source and target', function(t) {
var src = {
"b": {
"c": "foo"
}
}

var target = {
"a": {
"d": "bar"
}
}

var expected = {
"a": {
"d": "bar"
},
"b": {
"c": "foo"
}
}

var merged = merge(target, src, {clone: true})

t.deepEqual(merged, expected)

t.notEqual(merged.a, target.a)
t.notEqual(merged.b, src.b)

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two tests should probably assert that merged deep equals expected, since the two "modified" values wouldn't conflict with each other.

t.end()
})

test('should not clone source and target', function(t) {
var src = {
"b": {
"c": "foo"
}
}

var target = {
"a": {
"d": "bar"
}
}

var expected = {
"a": {
"d": "bar"
},
"b": {
"c": "foo"
}
}

var merged = merge(target, src)
t.equal(merged.a, target.a)
t.equal(merged.b, src.b)

t.end()
})

test('should replace object with simple key in target', function (t) {
var src = { key1: 'value1' }
var target = {
Expand Down Expand Up @@ -176,6 +237,30 @@ test('should work on array properties', function (t) {
t.end()
})

test('should work on array properties with clone option', function (t) {
var src = {
key1: ['one', 'three'],
key2: ['four']
}
var target = {
key1: ['one', 'two']
}

var expected = {
key1: ['one', 'two', 'three'],
key2: ['four']
}

t.deepEqual(target, {
key1: ['one', 'two']
})
var merged = merge(target, src, {clone: true})
t.notEqual(merged.key1, src.key1)
t.notEqual(merged.key1, target.key1)
t.notEqual(merged.key2, src.key2)
t.end()
})

test('should work on array of objects', function (t) {
var src = [
{ key1: ['one', 'three'], key2: ['one'] },
Expand All @@ -202,6 +287,37 @@ test('should work on array of objects', function (t) {
t.end()
})

test('should work on array of objects with clone option', function (t) {
var src = [
{ key1: ['one', 'three'], key2: ['one'] },
{ key3: ['five'] }
]
var target = [
{ key1: ['one', 'two'] },
{ key3: ['four'] }
]

var expected = [
{ key1: ['one', 'two', 'three'], key2: ['one'] },
{ key3: ['four', 'five'] }
]

t.deepEqual(target, [
{ key1: ['one', 'two'] },
{ key3: ['four'] }
])
var merged = merge(target, src, {clone: true})
t.deepEqual(merged, expected)
t.ok(Array.isArray(merge(target, src)), 'result should be an array')
t.ok(Array.isArray(merge(target, src)[0].key1), 'subkey should be an array too')
t.notEqual(merged[0].key1, src[0].key1)
t.notEqual(merged[0].key1, target[0].key1)
t.notEqual(merged[0].key2, src[0].key2)
t.notEqual(merged[1].key3, src[1].key3)
t.notEqual(merged[1].key3, target[1].key3)
t.end()
})

test('should work on arrays of nested objects', function(t) {
var target = [
{ key1: { subkey: 'one' }}
Expand Down Expand Up @@ -231,6 +347,20 @@ test('should treat regular expressions like primitive values', function (t) {
t.end()
})

test('should treat regular expressions like primitive values and should not'
+ ' clone even with clone option',
function (t) {
var target = { key1: /abc/ }
var src = { key1: /efg/ }
var expected = { key1: /efg/ }

var output = merge(target, src, {clone: true})

t.equal(output.key1, src.key1)
t.end()
}
)

test('should treat dates like primitives', function(t) {
var monday = new Date('2016-09-27T01:08:12.761Z')
var tuesday = new Date('2016-09-28T01:18:12.761Z')
Expand All @@ -252,6 +382,27 @@ test('should treat dates like primitives', function(t) {
t.end()
})

test('should treat dates like primitives and should not clone even with clone'
+ ' option', function(t) {
var monday = new Date('2016-09-27T01:08:12.761Z')
var tuesday = new Date('2016-09-28T01:18:12.761Z')

var target = {
key: monday
}
var source = {
key: tuesday
}

var expected = {
key: tuesday
}
var actual = merge(target, source, {clone: true})

t.equal(actual.key, tuesday)
t.end()
})

test('should work on array with null in it', function(t) {
var target = []

Expand All @@ -263,6 +414,18 @@ test('should work on array with null in it', function(t) {
t.end()
})

test('should clone array\'s element if it is object', function(t) {
var a = { key: 'yup' }
var target = []
var source = [a]
var expected = [{key: 'yup'}]

var output = merge(target, source, {clone: true})

t.notEqual(output[0], a)
t.equal(output[0].key, 'yup')
t.end()
})
test('should overwrite values when property is initialised but undefined', function(t) {
var target1 = { value: [] }
var target2 = { value: null }
Expand Down