JSPM

  • Created
  • Published
  • Downloads 6316
  • Score
    100M100P100Q127335F
  • License MIT

Recursive, deep merge of anything (objects, arrays, strings or nested thereof), which weighs contents by type hierarchy to ensure the maximum content is retained

Package Exports

  • object-merge-advanced

This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (object-merge-advanced) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.

Readme

object-merge-advanced

ESLint on airbnb-base with caveats

Recursive, deep merge of anything (objects, arrays, strings or nested thereof), which weighs contents by type hierarchy to ensure the maximum content is retained

Minimum Node version required Link to npm page Build Status Coverage bitHound Overall Score bitHound Dependencies View dependencies as 2D chart bitHound Dev Dependencies Known Vulnerabilities Downloads/Month Test in browser All Contributors MIT License

Table of Contents

Install

npm i object-merge-advanced
// consume via CommonJS require():
const mergeAdvanced = require('object-merge-advanced')
// or import as an ES module:
import mergeAdvanced from 'object-merge-advanced'

Here's what you'll get:

Type Key in package.json Path Size
Main export - CommonJS version, transpiled to ES5, contains require and module.exports main dist/object-merge-advanced.cjs.js 12 KB
ES module build that Webpack/Rollup understands. Untranspiled ES6 code with import/export. module dist/object-merge-advanced.esm.js 11 KB
UMD build for browsers, transpiled, minified, containing iife's and has all dependencies baked-in browser dist/object-merge-advanced.umd.js 35 KB

⬆  back to top

Purpose

It's like _.merge, but it correctly merges different-type things and behaves well when it encounters nested things like parsed HTML (lots of nested arrays, objects and strings).


Imagine, if we merged the identical keys of two objects judging their values by the hierarchy instead:

  • non-empty array trumps all below
  • non-empty plain object trumps all below
  • non-empty string ...
  • empty plain object ...
  • empty array
  • empty string
  • number
  • boolean
  • null
  • undefined doesn't trump anything

The idea is, we strive to retain as much info as possible after merging. For example, you'd be better off with a non-empty string than with an empty array or boolean.

The fun does not stop here. Sometimes life demands unidirectional merges from either source or destination ("overwrite no matter what, from either side"). This can be done per object-key-basis, see opts.ignoreKeys where first input object's key overrides the second's and opts.hardMergeKeys for the opposite.

That's what this library does

When object-merge-advanced merges two objects, it will check the types of their key values:

  • If a key exists only in one of the objects, it goes straight into the result object.
  • If a key exists on both, we got a clash. Key's value will be chosen judging by its value's type:
    • Arrays trump objects which trump strings which trump numbers which trump Booleans
    • Non-empty array as value trumps any object or string as value
    • Anything empty won't trump anything not empty
    • If both keys have plain object values, they'll get recursively fed back into the library again
    • Booleans will be merged using logical "OR"
    • Arrays will be merged, and if there are objects within, those objects will be merged smartly, depending if their keysets are similar. If not, objects will be merged as separate array elements.

