Reputation: 8277
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.
Upvotes: 3
Views: 20130
Reputation: 1304
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.
Upvotes: 1
Reputation: 441
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() {
}
}
Upvotes: 4
Reputation: 31835
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:
this.theData = observableOf(this.array);
has a real purpose, then implement OnChanges
into your component and update theData
into ngOnChanges()
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
Upvotes: 1