deep merge objects with AngularJS
Asked Answered
P

5

38

Normally to shallow copy objects I would use angular.extend()

Here's an example of that:

var object1 = {
  "key": "abc123def456",
  "message": {
    "subject": "Has a Question",
    "from": "[email protected]",
    "to": "[email protected]"
   }
};

var object2 = {
  "key": "00700916391"
};

console.log(angular.extend({}, object1, object2));

Would give us:

{
 "key": "00700916391",
 "message": {
   "subject": "Has a Question",
   "from": "[email protected]",
   "to": "[email protected]"
  }
}

But what if I wanted to merge objects so that parent keys are not over written by child objects:

var object1 = {
  "key": "abc123def456",
  "message": {
    "subject": "Has a Question",
    "from": "[email protected]",
    "to": "[email protected]"
   }
};

var object2 = {
  "key": "00700916391",              //Overwrite me
  "message": {                       //Dont overwrite me!
    "subject": "Hey what's up?",     //Overwrite me
    "something": "something new"     //Add me
   }
};

console.log(merge(object1, object2));

Would give us:

{
 "key": "00700916391",
 "message": {
   "subject": "Hey what's up?",
   "from": "[email protected]",
   "to": "[email protected]",
   "something": "something new"
  }
}
  • Is there an Angular function that already does a deep merge that I am not aware of?

  • If not is there a native way to do this in javascript recursively for n levels deep?

Prairial answered 21/6, 2013 at 19:29 Comment(0)
C
39

Angular 1.4 or later

Use angular.merge:

Unlike extend(), merge() recursively descends into object properties of source objects, performing a deep copy.

angular.merge(object1, object2); // merge object 2 into object 1

Older versions of Angular:

There is no reason a simple recursive algorithm shouldn't work :)

Assuming they're both the result of JSON.stringify or similar:

function merge(obj1,obj2){ // Our merge function
    var result = {}; // return result
    for(var i in obj1){      // for every property in obj1 
        if((i in obj2) && (typeof obj1[i] === "object") && (i !== null)){
            result[i] = merge(obj1[i],obj2[i]); // if it's an object, merge   
        }else{
           result[i] = obj1[i]; // add it to result
        }
    }
    for(i in obj2){ // add the remaining properties from object 2
        if(i in result){ //conflict
            continue;
        }
        result[i] = obj2[i];
    }
    return result;
}

Here is a working fiddle

(Note, arrays are not handled here)

Cota answered 21/6, 2013 at 19:37 Comment(6)
Now, what would be really cool, is if you could pass n objects as paramters. Similarly to how angular.extend() works... Up to the challenge ? :)Prairial
@DanKanze Instead of looping through obj2 in the second for... in, wrap that in another loop through arguments (don't start the index at 0, you want to skip the first object :) (The reason it's not a part of the answer, is the same reason why there is no depth limit or array treatment - it complicates a simple answer with specifics) If you have trouble implementing this - you're welcome to discuss this with me in the JS room.Cota
You omit a var declaration for i - var result = {}, i;Pork
@Pork thanks for noticing, I make a such mistakes when writing code in a jshint-less environment like the SO markdown editor all the time. I've fixed it - nice catch.Cota
I think this merge function is no real replacement for the angular.merge function in 1.4. The aim of merging is to deeply extend an object. Your solution creates a new object (result) and obj1 isn't changed in any way.Swop
@TonyO'Hagan what's typeof null?Cota
R
6

In the new version of Angularjs they added merge function which will perform the deep copy.

For the older versions, I have created my custom function by copying the code of merge function from new version of Angularjs. Below is the code for the same,

function merge(dst){
  var slice = [].slice;
  var isArray = Array.isArray;
  function baseExtend(dst, objs, deep) {
    for (var i = 0, ii = objs.length; i < ii; ++i) {
      var obj = objs[i];
      if (!angular.isObject(obj) && !angular.isFunction(obj)) continue;
      var keys = Object.keys(obj);
      for (var j = 0, jj = keys.length; j < jj; j++) {
        var key = keys[j];
        var src = obj[key];
        if (deep && angular.isObject(src)) {
          if (!angular.isObject(dst[key])) dst[key] = isArray(src) ? [] : {};
          baseExtend(dst[key], [src], true);
        } else {
          dst[key] = src;
        }
      }
    }

    return dst;
  }
  return baseExtend(dst, slice.call(arguments, 1), true);
}

Hope this will help someone who is wondering why angular.merge is not working in older versions.

Reeder answered 12/3, 2015 at 6:46 Comment(3)
I've upvoted you as this is the best answer as it uses the a modified version of the angular code. Perhaps you could add a line like "if(angular.hasOwnProperty('merge'))return angular.merge.apply(angular, arguments);". This would mean that the native Angular version is used if available and the polyfill version when using <1.4.Garber
Almost ... what I finally coded was: if (!angular.hasOwnProperty('merge')) { angular.merge = function(dst) { ... } }Ronnironnica
Watch out for this 1.4 bug in baseExtend() ... geekswithblogs.net/shaunxu/archive/2015/05/29/… that treats Dates() as objects and incorrectly attempts to merge themRonnironnica
A
4

angular.merge polyfill for angular < 1.4.0

if (!angular.merge) {
  angular.merge = (function mergePollyfill() {
    function setHashKey(obj, h) {
      if (h) {
        obj.$$hashKey = h;
      } else {
        delete obj.$$hashKey;
      }
    }

    function baseExtend(dst, objs, deep) {
      var h = dst.$$hashKey;

      for (var i = 0, ii = objs.length; i < ii; ++i) {
        var obj = objs[i];
        if (!angular.isObject(obj) && !angular.isFunction(obj)) continue;
        var keys = Object.keys(obj);
        for (var j = 0, jj = keys.length; j < jj; j++) {
          var key = keys[j];
          var src = obj[key];

          if (deep && angular.isObject(src)) {
            if (angular.isDate(src)) {
              dst[key] = new Date(src.valueOf());
            } else {
              if (!angular.isObject(dst[key])) dst[key] = angular.isArray(src) ? [] : {};
              baseExtend(dst[key], [src], true);
            }
          } else {
            dst[key] = src;
          }
        }
      }

      setHashKey(dst, h);
      return dst;
    }

    return function merge(dst) {
      return baseExtend(dst, [].slice.call(arguments, 1), true);
    }
  })();
}
Apiece answered 24/6, 2015 at 14:27 Comment(0)
R
3

here is a solution that handels multiple objects merge (more than two objects):

Here is an extendDeep function based off of the angular.extend function. If you add this to your $scope, you would then be able to call

$scope.meta = $scope.extendDeep(ajaxResponse1.myMeta, ajaxResponse2.defaultMeta);

and get the answer you are looking for.

$scope.extendDeep = function extendDeep(dst) {
  angular.forEach(arguments, function(obj) {
    if (obj !== dst) {
      angular.forEach(obj, function(value, key) {
        if (dst[key] && dst[key].constructor && dst[key].constructor === Object) {
          extendDeep(dst[key], value);
        } else {
          dst[key] = value;
        }     
      });   
    }
  });
  return dst;
};
Refugiorefulgence answered 11/8, 2014 at 14:20 Comment(1)
PS: This function has the side-effect of copying values from later arguments into the earlier arguments.Combs
G
1

If you're using < 1.4

You can use lodash's built in _.merge() that does the same thing as angular > 1.4's version

Saved me from writing new functions since lodash is pretty popular with angular folks already

Gripe answered 25/6, 2015 at 14:31 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.