Angular 2 template updating but breaking click handlers

483 Views Asked by At

I have an app that deals with a single type of object. I manage the state of the object using a service. In one component I display the object. From here you can click on fields to edit them in another component. You can also click on buttons to add new items to a field and also delete items.

Most of the time this works fine but sometimes it breaks without any error messages. I click on the delete or edit buttons or fields and nothing happens. The click handlers are broken (I have checked this in the console). This usually happens after I have added or deleted from an array.

This is very confusing because when I add an element I use http to make sure it is saved to the database and then only after the observable with the new droplet is returned do I let angular update the display. And the display always updates. However, sometimes the display updates, but Augury does not realise, which is very odd. So I will add a hint, for example, the hint will be visible in the database, be returned and the view will be updated, but Augury will not see it in the component.

It is also inconsistent. Sometimes is works just fine.

Here is some sample code from the view.

<h4>Hints (optional)</h4>
<button class="btn btn-sm" [routerLink]="['/create/create5']">Add New</button>
<div *ngIf="droplet.hints.length < 1">None</div>
<div class="row" *ngFor="let hint of droplet.hints; let i=index">
  <div class="hint col-md-10" (click)="selectHint(i)">
    <span [innerHTML]="hint.content || empty"></span>
    <span (click)="removeElement(i, 'hint')" class="pull-right glyphicon glyphicon-remove" aria-hidden="true"></span>
  </div>
</div>

<h4>Tags
  <div class="progress-marker" [class.complete]="droplet.tags.length > 0"></div>
  <div class="progress-marker" [class.complete]="droplet.tags.length > 1"></div>
  <div class="progress-marker" [class.complete]="droplet.tags.length > 2"></div>
</h4>
<button class="btn btn-sm" [routerLink]="['/create/create6']">Add New</button>
<div *ngIf="droplet.tags.length < 1">None</div>
<br>
<button *ngFor="let tag of droplet.tags; let i=index" type="button" class="btn btn-default btn-sm" (click)="removeElement(i, 'tag')">
  <span class="glyphicon glyphicon-remove" aria-hidden="true"></span> {{ tag.tag }}
</button> 

There are several fields like this but in general the ones with *ngIf are the ones causing the problems. As I said if I add or remove or edit an element in the array it works and the template is updated, but often nothing in the arrays works after this (though the non arrays work fine).

The relevant component code looks like this:

import { Component, OnInit } from '@angular/core';
import { Droplet } from '../droplet';
import { DropletService } from '../droplet.service';
import { Router } from '@angular/router';
import { Subscription } from 'rxjs/Rx';
import { HttpService } from '../http.service';

export class ShowDropletComponent implements OnInit {
  droplet: Droplet;

  constructor(
    private dropletService: DropletService,
    private router: Router,
    private httpService: HttpService
  ) { }

  ngOnInit() {
    this.droplet = this.dropletService.getCurrentDroplet();
    this.dropletService.pushedDroplet.subscribe(
      droplet => this.droplet = droplet
    )
  }

    //using dummy to ensure element not updated unless returned from server
  removeElement(index, element) {
    console.log("remove clicked");
    let dummy = this.droplet;
    if (element === "explanation") {
      this.router.navigate(['create/create3']);
      dummy.explanations.splice(index, 1);
    } else if (element === "question") {
      this.router.navigate(['create/create4']);
      dummy.questions.splice(index, 1);
    } else if (element === "hint") {
      this.router.navigate(['create/create5']);
      dummy.hints.splice(index, 1);
    } else if (element === "tag") {
      dummy.tags.splice(index, 1);
    }
    this.httpService.saveDroplet(dummy)
      .subscribe(
        (droplet: Droplet) => {
          this.dropletService.updateCurrentDroplet(droplet);
        }
      );
  }

  editThis(field) {
    if (field === "description") {
      this.router.navigate(['create/create2']);
    } else if (field === "name") {
      this.router.navigate(['create/create1']);
    }
  }

  selectExplanation(index) {
    console.log("select exp clicked");
    this.router.navigate(['create/create3', index]);
  }

  selectQuestion(index) {
    console.log("rselect q clicked");
    this.router.navigate(['create/create4', index]);
  }

  selectHint(index) {
    console.log("select hint clicked");
    this.router.navigate(['create/create5', index]);
  }

}

My guess is that it's something to do with the *ngFor updating the array in the view but either the index is not updating properly or the click handlers are breaking, but it's not just with those in that specific part of the template. I'm at a loss.

2

There are 2 best solutions below

1
On

When manipulating items in an *ngFor directive, it is important to add a trackBy function that returns a unique index - so the directive can track them property when removing/adding items.

Assuming your tag.tag is unique you could write your *ngFor like this instead:

<button *ngFor="let tag of droplet.tags; let i=index; trackBy: tag.tag" type="button"...
0
On

The solution to this was odd. I had components side by side with editing forms showing on the left and the droplet being edited or created on the right. You could click on buttons or fields on the right and the appropriate form would appear on the left to edit it. The problem was that if the droplet being shown has fields extending on the right far below the component on the left, the clicks would fail to work and the browser would simply scroll to the top of the screen. I don't know why this happened, but forcing it all within the same viewport solved the problem.