1

I have a small sample angular app, where I'm experimenting with the kendo UI drawer component. My question isn't related to the drawer itself, but it has a general nature.

I have an items array of objects, based on which the drawer is populated:

export const items = [
  {
    text: 'France',
    icon: 'user',
    selected: false,
    expanded: false,
    parent: true,
    level: 0,
    id: '0',
    children: [
      {
        text: 'Paris',
        level: 1,
        id: '0.1',
      },
      {
        text: 'Lyon',
        selected: false,
        expanded: false,
        parent: true,
        level: 1,
        id: '0.2',
        children: [
          {
            text: 'Lyon 1',
            level: 2,
            id: '0.2.1',
          },
          {
            text: 'Lyon 2',
            level: 2,
            id: '0.2.2',
          },
        ],
      },
    ],
  },
  {
    text: 'Italy',
    icon: 'gear',
    expanded: false,
    selected: false,
    parent: true,
    level: 0,
    id: '1',
    children: [
      {
        text: 'Rome',
        level: 1,
        id: '1.1',
      },
      {
        text: 'Milan',
        level: 1,
        id: '1.2',
      },
    ],
  },
];

Now, on component loading or if the user selects the first time an item, I do a clone of this array with this function:

public resetItems(): Array<any> {
    const arr = [];
    items.forEach((item) => {
      arr.push(Object.assign({}, item));
    });
    return arr;
  }

And then I assign the returned array to a temporary array like this:

if (this.newItems.length === 0) {
      this.newItems = this.resetItems();
    }

The user can select, expand, collapse an item. By clicking on an item, I set the appr—property accordingly (selected, expanded).

In the above example data, I have a parent France and two children (Paris, Lyon), where Lyon has another two children too (for test purposes). Now, if I click on France and expand the drawer, in the items array, on the France object, the properties selected and expanded remains false. And this is as expected, because I do all this modification on the newItems array and not on the items itself.

But if I expand Lyon, then the properties selected and expanded on Lyon are set to true in the items array, which shouldn't happen. Why is this? Somehow it seems to me that the newItems and items array are connected. But they shouldn't.

I wrote two little functions too in order to set selected and expanded on newItems for all items to false and if I set this properties to false like this:

this.clearSelection(this.newItems);
this.clearExpandedState(this.newItems);

Then only in this case I will have an items array, where Lyon isn't selected nor expanded.

Below is all the component code to analyze this.

export class AppComponent {
  public drawerExpanded = false;
  public item: any = {};
  public drawerItems = this.resetItems();
  private newItems: any[] = [];

  @ViewChild('drawer') drawer: DrawerComponent;

  public onSelect(e: DrawerSelectEvent): void {
    this.item = e.item;
    const text = e.item.text;

    // if item is no parent, then no action
    if (!this.item.parent) {
      return;
    }

    // if the drawer is collapsed and we click on one of the icons, we expand it
    if (!this.drawerExpanded) {
      this.drawer.toggle();
    }

    // we take a fresh start and reset the items to their original state,
    // but only if we click the first time on any item
    // in other cases 'newItems' must be not overridden
    if (this.newItems.length === 0) {
      this.newItems = this.resetItems();
    }

    // we set the 'selected' property on the clicked item to true (only parents should have the 'selected' property)
    // but before do it, we clear all selection
    // it has the effect, that the parent items have the '.k-state-selected' class too (background color set to primary)
    const index = this.newItems.findIndex((i) => i.text === text);
    this.clearSelection(this.newItems);
    this.newItems[index].selected = true;

    // we expand the clicked item - if not expanded - and add the children into the 'newItems' array
    // else we remove the unnecessary items based on the 'id' field and set 'expanded' to false
    if (!this.item.expanded) {
      this.newItems[index].expanded = true;
      this.addChildren(this.newItems, index, this.newItems[index].children);
    } else {
      this.newItems = this.removeChildren(this.item.id, this.newItems);
      this.newItems[index].expanded = false;
    }

    // refresh the items in the component
    this.drawerItems = this.newItems;

    console.log(items);
  }

  public addChildren(arr, index: number, children: Array<any>) {
    arr.splice(index + 1, 0, ...children);
  }

  public removeChildren(id: string, arr): Array<any> {
    return arr.filter((item) => !item.id.startsWith(`${id}.`));
  }

  public clearSelection(arr) {
    arr.forEach((item) => {
      if (item.selected) {
        item.selected = false;
      }
    });
  }

  public clearExpandedState(arr) {
    arr.forEach((item) => {
      if (item.expanded) {
        item.expanded = false;
      }
    });
  }

  public resetItems(): Array<any> {
    const arr = [];
    items.forEach((item) => {
      arr.push(Object.assign({}, item));
    });
    return arr;
  }

  toggle() {
    this.clearSelection(this.newItems);
    this.clearExpandedState(this.newItems);
    this.newItems = [];
    this.drawerItems = this.resetItems();
    this.drawer.toggle();
  }
}
Yasin Br
  • 1,675
  • 1
  • 6
  • 16
derstauner
  • 1,478
  • 2
  • 23
  • 44

1 Answers1

0

The problem is that when you are creating a new array, you are only cloning the very top level of the objects to make new objects-- all the nested items are retaining the reference:

const items = [{ item: { name: 'apple' } }, { item: { name: 'bananas' } }];
const arr = [];
items.forEach((item) => {
  arr.push(Object.assign({}, item));
});

console.log(arr[0] === items[0]); // true 
console.log(arr[0].item === items[0].item) // false

items[0].item.name = 'hello';

console.log(arr[0].item); // { name: 'hello' }

To get a completely new object devoid of any shared references, you could use a deep-cloning utility like lodash's cloneDeep, use JSON.parse(JSON.stringify(yourObj)), or spin your own recursive, deep-cloning utility.

Alexander Nied
  • 12,804
  • 4
  • 25
  • 45
  • 1
    Believe or not, after asking the question, I went to eat dinner in the kitchen and while warming it, it just ran through my mind, that I do only a simple cloning and not deep cloning. I will accept the answer of course. – derstauner Sep 18 '21 at 18:56