There are ten possible combinations: 10 types of first input (object #1) and ten types of second input (object #2): non-empty (full) object, empty object, non-empty array, empty array, non-empty string, empty string, number, boolean, undefined and null.

matching algorithm

A large number in the centre of a square shows which value prevails.

In the diagram above, the squares show which value gets assigned to the merge result — the first object's (marked 1, pink fields) or second one's (marked 2, sky blue fields).

In some cases, we perform a custom actions:

  1. passing value objects back into the main function recursively (when both values are plain objects),
  2. when merging arrays, we pay extra attention to the options object (if present) and the contents of both arrays (taking special measures for objects within),
  3. Logical "OR" composition (when both values are Boolean).

I challenge you to check test.js unit tests to see this library in action.

⬆  back to top

In practice

In practice I needed this library to normalise JSON files - generate a "schema" object (a superset of all used keys) and fill any missing keys within all JSON files. Also, JSON files get their keys sorted. That library is used to keep us sane when using JSON to store content for email templates - it's enough to add one unique key in one JSON, and all other templates' content files get it added as well.

I use unidirectional merging when dealing with content mapping JSON files which are by definition unidirectional-flow (always overwrite normal data JSON files).

⬆  back to top

API

mergeAdvanced(input1, input2 [, { options }])

API - Input

Input argument Type Obligatory? Description
input1 Anything yes Normally an object literal, but array or string or whatever else will work too. Can be deeply nested.
input2 Anything yes Second thing to merge with first-one, normally an object, but can be an array or something else.
options Plain object no Optionally, pass all settings in a plain object, as a third argument
Options object's key Value Default Description
{
mergeObjectsOnlyWhenKeysetMatches Boolean true Controls the merging of the objects within arrays. See dedicated chapter below.
ignoreKeys String / Array of strings n/a These keys, if present on input1, will be kept and not merged, that is, changed. You can use wildcards.
hardMergeKeys String / Array of strings n/a These keys, if present on input2, will overwrite their counterparts on input1 (if present) no matter what. You can use wildcards.
mergeArraysContainingStringsToBeEmpty Boolean false If any arrays contain strings, resulting merged array will be empty IF this setting is set to true.
oneToManyArrayObjectMerge Boolean false If one array has one object, but another array has many objects, when oneToManyArrayObjectMerge is true, each object from "many-objects" array will be merged with that one object from "one-object" array. Handy when setting defaults on JSON data structures.
hardMergeEverything Boolean false If there's a clash of anywhere, second argument's value will always overwrite first one's. That's a unidirectional merge.
ignoreEverything Boolean false If there's a clash of anywhere, first argument's value will always overwrite the second one's. That's a unidirectional merge.
concatInsteadOfMerging Boolean true If it's true (default), when object keys clash and their values are arrays, when merging, concatenate those arrays. If it's false, array contents from the first argument object's key will go intact into final result, but second array's contents will be added into result only if they don't exist in the first array.
dedupeStringsInArrayValues Boolean false When we merge two values and they are arrays, full of strings and only strings, this option allows to dedupe the resulting array of strings. Setting should be used in conjunction with concatInsteadOfMerging to really ensure than resulting string array contains only unique strings.
mergeBoolsUsingOrNotAnd Boolean true When two values are Booleans, by default, result will be calculated using logical OR on them. If you switch this to false, merging will use logical AND. Former setting is handy when dealing with JSON content driving email templates, latter is handy when merging settings ("off", false overrides default "on", true).
useNullAsExplicitFalse Boolean false When set to true, null vs. anything (argument order doesn't matter) will yield false. This is used in data structures as an explicit "false" to "turn off" incoming defaults for good without the need of extra values or wrapping with conditionals in templates.
hardArrayConcat Boolean false When set to true, an array vs. array merge will always result from a concat operation from the input1 parameter with input2, no matter which items are contained on those arrays.
hardArrayConcatKeys String / Array of strings n/a These keys, if present on input1 will force hardArrayConcat option on those values. You can use wildcards.
}

Here are all defaults in one place:

{
  mergeObjectsOnlyWhenKeysetMatches: true,
  ignoreKeys: undefined,
  hardMergeKeys: undefined,
  mergeArraysContainingStringsToBeEmpty: false,
  oneToManyArrayObjectMerge: false,
  hardMergeEverything: false,
  ignoreEverything: false,
  concatInsteadOfMerging: true,
  dedupeStringsInArrayValues: false,
  mergeBoolsUsingOrNotAnd: true,
  useNullAsExplicitFalse: false,
  hardArrayConcat: false,
  hardArrayConcatKeys: undefined,
}

⬆  back to top

opts.mergeObjectsOnlyWhenKeysetMatches use cases

mergeObjectsOnlyWhenKeysetMatches is an extra insurance from accidental merging two objects within arrays, where key sets are too different (both have at least one unique key).

For example:

Let's merge these two objects. Notice that each has a unique key (yyyy and xxxx in the object that sits within the first position of each array).

// #1
const obj1 = {
  a: [
    {
      a: 'a',
      b: 'b',
      yyyy: 'yyyy'
    }
  ]
}

const obj2 = {
  a: [
    {
      xxxx: 'xxxx',
      b: 'b',
      c: 'c'
    }
  ]
}

const res1 = mergeAdvanced(object1, object2)

console.log('res1 = ' + JSON.stringify(res1, null, 4))
// => {
//      a: [
//        {
//          a: 'a',
//          b: 'b',
//          yyyy: 'yyyy'
//        },
//        {
//          xxxx: 'xxxx',
//          b: 'b',
//          c: 'c'
//        }
//      ]
//    }

but if you turn off the safeguard, { mergeObjectsOnlyWhenKeysetMatches: false } each object within an array is merged no matter their differences in the keysets:

const res2 = mergeAdvanced(object1, object2, { mergeObjectsOnlyWhenKeysetMatches: false })
console.log('res2 = ' + JSON.stringify(res2, null, 4))
// => {
//      a: [
//        {
//          a: 'a',
//          b: 'b',
//          yyyy: 'yyyy',
//          xxxx: 'xxxx',
//          c: 'c'
//        }
//      ]
//    }

⬆  back to top

API - Output

A merged thing is returned. It's probably the same type of your inputs.

Objects or arrays in the inputs are not mutated. This is very important.

Difference from Lodash _.merge

Lodash _.merge gets stuck when encounters a mismatching type values within plain objects. It's neither suitable for merging AST's, nor for deep recursive merging.

Difference from Object.assign()

Object.assign() is just a hard overwrite of all existing keys, from one object to another. It does not weigh the types of the input values and will happily overwrite the string value with a boolean placeholder.

Object.assign() is not for merging data objects, it's for setting defaults in the options objects.

For example, in my email template builds, I import SCSS variables file as an object. I also import variables for each template, and template variables object overwrites anything existing in SCSS variables object.

That's because I want to be able to overwrite global colours per-template when needed.

Now imagine, we're merging those two objects, and SCSS variables object has a key "mainbgcolor": "#ffffff". Now, a vast majority of templates don't need any customisation for the main background, therefore in their content JSON files the key is set to default, Boolean false: "mainbgcolor": false.

If merging were done using object-assign, placeholder false would overwrite real string value "#ffffff. That means, HTML would receive "false" as a CSS value, which is pink!

If merging were done using object-merge-advanced, all would be fine, because String trumps Boolean — placeholder falses would not overwrite the default SCSS string values.

⬆  back to top

Contributing

  • If you want a new feature in this package or you would like us to change some of its functionality, raise an issue on this repo.

  • If you tried to use this library but it misbehaves, or you need an advice setting it up, and its readme doesn't make sense, just document it and raise an issue on this repo.

  • If you would like to add or change some features, just fork it, hack away, and file a pull request. We'll do our best to merge it quickly. Code style is airbnb-base, only without semicolons. If you use a good code editor, it will pick up the established ESLint setup.

⬆  back to top

Contributors

Thanks goes to these wonderful people (hover the cursor over contribution icons for a tooltip to appear):


Roy Revelt

💻 📖 ⚠️

Jabi

💻 📖 ⚠️

Jason Ware

🐛

This project follows the all-contributors specification. Contributions of any kind are welcome!

⬆  back to top

Licence

MIT License (MIT)

Copyright © 2018 Codsen Ltd, Roy Revelt