5

I have an Item container and item pattern where the child items shoud be added and removed from its container. Adding is fine while remove does nothing. It seems that the angular2 *ngFor directive does not work when any of the child items removed.

    import { NgFor} from 'angular2/common';
    import { bootstrap } from 'angular2/platform/browser';
    import { Component, View, Directive, OnDestroy, Input, enableProdMode } from 'angular2/core';
    import { CORE_DIRECTIVES} from 'angular2/common';


    @Component({selector: 'itemcontainer',})
    @View({ template: `<ul (click)="$event.preventDefault()">
                       <li *ngFor="#it of items">Any Item</li>
                       </ul>
                       <div><ng-content></ng-content></div>`,

            directives: [NgFor],
    })
    export class ItemContainer {
        public items: Array<Item> = [];

        public addItem(item: Item) {
            this.items.push(item);
        }

        public removeItem(item: Item) {
            var index = this.items.indexOf(item);
            if (index === -1) {
                return;
            }

            console.log(`Index about to remove: ${index} this.items length: ${this.items.length}`);
            this.items.slice(index, 1);
            console.log(`this.items length: ${this.items.length}`);
        }
    }

    @Directive({ selector: 'item' })
    export class Item implements OnDestroy {

        @Input() public heading: string;

        constructor(public itemcontainer: ItemContainer) {
            this.itemcontainer.addItem(this);
        }

        ngOnDestroy() {
            this.itemcontainer.removeItem(this);
        }
    }

    @Component({
        selector: 'my-app'
    })
    @View({
        template: `<div (click)="$event.preventDefault()">
            <button type="button" (click)="addItem()">Add item</button>
            <button type="button" (click)="removeItem()">Remove item</button>

        <itemcontainer>
            <item *ngFor="#containerItem of containerItems" [heading]="containerItem.title">Content </item>
        </itemcontainer>
    </div>`,

        directives: [CORE_DIRECTIVES, Item, ItemContainer],

    })

    class Tester {

        private counter: number = 2;

        public containerItems: Array<any> = [
            { title: 'Item1' },
            { title: 'Item2' },
        ];

        addItem() {
            this.containerItems.push({ title: `Item ${this.counter}` });
        }

        removeItem() {

            if (this.containerItems.length > 0) {
                this.containerItems.splice(this.containerItems.length - 1, 1);
            }
        }
    }

    enableProdMode();
    bootstrap(Tester);

Here is the DOM look like after two new items added and removed:

    <itemcontainer>
        <ul>
            <li>Any Item</li>
            <li>Any Item</li>
            <li>Any Item</li>
            <li>Any Item</li>
        </ul>
        <div>
            <item>Content </item>
            <item>Content </item>
        </div>
    </itemcontainer>

The issue is the li part does not removed. Any idea?

(I tested it with angular 2.0.0-beta.3 and 2.)

McLac
  • 2,713
  • 3
  • 15
  • 19

5 Answers5

13

You need to use splice() not slice(). There are no issues with Angular change detection here.

this.items.splice(index, 1);

NgFor will loop over your array items, and it will detect when something is added or removed just fine.

Plunker

Also, you can remove this stuff:

import { NgFor} from 'angular2/common';
import { CORE_DIRECTIVES} from 'angular2/common';

     directives: [NgFor],

Also, you have circular references in your data. Change your code to the following

<li *ngFor="#it of items">Any Item {{it | json}}</li>

and note the error in the console:

