Я пытаюсь создать универсальный компонент данных с возможностью повторного использования с разбивкой на страницы / сортировку / фильтрацию, который использует таблицу материалов, но удаляет значительную часть шаблона, используемого при его использовании, во всем приложении.
Целями этой таблицы являются следующие:
- использование таблицы mat с paginator / mat-sort / etc. без связки в каждом шаблоне и компоненте, который его использует
- берет наблюдаемый источник данных и обрабатывает подписку и управление данными
- допускает проекцию содержимого с учетом данных (например, * matCellDef)
- возможно в будущем перейти от таблицы mat и использовать другого поставщика данных, не затрагивая ничего за пределами универсального компонента с данными (так что никаких директив или компонентов mat * нигде, кроме самого универсального компонента с данными)
Я уже разработал это, и по большей части он работает отлично, но у меня возникла странная проблема, связанная с сортировкой. Все загружается правильно, и сортировка работает отлично - по первому столбцу. Я могу сортировать столько раз, сколько захочу, и это будет хорошо работать в первом столбце. Как только я выполняю сортировку по любому другому столбцу, я получаю сообщение «Ошибка ОШИБКИ: предоставлено повторяющееся имя определения столбца: ...», и после этого действия сортировки для любого столбца (включая первый) просто генерируют эту ошибку и ничего не делают. Другие взаимодействия таблиц (нумерация страниц, фильтрация) также перестают работать в этом случае.
Не имеет значения, в каком столбце, в каком порядке или в каких атрибутах определен шаблон или нет и т. Д.
Ниже приведен пример использования компонента и всего соответствующего кода.
Заранее спасибо за помощь!
Пример файла с использованием моего компонента данных
<app-datatable [dataSource]="testData">
<app-datatable-column name="name">
<ng-container *datatableCell="let data">
<a href="somewhere/{{data.id}}">{{data.name}}</a>
</ng-container>
</app-datatable-column>
<app-datatable-column name="age" label="Years on Earth"></app-datatable-column>
datatable.component.ts
@Component({
selector: 'app-datatable',
template: `
<mat-form-field *ngIf="filterable">
<input matInput (keyup)="applyFilter($event.target.value)" placeholder="Filter">
</mat-form-field>
<table mat-table [dataSource]="data">
<tr mat-header-row *matHeaderRowDef="columns"></tr>
<tr mat-row *matRowDef="let row; columns: columns"></tr>
</table>
<mat-progress-bar mode="indeterminate" *ngIf="loading"></mat-progress-bar>
<mat-paginator [pageSizeOptions]="pageSizeOptions" *ngIf="pagination"></mat-paginator>`,
})
export class DatatableComponent<T> implements AfterContentInit {
/** The list of columns which should appear - this will default to all defined columns if not set */
@Input() columns: string[];
/** The datasource from which the table should populate */
@Input() dataSource: Observable<DatatableDataSource<T>>;
/** Toggles for pagination, sorting, and filtering */
@Input() pagination: boolean = true;
@Input() sortable: boolean = true;
@Input() filterable: boolean = true;
/** Page size options provided */
@Input() pageSizeOptions: number[] = [10, 25, 50, 100];
/** Default sort options */
@Input() defaultSortColumn: string;
@Input() defaultSortDirection: 'asc' | 'desc' = 'asc';
/** Column definitions added via the DatatableColumn component */
@ContentChildren(DatatableColumnComponent) datatableColumns: QueryList<DatatableColumnComponent<T>>;
/** References to the table and row definitions in this template */
@ViewChild(MatTable) table: MatTable<T>;
@ViewChild(MatHeaderRowDef) headerRows: MatHeaderRowDef;
@ViewChildren(MatRowDef) rows: QueryList<MatRowDef<T>>;
/** Paginator */
@ViewChild(MatPaginator) paginator: MatPaginator;
/** Sorter */
public sort: MatSort = new MatSort();
/** The data resultant from the provided dataSource - this is fed directly into the table for display */
public data: MatTableDataSource<T>;
/** Whether the data is loading from the source or not */
public loading: boolean = true;
/**
* Apply filter to current data source
* @param filterValue The value to filter against
* @return void
*/
public applyFilter(filterValue: string): void {
if (this.filterable) {
this.data.filter = filterValue.trim().toLowerCase();
if (this.data.paginator) {
this.data.paginator.firstPage();
}
}
}
ngAfterContentInit() {
// define columns if not already set
this.columns = this.columns || this.datatableColumns.map(column => column.name);
// set data result and status from source
this.dataSource.pipe(takeUntil(this.ngUnsubscribe)).subscribe((data: DatatableDataSource<T>) => {
this.loading = data.loading;
this.data = new MatTableDataSource<T>(data.data);
this.data.sort = this.sort;
this.data.paginator = (this.pagination) ? this.paginator : null;
// if there is sorting, default sort must also be set for table interactions to work
if (this.sortable) {
// get a sub-set of columns which are defined as sortable
const sortableColumns = this.datatableColumns
.filter(column => (column.sortable && this.columns.includes(column.name)))
.map(column => column.name);
// only implement sorting if there are sortable columns
if (sortableColumns.length > 0) {
this.data.sort = this.sort;
this.sort.sort(<MatSortable>{
id: (this.defaultSortColumn && sortableColumns.includes(this.defaultSortColumn)) ? this.defaultSortColumn : sortableColumns[0],
start: this.defaultSortDirection
});
}
}
});
// register datatable-columns to the table
this.datatableColumns.forEach(datatableColumn => {
this.table.addColumnDef(datatableColumn.columnDef);
// register sort header for each sortable column
if (datatableColumn.sortable) {
this.sort.register(<MatSortable>{
id: datatableColumn.name,
start: 'asc'
});
datatableColumn.sortUpdate.pipe(takeUntil(this.ngUnsubscribe)).subscribe((column: string) => {
this.sort.sort(<MatSortable>{
id: column,
start: 'asc'
});
});
}
});
// send updated sort direction information back to column for display update when sorting event occurs
this.sort.sortChange.subscribe(event => {
this.datatableColumns.find(column => column.name = event.active).sortDirection = event.direction;
});
}
}
DataTable-column.component.ts
@Component({
selector: 'app-datatable-column',
template: `
<ng-container matColumnDef>
<th mat-header-cell *matHeaderCellDef
(click)="sort()"
[class.datatable-header-sort]="sortable"
[class.datatable-header-sort-asc]="sortable && sortDirection === 'asc'"
[class.datatable-header-sort-desc]="sortable && sortDirection === 'desc'">
{{getTitle()}}
<mat-icon *ngIf="sortable">arrow_right_alt</mat-icon>
</th>
<td mat-cell *matCellDef="let data">
<ng-container *ngIf="!template">{{getData(data)}}</ng-container>
<ng-container *ngTemplateOutlet="template; context: {$implicit: data}"></ng-container>
</td>
</ng-container>
`,
styleUrls: ['./datatable-column.component.scss']
})
export class DatatableColumnComponent<T> implements OnDestroy, OnInit {
/** The current direction of the sorting of the column */
public sortDirection: SortDirection = '';
/** Column name that should be used to reference this column. */
@Input()
get name(): string { return this._name; }
set name(name: string) {
this._name = name;
this.columnDef.name = name;
}
_name: string;
/**
* Text label that should be used for the column header. If this property is not
* set, the header text will default to the column name.
*/
@Input() label: string;
/**
* Accessor function to retrieve the data should be provided to the cell. If this
* property is not set, the data cells will assume that the column name is the same
* as the data property the cells should display.
*/
@Input() dataAccessor: ((data: T, name: string) => string);
/** Alignment of the cell values. */
@Input() align: 'before' | 'after' = 'before';
/** Whether the column is sortable */
@Input() sortable: boolean = true;
/** Event to emit when sorting is updated */
@Output() sortUpdate = new EventEmitter<string>();
/** Reference to column definitions and sort headers */
@ViewChild(MatColumnDef) columnDef: MatColumnDef;
@ViewChild(MatSortHeader) sortHeader: MatSortHeader;
@ContentChild(DatatableCellDirective, {read: TemplateRef}) template;
constructor(@Optional() public table: MatTable<any>) { }
/**
* Gives a formatted version of the name if label is not present
* @return The formatted label
*/
public getTitle(): string {
return this.label || startCase(this.name);
}
public getData(data: T): any {
return this.dataAccessor ? this.dataAccessor(data, this.name) : (data as any)[this.name];
}
/**
* Triggers a sort action on this column by emitting a sort update action to the parent datatable
*/
public sort(): void {
this.sortUpdate.emit(this.name);
}
ngOnInit() {
if (this.table) {
this.table.addColumnDef(this.columnDef);
}
}
ngOnDestroy() {
if (this.table) {
this.table.removeColumnDef(this.columnDef);
}
}
}
DataTable-cell.directive.ts
@Directive({
selector: '[datatableCell]'
})
export class DatatableCellDirective {
constructor(public template: TemplateRef<any>){}
}