Angular6: view not updating on data change despite binding

17,806

Solution 1

Because in your ngOnInit you are changing the type of data from an Array into an Observable.

In your case since you are binding to [dataSource], which expects a type of dataSource: DataSource<T> | Observable<T[]> | T[], you can simply have the parent component pass in the data through an async pipe to your child component.

Parent:

@Component({
  selector: 'parent-component',
  template: `
<div>
    <app-rectable [data]="data$ | async" [isTop]="true"></app-rectable>
    ...
</div>
  `,
})

export class ParentComponent implements OnInit {

    data$: Observable<SomeType[]>;
    constructor(private dataService: DataService) {
    }

    ngOnInit() {
        // fetch your data via the service
        this.data$ = this.dataService.fetchData();
    }
 }

Child:

@Component({
  selector: 'app-rectable',
  template: `
<div class="example-container mat-elevation-z8">
  <mat-table [dataSource]="data">
  ...
</div>
  `,
})

export class RectableComponent implements OnInit {

  @Input() data: SomeType[];
  @Input() isTop: boolean;

  displayedColumns = ["name", "min", "max", "value"];
  isExpansionDetailRow = (i: number, row: Object) => row.hasOwnProperty('detailRow');

  constructor() {
  }
}

Solution 2

I don't know what's the purpose of this line this.theData = observableOf(this.array);, but this is what breaks your code (I think what led you to do this is a misunderstanding about Angular @Input() decorator and about Observable.of() operator which emits one event with the initial value of the array).

By doing this, you break the reference from the @Input() array, so when you update it from a parent component, the template does not update since it is bound to theData, and theData is set only on component initialization.

Then the solution is:

  • If this.theData = observableOf(this.array); has a real purpose, then implement OnChanges into your component and update theData into ngOnChanges()
  • If this.theData = observableOf(this.array); has no real purpose (and I strongly believe it hasn't), then just completely remove this.theData and just bind your template to array

Solution 3

Angular 2+ is one way data binding.

So if you change data in your root you can see changes inside your component but you cant see changes in your root when it changed inside your component.

You can make it work this way:

import { EventEmitter} from '@angular/core';

export class YourComponent {
    @Input() item: any;
    @Output() itemChange = new EventEmitter();
  emitItem() {
      this.itemChange.emit(this.item);
  }
}

This will do the trick.

  this.itemChange.emit(this.item);

I use it this way

  <input matInput type=number [(ngModel)]="item" (ngModelChange)="emitItem()" (input)="update($event)">

So when item changes emitItem is called and I'll have my changes in root component.

I hope this can be helpful.

Share:
17,806
Learning is a mess
Author by

Learning is a mess

Ignorance was bliss - Piece of cheese

Updated on June 04, 2022

Comments

  • Learning is a mess
    Learning is a mess almost 2 years

    I made a custom component out of Angular material table. It is basically a recursive table that shows a subtable when clicking on a row, and a subsubtable when clicking on a row of the later. It required a recursive template. The table input data is quite large and takes the form of a JSON with arrays of JSON, etc... This format was decided by the rest of the project and is inflexible.

    The data is stored in a variable called data. My table has input field to modify entries in data and I checked that it does work properly, my issue is that if updating data from outside the table, the table view does not change, there is not update. I am surprised by that as use binding when passing data to the component. I am beginner at Angular and not comfortable with Subjects and Observables yet, so I have not looked in that direction.

    Here is the code.

    For the recursive component:

    @Component({
      selector: 'app-rectable',
      template: `
    <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
    
    <div class="example-container mat-elevation-z8">
      <mat-table [dataSource]="theData">
    
    
        <ng-container matColumnDef="name">
          <mat-header-cell *matHeaderCellDef> channel </mat-header-cell>
          <mat-cell *matCellDef="let element">
            <button *ngIf="element.isexpandable" mat-icon-button (click)="element.isexpanded = !element.isexpanded;">
              <mat-icon class="mat-icon-rtl-mirror">
                {{element.isexpanded ? 'expand_more' : 'chevron_right'}}
              </mat-icon>
            </button>
            <mat-form-field class="example-full-width">
              <input matInput [(ngModel)]="element.name" (input)="update($event)">
            </mat-form-field>
          </mat-cell>
        </ng-container>
    
        <ng-container matColumnDef="min">
          <mat-header-cell *matHeaderCellDef> min </mat-header-cell>
          <mat-cell *matCellDef="let element">
            <input matInput type=number [(ngModel)]="element.min" (input)="update($event)">
          </mat-cell>
        </ng-container>
    
        <ng-container matColumnDef="max">
          <mat-header-cell *matHeaderCellDef> max </mat-header-cell>
          <mat-cell *matCellDef="let element">
            <input matInput type=number [(ngModel)]="element.max" (input)="update($event)">
          </mat-cell>
        </ng-container>
    
        <ng-container matColumnDef="value">
          <mat-header-cell *matHeaderCellDef> value </mat-header-cell>
          <mat-cell *matCellDef="let element">
            {{element.value}}
          </mat-cell>
        </ng-container>
    
        <ng-container matColumnDef="expandedDetail">
          <mat-cell *matCellDef="let detail">
            <app-rectable *ngIf="detail.element.isexpandable" [array]="detail.element.variables" [isTop]="false">
            </app-rectable>
    
          </mat-cell>
        </ng-container>
    
        <div *ngIf="isTop">
        <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
        </div>
        <mat-row *matRowDef="let row; columns: displayedColumns;" matRipple class="element-row" [class.expanded]="row.isexpanded" >
        </mat-row>
        <mat-row *matRowDef="let row; columns: ['expandedDetail']; when: isExpansionDetailRow" [@detailExpand]="row.element.isexpanded ? 'expanded' : 'collapsed'" style="overflow: hidden">
        </mat-row>
      </mat-table>
    </div>
    <div *ngIf="!theData.value.length" > EMPTY DATA INPUT </div>
      `,
      styleUrls: ['./rectable.component.css'],
      animations: [
        trigger('detailExpand', [
          state('collapsed', style({ height: '0px', minHeight: '0', visibility: 'hidden' })),
          state('expanded', style({ height: '*', visibility: 'visible' })),
          transition('expanded <=> collapsed', animate('225ms cubic-bezier(0.4, 0.0, 0.2, 1)')),
        ]),
      ],
    })
    
    export class RectableComponent implements OnInit {
    
      @Input() array;
    
      @Input() isTop: boolean;
      theData = [];
      displayedColumns = ["name", "min", "max", "value"];
      isExpansionDetailRow = (i: number, row: Object) => row.hasOwnProperty('detailRow');
    
      constructor() {
      }
    
      ngOnInit() {
        this.theData = observableOf(this.array);
      }
    
    }
    

    The root component html is

    <app-rectable [array]='data' [isTop]="true"></app-rectable>
    

    Where data is a JSON object, which I prefer not to explicit here.