Browse Source

Add extensible table row detail feature

Introduces the ExtensibleTableRowDetailComponent for expandable row details in extensible tables. Updates the extensible table to support row detail templates via both direct input and content child component, adds toggle logic and emits rowDetailToggle events. Documentation and exports are updated accordingly.
pull/24660/head
Fahri Gedik 7 months ago
parent
commit
2a7ff01e88
  1. 189
      docs/en/framework/ui/angular/extensible-table-row-detail.md
  2. 10
      npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table-row-detail/extensible-table-row-detail.component.ts
  3. 1
      npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table-row-detail/index.ts
  4. 169
      npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table.component.html
  5. 36
      npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table.component.ts
  6. 1
      npm/ng-packs/packages/components/extensible/src/lib/components/index.ts
  7. 1
      npm/ng-packs/packages/components/extensible/src/public-api.ts

189
docs/en/framework/ui/angular/extensible-table-row-detail.md

@ -0,0 +1,189 @@
```json
//[doc-seo]
{
"Description": "Learn how to add expandable row details to data tables using the Extensible Table Row Detail component in ABP Framework Angular UI."
}
```
# Extensible Table Row Detail for Angular UI
## Introduction
The `<abp-extensible-table-row-detail>` component allows you to add expandable row details to any `<abp-extensible-table>`. When users click the expand icon, additional content is revealed below the row.
<img alt="Extensible Table Row Detail Example" src="./images/extensible-table-row-detail-example.png" width="800px" style="max-width:100%">
## Quick Start
### Step 1. Import the Component
Import `ExtensibleTableRowDetailComponent` in your component:
```typescript
import {
ExtensibleTableComponent,
ExtensibleTableRowDetailComponent
} from '@abp/ng.components/extensible';
@Component({
// ...
imports: [
ExtensibleTableComponent,
ExtensibleTableRowDetailComponent,
],
})
export class MyComponent { }
```
### Step 2. Add Row Detail Template
Place `<abp-extensible-table-row-detail>` inside `<abp-extensible-table>` with an `ng-template`:
```html
<abp-extensible-table [data]="data.items" [recordsTotal]="data.totalCount" [list]="list">
<abp-extensible-table-row-detail>
<ng-template let-row="row" let-expanded="expanded">
<div class="p-3">
<h5>{{ row.name }}</h5>
<p>ID: {{ row.id }}</p>
<p>Status: {{ row.isActive ? 'Active' : 'Inactive' }}</p>
</div>
</ng-template>
</abp-extensible-table-row-detail>
</abp-extensible-table>
```
An expand/collapse chevron icon will automatically appear in the first column of each row.
## API
### ExtensibleTableRowDetailComponent
| Input | Type | Default | Description |
|-------|------|---------|-------------|
| `rowHeight` | `string \| number` | `'100%'` | Height of the expanded row detail area |
### Template Context Variables
| Variable | Type | Description |
|----------|------|-------------|
| `row` | `R` | The current row data object |
| `expanded` | `boolean` | Whether the row is currently expanded |
## Usage Examples
### Basic Example
Display additional information when a row is expanded:
```html
<abp-extensible-table [data]="data.items" [list]="list">
<abp-extensible-table-row-detail>
<ng-template let-row="row">
<div class="p-3 border rounded m-2">
<strong>Details for: {{ row.name }}</strong>
<pre>{{ row | json }}</pre>
</div>
</ng-template>
</abp-extensible-table-row-detail>
</abp-extensible-table>
```
### With Custom Row Height
Specify a fixed height for the detail area:
```html
<abp-extensible-table-row-detail [rowHeight]="200">
<ng-template let-row="row">
<div class="p-3">Fixed 200px height content</div>
</ng-template>
</abp-extensible-table-row-detail>
```
### Using Expanded State
Apply conditional styling based on expansion state:
```html
<abp-extensible-table-row-detail>
<ng-template let-row="row" let-expanded="expanded">
<div class="p-3" [class.fade-in]="expanded">
<p>This row is {{ expanded ? 'expanded' : 'collapsed' }}</p>
</div>
</ng-template>
</abp-extensible-table-row-detail>
```
### With Badges and Localization
```html
<abp-extensible-table-row-detail>
<ng-template let-row="row">
<div class="p-3 bg-light border rounded m-2">
<div class="row">
<div class="col-md-6">
<p class="mb-1"><strong>{{ 'MyModule::Name' | abpLocalization }}:</strong></p>
<p class="text-muted">{{ row.name }}</p>
</div>
<div class="col-md-6">
<p class="mb-1"><strong>{{ 'MyModule::Status' | abpLocalization }}:</strong></p>
<p>
@if (row.isActive) {
<span class="badge bg-success">{{ 'AbpUi::Yes' | abpLocalization }}</span>
} @else {
<span class="badge bg-secondary">{{ 'AbpUi::No' | abpLocalization }}</span>
}
</p>
</div>
</div>
</div>
</ng-template>
</abp-extensible-table-row-detail>
```
## Alternative: Direct Template Input
For simpler use cases, you can use the `rowDetailTemplate` input on `<abp-extensible-table>` directly:
```html
<abp-extensible-table
[data]="data.items"
[list]="list"
[rowDetailTemplate]="detailTemplate"
/>
<ng-template #detailTemplate let-row="row">
<div class="p-3">{{ row.name }}</div>
</ng-template>
```
## Events
### rowDetailToggle
The `rowDetailToggle` output emits when a row is expanded or collapsed:
```html
<abp-extensible-table
[data]="data.items"
[list]="list"
(rowDetailToggle)="onRowToggle($event)"
>
<abp-extensible-table-row-detail>
<ng-template let-row="row">...</ng-template>
</abp-extensible-table-row-detail>
</abp-extensible-table>
```
```typescript
onRowToggle(row: MyDto) {
console.log('Row toggled:', row);
}
```
## See Also
- [Data Table Column Extensions](data-table-column-extensions.md)
- [Entity Action Extensions](entity-action-extensions.md)
- [Extensions Overview](extensions-overall.md)

