angulartypescriptcanvasngonchanges

Angular in combination with canvas element not respecting OnChanges


See this stackblitz: https://stackblitz.com/edit/angular-ivy-wecw7p?file=src%2Fapp%2Fcanvas%2Fcanvas.component.ts

I have a regular AppComponent that includes a child CanvasComponent (using 'chord' selector). The app component create a Chord object and passes it to the child canvas component:

<chord [chord]=chordData></chord>

The Chord interface only has a Name string property for now, and that name is shown in the AppComponent using an <input> field and it is rendered in the canvas component using {{chord.Name}}. So far, so good.

In my canvas component I also render a <canvas> element and show the chord name in there.

import {
  Component,
  ViewChild,
  Input,
  ElementRef,
  OnChanges,
  SimpleChanges
} from '@angular/core';
import { Chord } from '../models/Chord';

@Component({
  selector: 'chord',
  template:
    '<div *ngIf="chord">{{chord.Name}}<br/><br/><canvas #canvas width=100 height=100></canvas></div>',
  styleUrls: ['./canvas.component.css']
})
export class CanvasComponent implements OnChanges {
  @Input() chord: Chord | undefined;
  @ViewChild('canvas') canvas: ElementRef<HTMLCanvasElement> | undefined;

  constructor() {}

  ngOnChanges(changes: SimpleChanges): void {
    console.log('changes in canvas');
    this.draw();
  }

  ngAfterViewInit(): void {
    this.draw();
  }

  private draw(): void {
    if (this.canvas) {
      let elmnt = this.canvas.nativeElement;
      let ctx = elmnt.getContext('2d');
      if (ctx && this.chord) {
        ctx.fillStyle = '#dddddd';
        ctx.fillRect(0,0,100,100);

        ctx.fillStyle = '#000000';
        ctx.font = '20px Arial';
        ctx.fillText(this.chord.Name, 20, 40);
      }
    }
  }
}

The thing is, when I update the chord name using the input field, the name in the canvas component also changes but not inside the canvas element.

enter image description here

This is because the canvas needs to be redrawn, fair enough. I've implemented OnChanges, which I will be needing to redraw my canvas, but it is not hit in any way.

How can I make sure that, when the parent Chord object is updated, the canvas will also be redrawn?

And any tips on code improvements are welcome also, just starting out with Angular :)


Solution

  • The method ngOnChanges isn't getting fired because the chord input value is never a changing. When you update the name, you're probably just setting the property on the Chord object, not changing the object itself. Angular will not catch that change because it's still the same object.

    If you just passed the name as an input then that should work fine, and you should be seeing updates since strings are immutable and a new object is created. Alternatively, when the input value changes in the outer component, you could update the value of chord using object spread syntax: this.chordOuter = { ...this.chordOuter };. This should cause a new value to be set to the chord property.

    Spread Example

    class OuterComponent {
      chord: Chord = {};
    
      updateChord(partial: Partial<Chord>): void {
        this.chord = { ...this.chord, ...partial };
      }
    }
    

    Template

    <input [ngModel]="chord.name" (ngModelChange)="updateChord({ name: $event })" />
    <chord [chord]="chord"></chord>
    

    Reactive Forms

    If you're planning on using reactive forms, you can create an observable from valueChanges and just bind that to your component with the async pipe.

    class OuterComponent {
       readonly form = new FormGroup({ name: new FormControl('') });
       readonly chord$ = this.form.valueChanges.pipe(map(x => this.convertToChord(x)));
    
       private convertToChord(value: any): Chord {
          /* Implement this yourself */
       }
    }
    

    Template

    <chord [chord]="chord$ | async"></chord>