IVY: ngFor weird behaviour

🐞 bug report

Affected Package

@angular/*

Is this a regression?

Dont know if it worked with IVY before, but it is working without IVY.

Description

NgFor removes elements even thou they are not removed from array.
Also removing all items from array, removes animation.

🔬 Minimal Reproduction

git clone https://github.com/kukjevov/ivy-ngfor-error.git
cd ivy-ngfor-error
npm install
npm run server

open Chrome with URL http://localhost:8888

first on notifications page http://localhost:8888

  • under Local notifications click several times on buttons (except Clear Message)
  • several notifications are displayed below
  • if you click on any of notifications only clicked notification should be removed
  • all notifications below clicked notification are removed (event thou in array of notifications only one notification is removed)
  • also if you click on Clear Message all messages are removed without animation

second on select page http://localhost:8888/select

  • options are loaded after timeout and displayed using ngFor
  • assertion fails with error “Index expected to be less than 4 but got 25”
  • options are loaded using ContentChildren
  • this assertion does not fail if options are provided staticaly, or using not delayed ngFor as you can see http://localhost:8888/static

🔥 Exception or Error

🌍 Your Environment

Angular Version:

Angular: 8.1.0-next.2
Browser: any
Node: 11.6.0
NPM: 6.9.0

1 thought on “IVY: ngFor weird behaviour

  1. Hi,

    I’ve looked at your reproduction and created minimal reproducible scenarios:

    Case 1:

    import { Component, Directive, ViewContainerRef, Output, 
      EventEmitter, ComponentFactoryResolver, HostListener, Input
    } from '@angular/core';
    
    let id = 0;
    
    @Component({
      selector: 'my-app',
      template: `
        <button (click)="add()">Add</button>
        <div>
          <ng-container *ngFor="let item of items; let i = index;" >
            <ng-template [messageRenderer]="item" (removing)="removeAtIndex(i)"></ng-template>
          </ng-container>
        </div>
      `
    })
    export class AppComponent {
      items = [];
    
      add() {
        this.items.push(id++);
      }
    
      removeAtIndex(i) {
        this.items.splice(i, 1)
      }
    }
    
    @Component({
      selector: 'ng-message',
      template: `message {{ text }} <span (click)="clicked.emit()">✖</span>`
    })
    export class MessageComponent {
      @Input() text: string;
    
      @Output() clicked = new EventEmitter();
    }
    
    @Directive({
      selector: '[messageRenderer]'
    })
    export class MessageRenderer {
      @Input() messageRenderer;
    
      @Output() removing = new EventEmitter();
    
      constructor(private vcRef: ViewContainerRef, private resolver: ComponentFactoryResolver) { }
    
      ngOnInit() {
        const factory = this.resolver.resolveComponentFactory(MessageComponent);
        const componentRef = this.vcRef.createComponent(factory);
        componentRef.instance.text = this.messageRenderer;
        componentRef.instance.clicked.subscribe(() => this.removing.emit());
      }
    }
    

    Demo https://ivy.ng-run.com/edit/n4DOKUHyA9izdnfQTr2X

    • click several times on Add button
    • remove the first message by clicking on ✖

    It will also remove all notifications below

    Notice that it uses ng-template inside ngFor loop.

    @kara Seems it has something to do with unexpected behavior in walkTNodeTree function

    Case 2:

    @Component({
      selector: 'my-app',
      template: `
        <ng-select (queryListChanged)="someComp.doSomething()">
        	<div #x *ngFor="let item of items"></div>
        </ng-select>
        <some-comp #someComp></some-comp>
        <button (click)="test()">Test</button>
      `
    })
    export class AppComponent { 
      items = [];
    
      test() {
        this.items = [1, 2, 3];
        markDirty(this)
      }
    }
    
    @Component({
      selector: 'ng-select',
      template: `<ng-content></ng-content>`,
    })
    export class SelectComponent implements AfterViewInit {
      @ContentChildren('x') queryList: QueryList<any>;
    
      @ContentChildren('y') queryList2: QueryList<any>;
    
      @Output() queryListChanged = new EventEmitter();
    
      ngAfterViewInit() {
        this.queryList.changes.subscribe(() => {
          this.queryListChanged.emit();
        })
      }
    }
    
    @Component({
      selector: 'some-comp',
      template: `Some component`,
    })
    export class SubChild {
      @ViewChild('x', { static: true }) x;
    
      constructor(private cdRef: ChangeDetectorRef) { }
    
      doSomething() {
        this.cdRef.detectChanges();
      }
    }
    

    Demo https://ivy.ng-run.com/edit/7PEjtK2lAoET6Ge3z67B

    • click on Test button
    • look at how the console error “Uncaught Error: ASSERTION ERROR: index expected to be a valid data index” is being araised

    The problem here is that currentQueryIndex is being changed during query changes.

    Here’s how SelectComponent is being processing during change detection cycle:

    🡇 refreshContentQueries

    • refresh queryList => this.queryList.changes.subscribe =>SubChild::cdRef.detectChanges
    • refresh queryList2 => results in error since we’ve changed currentQueryIndex by calling detectChanges in another component that has a query

    Seems we can fix it by calling markDirty instead of cdRef.detectChanges in SubChild

Comments are closed.