10
npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table-row-detail/extensible-table-row-detail.component.ts

@ -0,0 +1,10 @@
import { Component, contentChild, input, TemplateRef } from '@angular/core';
@Component({
selector: 'abp-extensible-table-row-detail',
template: '',
})
export class ExtensibleTableRowDetailComponent<R = any> {
readonly rowHeight = input<string | number>('100%');
readonly template = contentChild(TemplateRef<{ row: R; expanded: boolean }>);
}

1
npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table-row-detail/index.ts

@ -0,0 +1 @@
export * from './extensible-table-row-detail.component';

169
npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table.component.html

@ -1,21 +1,30 @@
@if (isBrowser) {
<ngx-datatable
default
[rows]="data"
[count]="recordsTotal"
[list]="list"
[selectionType]="selectable ? _selectionType : undefined"
(activate)="tableActivate.emit($event)"
(select)="onSelect($event)"
[selected]="selected"
(scroll)="onScroll($event)"
[scrollbarV]="infiniteScroll"
[style.height]="getTableHeight()"
[loadingIndicator]="infiniteScroll && isLoading"
[footerHeight]="infiniteScroll ? false : 50"
>
@if(selectable) {
<ngx-datatable-column [width]="50" [sortable]="false" [canAutoResize]="false" [draggable]="false" [resizeable]="false">
<ngx-datatable #table default [rows]="data" [count]="recordsTotal" [list]="list"
[selectionType]="selectable ? _selectionType : undefined" (activate)="tableActivate.emit($event)"
(select)="onSelect($event)" [selected]="selected" (scroll)="onScroll($event)" [scrollbarV]="infiniteScroll"
[style.height]="getTableHeight()" [loadingIndicator]="infiniteScroll && isLoading"
[footerHeight]="infiniteScroll ? false : 50">
@if (effectiveRowDetailTemplate) {
<ngx-datatable-row-detail [rowHeight]="effectiveRowDetailHeight">
<ng-template let-row="row" let-expanded="expanded" class="bg-transparent" ngx-datatable-row-detail-template>
<ng-container class="bg-transparent"
*ngTemplateOutlet="effectiveRowDetailTemplate; context: { row: row, expanded: expanded }" />
</ng-template>
</ngx-datatable-row-detail>
<ngx-datatable-column [width]="50" [resizeable]="false" [sortable]="false" [draggable]="false"
[canAutoResize]="false">
<ng-template let-row="row" let-expanded="expanded" ngx-datatable-cell-template>
<a href="javascript:void(0)" class="text-decoration-none text-muted"
[attr.aria-label]="expanded ? 'Collapse' : 'Expand'" (click)="toggleExpandRow(row)">
<i class="fa" [class.fa-chevron-down]="!expanded" [class.fa-chevron-up]="expanded"></i>
</a>
</ng-template>
</ngx-datatable-column>
}
@if(selectable) {
<ngx-datatable-column [width]="50" [sortable]="false" [canAutoResize]="false" [draggable]="false"
[resizeable]="false">
<ng-template ngx-datatable-header-template let-value="value" let-allRowsSelected="allRowsSelected"
let-selectFn="selectFn">
@ -44,97 +53,67 @@
</ngx-datatable-column>
}
@if (actionsTemplate || (actionList.length && hasAtLeastOnePermittedAction)) {
<ngx-datatable-column
[name]="actionsText | abpLocalization"
[maxWidth]="_actionsColumnWidth() ?? undefined"
[width]="_actionsColumnWidth() ?? 200"
[canAutoResize]="!_actionsColumnWidth()"
[sortable]="false"
>
<ng-template let-row="row" let-i="rowIndex" ngx-datatable-cell-template>
<ng-container
*ngTemplateOutlet="actionsTemplate || gridActions; context: { $implicit: row, index: i }"
></ng-container>
<ng-template #gridActions>
@if (isVisibleActions(row)) {
<abp-grid-actions [index]="i" [record]="row" text="AbpUi::Actions"></abp-grid-actions>
}
</ng-template>
<ngx-datatable-column [name]="actionsText | abpLocalization" [maxWidth]="_actionsColumnWidth() ?? undefined"
[width]="_actionsColumnWidth() ?? 200" [canAutoResize]="!_actionsColumnWidth()" [sortable]="false">
<ng-template let-row="row" let-i="rowIndex" ngx-datatable-cell-template>
<ng-container
*ngTemplateOutlet="actionsTemplate || gridActions; context: { $implicit: row, index: i }"></ng-container>
<ng-template #gridActions>
@if (isVisibleActions(row)) {
<abp-grid-actions [index]="i" [record]="row" text="AbpUi::Actions"></abp-grid-actions>
}
</ng-template>
</ngx-datatable-column>
</ng-template>
</ngx-datatable-column>
}
@for (prop of propList; track prop.name; let i = $index) {
<ngx-datatable-column
*abpVisible="prop.columnVisible(getInjected)"
[width]="columnWidths[i] ?? 200"
[canAutoResize]="!columnWidths[i]"
[name]="(prop.isExtra ? '::' + prop.displayName : prop.displayName) | abpLocalization"
[prop]="prop.name"
[sortable]="prop.sortable"
>
<ng-template ngx-datatable-header-template let-column="column" let-sortFn="sortFn">
@if (prop.tooltip) {
<span
[ngbTooltip]="prop.tooltip.text | abpLocalization"
[placement]="prop.tooltip.placement || 'auto'"
container="body"
[class.pointer]="prop.sortable"
(click)="prop.sortable && sortFn(column)"
>
{{ column.name }} <i class="fa fa-info-circle" aria-hidden="true"></i>
</span>
} @else {
<span
[class.pointer]="prop.sortable"
(click)="prop.sortable && sortFn(column)"
>
{{ column.name }}
</span>
}
</ng-template>
<ng-template let-row="row" let-i="index" ngx-datatable-cell-template>
<ng-container *abpPermission="prop.permission; runChangeDetection: false">
<ng-container *abpVisible="row['_' + prop.name]?.visible">
@if (!row['_' + prop.name].component) {
@if (prop.type === 'datetime' || prop.type === 'date' || prop.type === 'time') {
<div
[innerHTML]="
<ngx-datatable-column *abpVisible="prop.columnVisible(getInjected)" [width]="columnWidths[i] ?? 200"
[canAutoResize]="!columnWidths[i]"
[name]="(prop.isExtra ? '::' + prop.displayName : prop.displayName) | abpLocalization" [prop]="prop.name"
[sortable]="prop.sortable">
<ng-template ngx-datatable-header-template let-column="column" let-sortFn="sortFn">
@if (prop.tooltip) {
<span [ngbTooltip]="prop.tooltip.text | abpLocalization" [placement]="prop.tooltip.placement || 'auto'"
container="body" [class.pointer]="prop.sortable" (click)="prop.sortable && sortFn(column)">
{{ column.name }} <i class="fa fa-info-circle" aria-hidden="true"></i>
</span>
} @else {
<span [class.pointer]="prop.sortable" (click)="prop.sortable && sortFn(column)">
{{ column.name }}
</span>
}
</ng-template>
<ng-template let-row="row" let-i="index" ngx-datatable-cell-template>
<ng-container *abpPermission="prop.permission; runChangeDetection: false">
<ng-container *abpVisible="row['_' + prop.name]?.visible">
@if (!row['_' + prop.name].component) {
@if (prop.type === 'datetime' || prop.type === 'date' || prop.type === 'time') {
<div [innerHTML]="
!prop.isExtra
? (row['_' + prop.name]?.value | async | abpUtcToLocal:prop.type)
: ('::' + (row['_' + prop.name]?.value | async | abpUtcToLocal:prop.type) | abpLocalization)
"
(click)="
" (click)="
prop.action && prop.action({ getInjected: getInjected, record: row, index: i })
"
[ngClass]="entityPropTypeClasses[prop.type]"
[class.pointer]="prop.action"
></div>
} @else {
<div
[innerHTML]="
" [ngClass]="entityPropTypeClasses[prop.type]" [class.pointer]="prop.action"></div>
} @else {
<div [innerHTML]="
!prop.isExtra
? (row['_' + prop.name]?.value | async)
: ('::' + (row['_' + prop.name]?.value | async) | abpLocalization)
"
(click)="
" (click)="
prop.action && prop.action({ getInjected: getInjected, record: row, index: i })
"
[ngClass]="entityPropTypeClasses[prop.type]"
[class.pointer]="prop.action"
></div>
}
} @else {
<ng-container
*ngComponentOutlet="
" [ngClass]="entityPropTypeClasses[prop.type]" [class.pointer]="prop.action"></div>
}
} @else {
<ng-container *ngComponentOutlet="
row['_' + prop.name].component;
injector: row['_' + prop.name].injector
"
></ng-container>
}
</ng-container>
"></ng-container>
}
</ng-container>
</ng-template>
</ngx-datatable-column>
</ng-container>
</ng-template>
</ngx-datatable-column>
}
</ngx-datatable>
}
}

