0

I've looked at a bunch of posts about this but none seem to do what I'm trying to do or at least none that I found durning my search.

I have two objects and I need to combine them both without overriding any properties. If two keys are the same then I need to concat their values. Example:

const base = {
  icon: "icon--value1"
  item: "item--value"
} 

const extend = {
  icon: "icon--value2"
  item: "item--value"
  list: "list--value"
} 

Combined Objects

const combined = { 
  icon: "icon--value1 icon--value2"
  item: "item--value"
  list: "list--value"
}

I tried using es6 assign and es6 destructuring but they just override the values.

 combined = {...base, ... extend}

Was not the result I was after. Anyone know how I could achieve the above ? Thanks in advance for any help.

me-me
  • 5,139
  • 13
  • 50
  • 91

3 Answers3

2

If you are happy to use lodash (which also means not reinventing the wheel), you can use the mergeWith() function documented here. For example:

const base = {
  icon: "icon--value1",
  item: "item--value"
} 

const extend = {
  icon: "icon--value2",
  item: "item--value",
  list: "list--value"
} 

const res = _.mergeWith(base, extend, (objVal, srcVal) => 
  objVal && objVal !== srcVal ? `${objVal} ${srcVal}` : srcVal
)

console.log(res)
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.11/lodash.min.js"></script>

Alternatively you can create your own mergeWith by doing the following:

const base = {
  icon: "icon--value1",
  item: "item--value"
} 

const extend = {
  icon: "icon--value2",
  item: "item--value",
  list: "list--value"
} 

const customMergeWith = (a, b, proc) => {
  const result = { ...a }
  Object.keys(b).forEach(k => {
    result[k] = proc(result[k], b[k])
  })
  return result
}

const res = customMergeWith(base, extend, (objVal, srcVal) => 
  objVal && objVal !== srcVal ? `${objVal} ${srcVal}` : srcVal
)

console.log(res)

Just note that I personally would prefer to use the lodash version, as it will be more robust, and less code you need to worry about. Don't try and reinvent the wheel if you don't have to.


Finally, people seem to feel that libs like lodash are to be avoided, likely because of their size. However you can import only what you need these days. See this answer for details: How to Import a Single Lodash Function?

Matt Way
  • 32,319
  • 10
  • 79
  • 85
  • I tried your lodash function outside my project and I can't seem to get rid of the linter waring. [eslint] Arrow function used ambiguously with a conditional expression. [no-confusing-arrow]. – me-me Jan 27 '19 at 04:15
  • I appreciate your answer to my post. Thanks – me-me Jan 27 '19 at 04:17
  • 1
    @me-me pretty sure that warning means eslint likes `(objVal, srcVal) => {return () }` more than `(objVal, srcVal) => () ` – cantuket Jan 27 '19 at 05:11
1

You can do it using reduce.

Here the idea is :-

  1. We make a Set out of keys from both of the object.
  2. And than we loop through the key array. And for each key we search for the value in both object and concat them (Keep in mind we need to use || here otherwise if one object doesn't have property will return undefined which will be added in output which is not desired). And than we add key and value to output object.

const base = {icon: "icon--value1",item: "item--value"} 
const extend = {icon: "icon--value2",item: "item--value", list: "list--value"} 

let keys = [...new Set([...Object.keys(base), ...Object.keys(extend)]) ]

let output = keys.reduce( (op, cur) => {
  if((base[cur] && extend[cur]) && (base[cur] !== extend[cur])){
    op[cur] = (base[cur]||'') + " " + (extend[cur]||'')
  }
  else {
    op[cur] = base[cur] || extend[cur]
  }
  return op;
},{})

console.log(output)

An alternative to achieve what i was doing with Set

const base = {icon: "icon--value1",item: "item--value"} 
    const extend = {icon: "icon--value2",item: "item--value", list: "list--value"} 

let keys = [...Object.keys(base), ...Object.keys(extend)]

let uniqueKeys = Object.keys(keys.reduce((output,cur)=>{
  if( !output[cur] ){
    output[cur]=''
  }
  return output;
},{}))

let output = uniqueKeys.reduce( (op, cur) => {
  if((base[cur] && extend[cur]) && (base[cur] !== extend[cur])){
    op[cur] = (base[cur]||'') + " " + (extend[cur]||'')
  }
  else {
    op[cur] = base[cur] || extend[cur]
  }
  return op;
},{})

console.log(output)
Code Maniac
  • 37,143
  • 5
  • 39
  • 60
  • Thanks Code Maniac unfortunately I can't use new Set as its not supported in IE 11 and I need to support that. Could I do it without new Set – me-me Jan 27 '19 at 03:27
  • 1
    Amazing! Thank you. – me-me Jan 27 '19 at 03:51
  • 1
    @CodeManiac nice one! – Nick Parsons Jan 27 '19 at 03:52
  • @CodeManiac I'm writing some tests for this and not seeing how (base[cur]||'') or (extend[cur]||'') every get to the || ' ' statements ? Because extend[cur] and base[cur] will always have a value based on the if checks above it. Can you verify this ? I'd like to remove them if they are not doing anything. – me-me Feb 08 '19 at 01:51
0

For me this would be the most readable way to handle it...

const base = {icon: "icon--value1",item: "item--value"} 
const extend = {icon: "icon--value2",item: "item--value", list: "list--value"} 

let keys = [...Object.keys(base), ...Object.keys(extend)]

let output = keys.reduce((acc, key) => {
   if (acc[key]) return acc;
   let newVal = (
       (base[key] || "") 
      + " " 
      +(extend[key] || "")
   );
   return {...acc, [key]: newVal.trim().trimStart() };            
},{})
cantuket
  • 1,582
  • 10
  • 19
  • As with @CodeManiacs, you answer will not produce the correct result. – Matt Way Jan 27 '19 at 03:59
  • @MattWay Ah you're right. Forgot about those spaces. I'd happily remove mine so there isn't a bunch of duplication, but I think this is the cleanest now. Thoughts? – cantuket Jan 27 '19 at 04:14