Browse Source

Merge pull request #24636 from abpframework/issue-24635

Add extensible table row detail feature
pull/24694/head
Yağmur Çelik 9 months ago
committed by GitHub
parent
commit
2cc3f59f9e
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      docs/en/framework/ui/angular/data-table-column-extensions.md
  2. 189
      docs/en/framework/ui/angular/extensible-table-row-detail.md
  3. BIN
      docs/en/framework/ui/angular/images/row-detail-image.png
  4. 10
      npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table-row-detail/extensible-table-row-detail.component.ts
  5. 1
      npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table-row-detail/index.ts
  6. 169
      npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table.component.html
  7. 36
      npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table.component.ts
  8. 1
      npm/ng-packs/packages/components/extensible/src/lib/components/index.ts
  9. 4
      npm/ng-packs/packages/components/extensible/src/lib/extensible.module.ts
  10. 4
      npm/ng-packs/packages/components/extensible/src/lib/models/row-detail.ts
  11. 2
      npm/ng-packs/packages/components/extensible/src/public-api.ts

1
docs/en/framework/ui/angular/data-table-column-extensions.md

@ -342,4 +342,5 @@ export const identityEntityPropContributors = {
## See Also
- [Extensible Table Row Detail](extensible-table-row-detail.md)
- [Customizing Application Modules Guide](../../architecture/modularity/extending/customizing-application-modules-guide.md)

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/row-detail-image.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` &#124; `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)

BIN
docs/en/framework/ui/angular/images/row-detail-image.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

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" ngx-datatable-row-detail-template>
<ng-container
*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>
<button type="button" class="btn btn-link text-decoration-none text-muted p-0"
[attr.aria-label]="expanded ? 'Collapse' : 'Expand'" (click)="toggleExpandRow(row)">
<i class="fa" [class.fa-chevron-down]="!expanded" [class.fa-chevron-up]="expanded"></i>
</button>
</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,8 @@ import {
ROW_RECORD,
} from '../../tokens/extensions.token';
import { GridActionsComponent } from '../grid-actions/grid-actions.component';
import { ExtensibleTableRowDetailComponent } from './extensible-table-row-detail';
import { RowDetailContext } from '../../models/row-detail';
const DEFAULT_ACTIONS_COLUMN_WIDTH = 150;
@ -75,6 +79,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 +137,23 @@ export class ExtensibleTableComponent<R = any> implements OnChanges, AfterViewIn
@Output() loadMore = new EventEmitter<void>();
@Input() tableHeight: number;
@Input() rowDetailTemplate?: TemplateRef<RowDetailContext<R>>;
@Input() rowDetailHeight: string | number = '100%';
@Output() rowDetailToggle = new EventEmitter<R>();
@ContentChild(ExtensibleTableRowDetailComponent)
rowDetailComponent?: ExtensibleTableRowDetailComponent<R>;
@ViewChild('table', { static: false }) table!: DatatableComponent;
protected get effectiveRowDetailTemplate(): TemplateRef<RowDetailContext<R>> | undefined {
return this.rowDetailComponent?.template() ?? this.rowDetailTemplate;
}
protected get effectiveRowDetailHeight(): string | number {
return this.rowDetailComponent?.rowHeight() ?? this.rowDetailHeight;
}
hasAtLeastOnePermittedAction: boolean;
readonly propList: EntityPropList<R>;
@ -290,6 +317,13 @@ export class ExtensibleTableComponent<R = any> implements OnChanges, AfterViewIn
return this.tableHeight ? `${this.tableHeight}px` : 'auto';
}
toggleExpandRow(row: R): void {
if (this.table && this.table.rowDetail) {
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';

4
npm/ng-packs/packages/components/extensible/src/lib/extensible.module.ts

@ -13,6 +13,7 @@ import {
ExtensibleFormComponent,
ExtensibleFormPropComponent,
ExtensibleTableComponent,
ExtensibleTableRowDetailComponent,
GridActionsComponent,
PageToolbarComponent,
ExtensibleDateTimePickerComponent,
@ -32,6 +33,7 @@ const importWithExport = [
CreateInjectorPipe,
ExtensibleFormComponent,
ExtensibleTableComponent,
ExtensibleTableRowDetailComponent,
ExtensibleFormMultiselectComponent,
];
@ -50,4 +52,4 @@ const importWithExport = [
],
exports: [...importWithExport],
})
export class ExtensibleModule {}
export class ExtensibleModule { }

4
npm/ng-packs/packages/components/extensible/src/lib/models/row-detail.ts

@ -0,0 +1,4 @@
export interface RowDetailContext<R = any> {
row: R;
expanded: boolean;
}

2
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';
@ -68,4 +69,5 @@ export * from './lib/utils/form-props.util';
export * from './lib/utils/props.util';
export * from './lib/utils/state.util';
export * from './lib/utils/model.utils';
export * from './lib/models/row-detail';
export * from './lib/extensible.module';

Loading…
Cancel
Save