Search code examples
javascriptarraysangularjsecmascript-6lodash

Two arrays cloned with Lodash, separate but the same. Why?


I know for a fact there is something pretty obvious here that I am completely missing, so your help is greatly appreciated.

I have a feature that provides two dropdowns. They contain the same data (the feature allows a trade between two people, the people is the data), but I want them each to get their own copy of said data.

Another part of this feature is that by picking Person A in the first dropdown, I want to disable Person A in the second dropdown, and vice versa, so I have the ng-options tag paying attention to a disabled property on the object.

The issue I have is that even with using a method such as Lodash's clone to properly create a "new" array upon first time assignment, every time I access Person A in ONE array (and specifically do NOT access the other array) invariably I am seeing that when I touch Person A, that object is updated in BOTH arrays, which has me flustered.

This feels like a down-to-the-metal, barebones Javascript issue (standard PEBCAK, I feel like I'm clearly misunderstanding or straight up missing something fundamental), maybe with a bit of AngularJS rendering-related fun-ness involved, but... What gives?

angular.module('myApp', [])
  .controller('weirdDataController', function($scope) {
    $scope.$watch('manager1_id', () => {
      if (angular.isDefined($scope.manager1_id) && parseInt($scope.manager1_id, 10) > 0) {
        $scope._disableManagerInOtherDropdown(false, $scope.manager1_id);
      }
    });

    $scope.$watch('manager2_id', () => {
      if (angular.isDefined($scope.manager2_id) && parseInt($scope.manager2_id, 10) > 0) {
        $scope._disableManagerInOtherDropdown(true, $scope.manager2_id);
      }
    });

    $scope._gimmeFakeData = () => {
      return [{
          manager_id: 1,
          manager_name: 'Bill',
          disabled: false
        },
        {
          manager_id: 2,
          manager_name: 'Bob',
          disabled: false
        },
        {
          manager_id: 3,
          manager_name: 'Beano',
          disabled: false
        },
        {
          manager_id: 4,
          manager_name: 'Barf',
          disabled: false
        },
        {
          manager_id: 5,
          manager_name: 'Biff',
          disabled: false
        },
      ];
    };

    const data = $scope._gimmeFakeData();
    $scope.firstManagers = _.clone(data);
    $scope.secondManagers = _.clone(data);

    $scope._disableManagerInOtherDropdown = (otherIsFirstArray, managerId) => {
      const disableManagers = manager => {
        manager.disabled = manager.manager_id === managerId;
      };

      if (otherIsFirstArray) {
        $scope.firstManagers.forEach(disableManagers);
      } else {
        $scope.secondManagers.forEach(disableManagers);
      }

      console.log('Is the first item the same??', $scope.firstManagers[0].disabled === $scope.secondManagers[0].disabled);
    }
  });
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.7.5/angular.min.js"></script>
<div ng-app="myApp" ng-controller="weirdDataController">
  <div class="col-xs-12 col-sm-6">
    <select class="form-control" ng-model="manager1_id" ng-options="manager.manager_id as manager.manager_name disable when manager.disabled for manager in firstManagers track by manager.manager_id">
      <option value="" disabled="disabled">Choose one manager</option>
    </select>
    <select class="form-control" ng-model="manager2_id" ng-options="manager.manager_id as manager.manager_name disable when manager.disabled for manager in secondManagers track by manager.manager_id">
      <option value="" disabled="disabled">Choose another manager</option>
    </select>
  </div>
</div>
<br /><br />

I threw everything relevant on $scope just for the sake of getting it working and illustrating the issue, but here's how it goes:

  • On init, I grab the array, then clone a copy for each dropdown
  • When each dropdown changes the model property (the object ID), I have a scope listener then call a method to handle disabling the selected object/person in the OPPOSITE list
  • Within this method, I determine which of the two lists/arrays to iterate through and mark the disabled object
  • At the end of this method, I do a simple console.log call to check the value of a given object. For quick-and-dirty simplicity, I just grab item at index 0 .
  • What I expected: one object have a disabled value of true, and the opposite object to have false. What I see: they both have true (assuming you select the first "real" item in the dropdown)

What's the deal? How big of an idiot am I being?


Solution

  • The answer to my question was: clone() does not perform a "deep" clone by default, therefore I was dealing with the same array despite making the flawed attempt that I was not. Using Lodash's cloneDeep() method solved my issue, but as Patrick suggested I reevaluated how I wrote the method in question and refactored it, which I removed the need to use any cloning at all.