Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
189 changes: 189 additions & 0 deletions docs/en/framework/ui/angular/extensible-table-row-detail.md
Original file line number Diff line number Diff line change
@@ -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."
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This document is not referenced in another one, so we can make a reference here to make it visible.

}
```

# 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%">

Copy link

Copilot AI Jan 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation references an image file "./images/extensible-table-row-detail-example.png" that is not included in this pull request. This will result in a broken image in the documentation. The image file should be added to the appropriate location or the image reference should be removed until the image is available.

Suggested change
<img alt="Extensible Table Row Detail Example" src="./images/extensible-table-row-detail-example.png" width="800px" style="max-width:100%">

Copilot uses AI. Check for mistakes.
## 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)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Component, contentChild, input, TemplateRef } from '@angular/core';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we still support the module usage in the packages, it is better to import this component ExtensibleTableRowDetailComponent in extensible module.


@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 }>);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './extensible-table-row-detail.component';
Original file line number Diff line number Diff line change
@@ -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"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use href="#" with (click)="toggleExpandRow(row); $event.preventDefault()" or better yet, use a <button> element styled as a link for better accessibility and security.

[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">
Expand Down Expand Up @@ -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>
}
}
Loading
Loading