Reputation: 3038
I'm stuck at creating a reusable component in Angular 4. I have a bunch of reports that all consist of a search form (fields are different for each report) and a material table result list (field list differs for each report). It works as expected when I duplicate the whole component for each report, but I want to refactor it into a reusable component/template and child components extending it. But the scopes are all wrong and I can't get my head around how this works.
report.component.ts (reusable component)
import {Component, ViewChild} from '@angular/core';
import {MatPaginator} from '@angular/material';
import 'rxjs/add/operator/map';
import {ReportsDataSource} from '../services/reports-datasource.service';
@Component({
selector: 'app-report',
templateUrl: './report.component.html',
})
export class ReportComponent {
@ViewChild(MatPaginator) paginator: MatPaginator;
/** result table columns */
columns = [];
/** Column definitions in order */
displayedColumns = this.columns.map(x => x.columnDef);
/** empty search parameters object, used for form field binding */
/** datasource service */
dataSource: ReportsDataSource;
/** submit the form */
getData() {
this.dataSource.getData();
}
}
report.component.html (reusable template)
<form (ngSubmit)="getData()" #ReportSearchForm="ngForm">
<ng-content select=".container-fluid"></ng-content>
<button type="submit" mat-button class="mat-primary" [disabled]="!ReportSearchForm.form.valid">Search</button>
</form>
<mat-table #table [dataSource]="dataSource">
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
<mat-header-cell *matHeaderCellDef>{{ column.header }}</mat-header-cell>
<mat-cell *matCellDef="let row">{{ column.cell(row) }}</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
</mat-table>
<mat-paginator #paginator
[length]="dataSource ? dataSource.meta.total_results : 0"
[pageSize]="dataSource ? dataSource.meta.per_page : 25"
[pageSizeOptions]="[10, 25, 50, 100]"
>
</mat-paginator>
childreport.component.ts (a specific report)
import {Component, OnInit} from '@angular/core';
import {ReportComponent} from '../report.component';
import {ChildreportService} from './childreport.service';
import {ReportsDataSource} from '../../services/reports-datasource.service';
@Component({
selector: 'app-report-child',
templateUrl: './childreport.component.html',
providers: [ChildreportService, ReportsDataSource]
})
export class ChildreportComponent extends ReportComponent implements OnInit {
constructor(private childreportService: ChildreportService) {
super();
}
/** result table columns */
columns = [
{columnDef: 'column1', header: 'Label 1', cell: (row) => `${row.column1}`},
{columnDef: 'column2', header: 'Label 2', cell: (row) => `${row.column2}`}
];
ngOnInit() {
this.dataSource = new ReportsDataSource(this.ChildreportService, this.paginator);
}
}
childreport.component.html (the search form for this report, embedded in the parent template)
<app-report>
<div class="container-fluid">
<mat-form-field>
<input matInput placeholder="some field" name="fieldx">
</mat-form-field>
</div>
</app-report>
What works: I get the form embedded in the main template and no errors.
What doesn't work: The form and table are bound to ReportComponent
instead of ChildreportComponent
. I kinda understand why this happens (because the scope of this template is that component) but I have no idea how I could "inherit" the template and be in the scope of the ChildreportComponent
. What am I missing?
Upvotes: 6
Views: 5186
Reputation: 3038
I figured it out myself. In fact, the solution is rather trivial. My mistake was to try two things at once in my report.component, providing a template as well as logic. What I ended up is an abstract component that holds the logic and is extended by each report, as well as several smaller components for the similar parts in each report (shell, result list, etc.). I also switched from template forms to reactive forms.
report-base.component.ts holds the common logic
import {OnInit} from '@angular/core';
import {FormBuilder, FormGroup} from '@angular/forms';
import {MatPaginator, MatSidenav} from '@angular/material';
import 'rxjs/add/operator/map';
import {ReportsDataSource} from '../common/services/reports-datasource.service';
import {ReportsService} from '../common/services/reports.service';
import {ReportsResultlistService} from '../common/services/reports-resultlist.service';
export abstract class ReportBaseComponent implements OnInit {
constructor(
protected _formBuilder: FormBuilder, protected _reportService: ReportsService, protected _resultlistService: ReportsResultlistService) {
}
/**
* For toggling the search form and resultlist action buttons
* @type {boolean}
*/
protected hasResults = false;
/** Default data source for the table */
protected dataSource: ReportsDataSource;
/** search form controls */
protected searchForm: FormGroup;
/** result table columns */
protected columns = [];
ngOnInit() {
this.createForm();
this.dataSource = new ReportsDataSource(this._reportService, this._resultlistService);
}
/**
* Builds the searchForm Group
*/
protected createForm() {
// create an empty form
this.searchForm = this._formBuilder.group({});
}
/**
* Submits the form/loads data (f.ex. pagination)
*/
protected getData() {
this.hasResults = true;
this.dataSource.search = this.searchForm.value;
this.dataSource.getData();
}
}
report-shell.component.ts is a CHILD component (one of my logical mistakes) that provides the shell around the components:
import {Component, Input} from '@angular/core';
import {ActivatedRoute} from '@angular/router';
@Component({
selector: 'app-report-shell',
templateUrl: './report-shell.component.html',
})
export class ReportShellComponent {
constructor(private route: ActivatedRoute) {
this.title = route.routeConfig.data['caption'];
}
@Input() hasResults = false;
title: string;
}
report-shell.component.html provides the HTML around the search form and result list
<mat-expansion-panel [expanded]="!hasResults">
<mat-expansion-panel-header>
Search
</mat-expansion-panel-header>
<ng-content select="form"></ng-content>
</mat-expansion-panel>
<div class="result-list">
<mat-toolbar class="result-header"><span>{{ title }}</span>
<span class="fill-remaining-space"></span>
<button class="fa fa-file-excel-o" (click)="exportExcel()"></button>
</mat-toolbar>
<ng-content select=".result-table"></ng-content>
</div>
So my reports extend the report-base and simply use the shell als a child:
childreport.component.ts is a specific report that only implements what is specific for this report
import {Component, OnInit} from '@angular/core';
import {FormBuilder, Validators} from '@angular/forms';
import {ReportChildreportService} from './childreport.service';
import {ReportsDataSource} from '../../common/services/reports-datasource.service';
import {ReportsResultlistService} from '../../common/services/reports-resultlist.service';
import {ReportBaseComponent} from '../report-base.component';
@Component({
selector: 'app-report-dispatches',
templateUrl: './dispatches.component.html',
providers: [ReportChildreportService, ReportsResultlistService, ReportsDataSource]
})
export class ReportDispatchesComponent extends ReportBaseComponent implements OnInit {
constructor(protected _reportService: ReportChildreportService, protected _formBuilder: FormBuilder, protected _resultlistService: ReportsResultlistService) {
super(_formBuilder, _reportService, _resultlistService);
}
/** result table columns */
columns = [
{columnDef: 'name', header: 'Name', cell: (row) => `${row.name}`}
];
createForm() {
this.searchForm = this._formBuilder.group({
name: ''
});
}
}
childreport.component.html
<app-report-shell [hasResults]="hasResults">
<form (ngSubmit)="getData()" [formGroup]="searchForm" novalidate>
<mat-form-field>
<input matInput placeholder="search for a name" name="name" formControlName="name">
<mat-error>Invalid name</mat-error>
</mat-form-field>
</div>
</div>
<app-form-buttons [status]="searchForm.status"></app-form-buttons>
</form>
<app-report-result-list
[(dataSource)]="dataSource"
[columns]="columns"
[displayedColumns]="displayedColumns"
class="result-table"
></app-report-result-list>
</app-report-shell>
I won't go into the details of the forms and resultlist components, this answer is long enough as it is :-)
So I managed to reduce code repetition a lot, although there still is some (evering in the childreport.component.html except for the form).
Upvotes: 4
Reputation: 1300
I suggest you to have a look to this article. @WjComponent decorator might give you a clue about your approach. What i understand from the article is you need a new component decorator to share properties between base and child classes.
Quote from article:
@Component({ selector: 'inherited-grid'
})
export class InheritedGrid extends wjGrid.WjFlexGrid {
...
}
Now we have the new element name for our component! But we’ve missed all the other necessary settings defined in the decorator of the base WjFlexGrid class. For example, WjFlexGrid’s decorator assigns the inputs decorator property with an array of grid properties available for bindings in markup. We lost it in our new component, and if you try to bind to them now, you’ll find that the bindings don’t work.
The answer: the @WjComponent decorator offered by the Wijmo for Angular 2 module. It’s used in the same way as the standard @Component decorator and accepts all @Component decorator’s properties (plus some that are Wijmo-specific), but its main benefit is that it merges its property values with the properties provided by the base class decorator. So the last step in our component definition is replacing @Component with @WjComponent: import { WjComponent } from 'wijmo/wijmo.angular2.directiveBase';
@WjComponent({
selector: 'inherited-grid'
})
export class InheritedGrid extends wjGrid.WjFlexGrid {
...
}
We may have redefined the decorator’s selector property with the ”inherited-grid” name, but all the other necessary properties like inputs and outputs were taken from the base WjFlexGrid component’s decorator. And now element creates our InheritedGrid component with all the property and event bindings correctly functioning!
Another approach might be defining the ReportComponent as a directive and share data between ChildReport and the base via @Host decorator.
You can also check out the source code of ngx-datatable. The source code of their examples and components are very informative and may give you ideas on sharing data between components and overriding templates.
Upvotes: 0