3

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.

0

3 Answers 3

4
+50

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() {
  }
}
Sign up to request clarification or add additional context in comments.

5 Comments

Many thanks for the input @DavidZ, I have implemented your code and I see that the binding works well, in the sense that the dataservice's subject state does get updated. However I have added something to the recursive table: each row has a delete button. It removes the corresponding element from this.data in place by use of data.splice. Again I see that the state of the subject in the data service gets updated and the entry is removed from its array, but this change is not propagated to the view and the deleted row is still visible. How can I get the view to update?
In case you want your data to be mutable you can consider wrapping the data inside a DataSource and use it in conjunction with a Paginator. Here's a good post to help you: stackoverflow.com/questions/49272575/…
Indeed there is the option of feeding a DataSource. I have given it some thoughts and I see the obstacle that I will need to first "translate" my JSON file so that instead of containing arrays with subarrays, and subsubarrays in subarrays, ... it will have DataSources containing other DataSources as rows of their data member. Will this data structure "be able to run" a rectable on it and will these table be mutable with respect to adding or deleting rows at any depth?
I would try my best to avoid nesting tables, especially if you don't know the depth of nesting that you will require and if you have a big data set the angular change detection is going to be very very slow. I found some additional links that may help you: github.com/angular/material2/issues/6095 and github.com/angular/material2/issues/10755
Thanks for the insights, I just accepted your answer.
1

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

3 Comments

I am not sure about what does this.theData = observableOf(this.array); does but the mat-table is expecting, as input, an observable and not an array, so it looks like I cannot feed it this.array straightforwardly (I tried and get a TypeError: Cannot read property 'length' of undefined) I have tried wrapping things in a DataSource to mock some canonical examples from angular material website, yet unsuccessful. Thanks for the input btw.
I am trying to understand your solution, in my setup what does trigger ngOnChanges()? Any change on array made by a parent component or service?
@Learningisamess Since you are using the default change detection strategy, Any change to array object by a parent component should trigger ngOnChanges
1

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.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.