r3plica
r3plica

Reputation: 13387

Angular cloned array still modifies parent

I am trying to filter an array by string, but when I do it modifies the original array. My code is like this:

private copy: MenuItem[];

@Input() links: MenuItem[];

ngOnInit(): void {
    this.copy = this.links;
}

public filter(target: MenuItem): void {
    console.log(target);
    let link = this.links.find((link: MenuItem) => link.path === target.path);
    if (!link) return;

    console.log(link);

    this.copy.forEach((item: MenuItem) => {
        if (item.path !== link.path) return;

        let children = [...item.children];
        link.childrenCount = children.length;

        console.log(children.length);
        console.log(item.children.length);
        
        link.children = children.filter((child: MenuItem) => child.label.indexOf(item.filterChildren) > -1);
    });
}

As you can see, I try to clone the children of the copied array, but when I try to filter, both item.children and children are modified. I don't want the items in the copy array to ever change, just the one in links.

I have tried a few ways. The way I thought would work was this:

public filter(target: MenuItem): void {
    let link = this.links.find((link: MenuItem) => link.path === target.path);
    if (!link) return;

    this.copy.forEach((item: MenuItem) => {
        if (item.path !== link.path) return;

        link.childrenCount = item.children.length;
        link.children = [...item.children.map((o) => ({ ...o }))].filter(
            (child: MenuItem) => child.label.indexOf(item.filterChildren) > -1,
        );

        console.log(item.children.length);
        console.log(link.children.length);
    });
}

But it doesn't. Both item.children.length and link.children.length return the length of the filtered array, not the original size.

Can anyone help?

PS: This is the MenuItem model:

export class MenuItem {
    label: string;
    path: string;
    filterChildren?: string;
    open?: boolean;
    children?: MenuItem[];
    childrenCount?: number;
}

Upvotes: 0

Views: 94

Answers (4)

Quan
Quan

Reputation: 21

Instead of this.copy = this.links; You can use Object.assign(this.coppy,this.links); or this.coppy = [...this.links];

Upvotes: 0

SeleM
SeleM

Reputation: 9658

That's because objects (arrays as well, since they're also objects in a specific way) in JS are cloned by reference not values. The best way to do so:

const clone = JSON.parse(JSON.stringify(original));

that wont mutate the original object if changes (even deep ones) occur in the clone.

In your case :

 this.copy = JSON.parse(JSON.stringify(this.links));

Upvotes: 1

Cédric S
Cédric S

Reputation: 519

When you say this.copy = this.links, you are creating a copy of the reference to an existing array. This mean any changes on either of the variables will reflect on the other (it is the same reference).

If you want to change only links and not copy, then copy must be a deep copy of links, not just a link to the same reference.

this.copy = [];
    this.links.forEach(item => {
      let copiedItem = {...item};
      copiedItem.children = {...item.children};
      this.copy.push(copiedItem);
    })

You can also include filter here already:

this.copy = [];
    this.links.filter(item => item.path === target.path).forEach(item => {
      let copiedItem = {...item};
      copiedItem.children = {...item.children};
      this.copy.push(copiedItem);
    })

Upvotes: 0

Orelsanpls
Orelsanpls

Reputation: 23545

You need to perform a total copy of your data in order to separate them.

There are multiple stackoverflow post about how to clone an array of value.


First, look at the following snippet where I reproduced the error.


Now with the fix : snippet


Code :

interface MenuItem {
    label: string;
    path: string;
    filterChildren?: string;
    open?: boolean;
    children?: MenuItem[];
    childrenCount?: number;
}

function cloneSomething<T>(something: T): T {
  //
  // Handle null, undefined and simple values
  if (something === null || typeof something === 'undefined') return something;

  //
  // Handle Array object and every values in the array
  if (something instanceof Array) {
      return something.map(x => cloneSomething(x)) as unknown as T;
  }
  
  //
  // Handle the copy of all the types of Date
  if (something instanceof Date) {
      return new Date(something.valueOf()) as unknown as T;
  }

  //
  // Handle all types of object
  if (typeof (something as any).toJSON === 'function') {
      return (something as any).toJSON();
  }

  if (typeof something === 'object') {
    return Object.keys(something as any)
      .reduce((tmp, x) => ({
        ...tmp,
        [x]: cloneSomething((something as any)[x]),
      }), {}) as unknown as T;
  }

  // No effect to simple types
  return something;
}

class Foo {
    public copy: MenuItem[] = [];

    public links: MenuItem[] = [
        {
            label: 'A',
            path: 'A',
        }, 
        {
            label: 'B',
            path: 'B',
            filterChildren: 'alex',

            children: [
                {
                    label: 'catherine',
                    path: 'B',
                },
                {
                    label: 'alexis',
                    path: 'D',
                },
            ],
            
            childrenCount: 2,
        },
    ];

    constructor() {
      this.copy = cloneSomething(this.links);
    }

    public filter(target: MenuItem): void {
      let link = this.links.find((link: MenuItem) => link.path === target.path);
      
      if (!link) {
        return;
      }
      
      this.copy.forEach((item: MenuItem) => {
        if (item.path !== link?.path) {
            return;
        }

        const children = [
            ...(item.children ?? []),
        ];
        
        link.childrenCount = children.length;

        link.children = children.filter((child: MenuItem) => child.label.indexOf(item?.filterChildren ?? '') > -1);
      });
    }
}

const foo = new Foo();

foo.filter((foo as any).links[1]);

console.log('COPY', foo.copy);
console.log('LINKS', foo.links);

Upvotes: 0

Related Questions