Search code examples
angularangular5angular-ngmodel

Update value of ngModel is not reflected inside the component


I have a component with ControlValueAccessor implementation, but when the bound value is changed outside the component, it does not update correctly.

EDIT:
Stackblitz example: https://stackblitz.com/edit/angular-lmojnj

I will explain the senario.
At the beginning there are two images in the array, if I click on one of the images it will trigger toggleSelected of images-sorter which will update the tempImages to have only 1 image (the one that was not clicked).
Then, If I put a url of image in the input of parent.component and click enter, it will trigger addImage of parent.component which pushes the url to tempImages and therefore should do the logic in the images-sorter (should call writeValue automatically cause value changed? - and it doesnt). Now, if I click back on the same image the I clicked before (or the other image which is selected), the new image that I added before appears! It should appear when I add it to tempImages array cause it's a two way binding, isnt it?

In order to solve it, I added reference to images sorter ( <app-images-sorter2 #imagesSorter [(ngModel)]="tempImages"></app-images-sorter2> ) and then pass the reference to addImage and call imageSorterCompoennt.writeValue(this.tempImages); which update what I expect, but it's bad practice and wrong solution as far as I know

images-sorter2.component.ts:

import { Component, forwardRef, ViewEncapsulation } from '@angular/core';
import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms';

@Component({
  selector: 'app-images-sorter2',
  templateUrl: './images-sorter2.component.html',
  styleUrls: ['./images-sorter2.component.scss'],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => ImagesSorter2Component),
      multi: true
    }
  ],
  encapsulation: ViewEncapsulation.None
})
export class ImagesSorter2Component implements ControlValueAccessor {
  public allImages: string[] = [];
  public images: string[] = [];

  private onChange: (_: any) => void = () => {};

  constructor() {}

  public writeValue(value: string[]): void {
    this.images = value || [];
    this.images.forEach((image) => {
      if (!this.allImages.includes(image)) {
        this.allImages.push(image);
      }
    });
    this.onChange(this.images);
  }

  public registerOnChange(fn: (_: any) => void): void {
    this.onChange = fn;
  }

  public registerOnTouched(): void {}

  public isSelected(image: string): boolean {
    return this.images.includes(image);
  }

  public getImageModelIndex(image: string): number {
    return this.images.indexOf(image);
  }

  public toggleSelected(image: string): void {
    const indexOfImage = this.getImageModelIndex(image);
    let newValue;
    if (indexOfImage > -1) {
      newValue = [...this.images.slice(0, indexOfImage), ...this.images.slice(indexOfImage + 1)];
    } else {
      newValue = [...this.images, image];
    }
    this.writeValue(newValue);
  }
}

images-sorter2.component.html:

<div fxLayout="row wrap">
  <div *ngFor="let image of allImages"
       class="image-wrapper"
       [ngClass]="{'selected': isSelected(image)}"
       (click)="toggleSelected(image)">
    <div class="index-container">
      <div>{{ getImageModelIndex(image)+1 }}</div>
    </div>
    <div class="image-container">
      <img [src]="image" />
    </div>
  </div>
</div>

parent.component.ts:

import { Component, ViewEncapsulation } from '@angular/core';

@Component({
  selector: 'app-parent',
  templateUrl: './parent.component.html',
  styleUrls: ['./parent.component.scss'],
  encapsulation: ViewEncapsulation.None
})
export class ParentComponent {
  public tempImages = [
    'https://upload.wikimedia.org/wikipedia/commons/thumb/a/a5/Google_Chrome_icon_%28September_2014%29.svg/1200px-Google_Chrome_icon_%28September_2014%29.svg.png',
    'https://upload.wikimedia.org/wikipedia/commons/thumb/f/fa/Apple_logo_black.svg/1200px-Apple_logo_black.svg.png'
  ];
  public newImage: string = '';

  public addImage(): void {
    this.tempImages.push(this.newImage);
    this.newImage = '';
  }
}

parent.component.html:

<app-images-sorter2 [(ngModel)]="tempImages"></app-images-sorter2>
  <div>
    <mat-form-field>
      <input matInput
             type="text"
             [(ngModel)]="newImage"
             (keyup.enter)="addImage()"
             placeholder="Image URL">
    </mat-form-field>
  </div>

Solution

  • I had this issue before and it seems that ControlValueAccesor has issues with noticing collections changes, so you have to resassign the array.

    For your example change line 18 in the parent.ts file from

    this.tempImages.push(this.newImage);
    

    to

    this.tempImages = this.tempImages.concat([this.newImage]);
    

    and it should work fine.