EXCEPTION: TypeError: Converting circular structure to JSON in [Any Item {{it | json}} in ItemContainer

Mark Rajcok
  • 362,217
  • 114
  • 495
  • 492
  • With Angular 2.4.0, this is the right answer. I was migrating ng-repeat codes from AngularJS and confused by various invalid information around here about ngFor change detection. You just edit existing item, add a new item by push(), or remove item by splice() then it will show up in view. – bob Mar 07 '17 at 18:12
3

In fact, Angular2 only detects changes when the instance changes. I mean the instance of the array not changes internally on elements.

You could use this (see the second slice call):

public removeItem(item: Item) {
  var index = this.items.indexOf(item);
  if (index === -1) {
    return;
  }

  console.log(`Index about to remove: ${index} this.items length: ${this.items.length}`);
  this.items.slice(index, 1);
  console.log(`this.items length: ${this.items.length}`);

  this.items = this.items.slice();
}

This answers could also help you:

Community
  • 1
  • 1
Thierry Templier
  • 198,364
  • 44
  • 396
  • 360
  • 3
    Since `ngFor` is being used, Angular change detection will notice when items are added or removed just fine. There are no issues with change detection here. The problem is easily solved using `splice()` instead of `slice()`. See my answer for details. – Mark Rajcok Feb 12 '16 at 18:42
  • That's strange! I'm almost sure to have had the same problem in the past that I fixed like this... Following your comment, I made a new try (see this plunkr: https://plnkr.co/edit/YZkC74yOnJgSLEdzjcOE?p=preview) but I can't reproduce it... Thanks for pointing this out, Mark! – Thierry Templier Feb 12 '16 at 22:50
  • Maybe you're thinking of the case where a stateless pipe is used with an array -- e.g., http://stackoverflow.com/a/34497504/215945 One way to fix that is to do what you did here... change the array reference. – Mark Rajcok Feb 13 '16 at 01:52
1

Yes, it is change detection indeed. This what worked for me:

    this.items = [
        ...this.items.slice(0, index),
        ...this.items.slice(index + 1, this.items.length)
    ];
McLac
  • 2,713
  • 3
  • 15
  • 19
0

The problem probably has to do with not setting forward references properly (i.e. you shouldn't use a class before its declared). To address the issue, you can use a shared service:

export class SharedService {
    public items: Array<Item>=[];

    public addItem(item:Item) {
      this.items.push(item);
    }

    public removeItem(item:Item) {
       var index = this.items.indexOf(item);
      if (index >=0) {
        this.items.splice(index,1);
      }
    }
}

Use the shared service in your Item constructor/destructor:

@Directive({ selector: 'item' })
export class Item implements OnDestroy {

    @Input() public heading: string;

    constructor(public sharedService:SharedService) {
        this.sharedService.addItem(this);
    }

    ngOnDestroy() {

        this.sharedService.removeItem(this);

    }
}

And also in your ItemContainer:

@Component({selector: 'itemcontainer',})
@View({ template: `<ul (click)="$event.preventDefault()">
                   <li *ngFor="#it of items">Any Item </li>
                   </ul>

                   <div><ng-content></ng-content></div> `,

        directives: [NgFor],
})
export class ItemContainer {
    public items: Array<Item> = [];
    constructor(public sharedService:SharedService) {
       this.items = this.sharedService.items;
    }


}

Demo Plnkr

Michael Kang
  • 52,003
  • 16
  • 103
  • 135
  • 2
    I don't think there are any forward reference issues here. I think the problem was simply that `slice()` was used instead of `splice()`. See my answer if interested. – Mark Rajcok Feb 12 '16 at 18:45
-1

I am having the same issue with you, even I used splice or detecChange it was still not working @McLac provided the method of reform array like this

this.items = [
    ...this.items.slice(0, index),
    ...this.items.slice(index + 1, this.items.length)
];

saved my life. It works perfect. I have my code like this:

let idx = this.prds.indexOf(prd);
if(idx==-1){
  return
};
/*    it was like this before, consol.log show deleted array correctly just the UI has no change..
for(let i =0; i< this.prds.length; i++){
  if(this.prds[i]==prd){
    this.prds.splice(i,1);
    break;
  }
}
*/
this.prds = [
  ...this.prds.slice(0,idx),
  ...this.prds.slice(idx+1,this.prds.length)
];
Deep Kakkar
  • 5,831
  • 4
  • 39
  • 75
lizzc
  • 1