36
npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table.component.ts

@ -4,6 +4,7 @@ import {
ChangeDetectorRef,
Component,
computed,
ContentChild,
EventEmitter,
inject,
Injector,
@ -17,13 +18,14 @@ import {
SimpleChanges,
TemplateRef,
TrackByFunction,
ViewChild,
} from '@angular/core';
import { AsyncPipe, isPlatformBrowser, NgComponentOutlet, NgTemplateOutlet } from '@angular/common';
import { Observable, filter, map, Subject, debounceTime, distinctUntilChanged } from 'rxjs';
import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap';
import { NgxDatatableModule, SelectionType } from '@swimlane/ngx-datatable';
import { NgxDatatableModule, SelectionType, DatatableComponent } from '@swimlane/ngx-datatable';
import {
ABP,
@ -53,6 +55,7 @@ import {
ROW_RECORD,
} from '../../tokens/extensions.token';
import { GridActionsComponent } from '../grid-actions/grid-actions.component';
import { ExtensibleTableRowDetailComponent } from './extensible-table-row-detail';
const DEFAULT_ACTIONS_COLUMN_WIDTH = 150;
@ -75,6 +78,12 @@ const DEFAULT_ACTIONS_COLUMN_WIDTH = 150;
],
templateUrl: './extensible-table.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
styles: [`
:host ::ng-deep .ngx-datatable.material .datatable-body .datatable-row-detail {
background: none;
padding: 0;
}
`],
})
export class ExtensibleTableComponent<R = any> implements OnChanges, AfterViewInit, OnDestroy {
readonly #injector = inject(Injector);
@ -127,6 +136,26 @@ export class ExtensibleTableComponent<R = any> implements OnChanges, AfterViewIn
@Output() loadMore = new EventEmitter<void>();
@Input() tableHeight: number;
// Row detail configuration (supports both direct template and child component)
@Input() rowDetailTemplate?: TemplateRef<{ row: R; expanded: boolean }>;
@Input() rowDetailHeight: string | number = '100%';
@Output() rowDetailToggle = new EventEmitter<R>();
@ContentChild(ExtensibleTableRowDetailComponent)
rowDetailComponent?: ExtensibleTableRowDetailComponent<R>;
@ViewChild('table') table!: DatatableComponent;
/** Gets the effective row detail template (from component or direct input) */
get effectiveRowDetailTemplate(): TemplateRef<any> | undefined {
return this.rowDetailComponent?.template() ?? this.rowDetailTemplate;
}
/** Gets the effective row detail height */
get effectiveRowDetailHeight(): string | number {
return this.rowDetailComponent?.rowHeight() ?? this.rowDetailHeight;
}
hasAtLeastOnePermittedAction: boolean;
readonly propList: EntityPropList<R>;
@ -290,6 +319,11 @@ export class ExtensibleTableComponent<R = any> implements OnChanges, AfterViewIn
return this.tableHeight ? `${this.tableHeight}px` : 'auto';
}
toggleExpandRow(row: R): void {
this.table?.rowDetail?.toggleExpandRow(row);
this.rowDetailToggle.emit(row);
}
ngAfterViewInit(): void {
if (!this.infiniteScroll) {
this.list?.requestStatus$?.pipe(filter(status => status === 'loading')).subscribe(() => {

1
npm/ng-packs/packages/components/extensible/src/lib/components/index.ts

@ -2,6 +2,7 @@ export * from './abstract-actions/abstract-actions.component';
export * from './extensible-form/extensible-form.component';
export * from './extensible-form/extensible-form-prop.component';
export * from './extensible-table/extensible-table.component';
export * from './extensible-table/extensible-table-row-detail';
export * from './date-time-picker/extensible-date-time-picker.component';
export * from './grid-actions/grid-actions.component';
export * from './page-toolbar/page-toolbar.component';

1
npm/ng-packs/packages/components/extensible/src/public-api.ts

@ -2,6 +2,7 @@ export * from './lib/components/date-time-picker/extensible-date-time-picker.com
export * from './lib/components/extensible-form/extensible-form-prop.component';
export * from './lib/components/extensible-form/extensible-form.component';
export * from './lib/components/extensible-table/extensible-table.component';
export * from './lib/components/extensible-table/extensible-table-row-detail';
export * from './lib/components/grid-actions/grid-actions.component';
export * from './lib/components/page-toolbar/page-toolbar.component';
export * from './lib/components/multi-select';

Loading…
Cancel
Save