Angular 7 Datatable后端分页模式下响应式列失效问题求助
Got it, let's tackle this responsive expansion issue in your server-side processed Angular DataTables. I’ve gone through your code and here’s exactly what’s breaking the functionality and how to fix it:
Root Cause
When using serverSide: true, DataTables takes full control of rendering table rows and handling data. Your current HTML template uses *ngFor to loop through configuration.data and build rows manually—this conflicts with DataTables' server-side rendering workflow. The library can’t recognize your custom-rendered columns, so it fails to trigger the responsive (+) expansion for hidden columns.
Additionally, your server-side ajax callback was returning an empty data: [] array, which meant DataTables had no actual rows to render at all. That’s a critical oversight!
Step-by-Step Fix
1. Simplify Your HTML Template
Let DataTables handle row rendering instead of manual *ngFor loops. Update your template to only define the table structure, not the rows:
<ng-container *ngIf="dtOptions"> <table datatable [dtOptions]="dtOptions" [dtTrigger]="dtTrigger" class="row-border hover"> <thead> <tr> <th *ngFor="let column of configuration.columns" [class.noExport]="column.data === 'remove' || column.data === 'more'"> {{ column.title || '' }} </th> </tr> </thead> <!-- Let DataTables auto-generate tbody for server-side mode --> <tbody></tbody> <!-- Keep empty state only for local data mode --> <tbody *ngIf="!configuration.ajax && configuration.data?.length === 0"> <tr style="width: 100% !important"> <td class="no-data-available" [attr.colspan]="numberOfColumns">No data available</td> </tr> </tbody> </table> </ng-container>
2. Centralize Column Rendering Logic
Create a reusable function to build DataTables column definitions with custom rendering (for icons, dates, and action buttons). This ensures consistency across both local and server-side modes:
private buildColumnDefs(): any[] { return this.configuration.columns.map(column => { const columnDef: any = { title: column.title, data: column.data, width: column.width, responsivePriority: column.responsivePriority, orderable: column.orderable ?? true, className: column.class }; // Custom display rendering for each column columnDef.render = (data: any, type: string, row: any) => { if (type !== 'display') return data; // Return raw data for sorting/filtering let content = ''; // Add icon if configured if (column.icon && this.showIcon(row, column)) { content += `<mat-icon style="color: ${column.icon.color || 'inherit'}" class="icon">${column.icon.iconName}</mat-icon>`; } // Format value (handle dates) let displayValue = data; if (column.dateFormat) { // Replicate Angular date pipe logic (adjust format options as needed) displayValue = new Date(data).toLocaleDateString('en-US', { year: 'numeric', month: column.dateFormat.includes('MM') ? '2-digit' : 'short', day: column.dateFormat.includes('dd') ? '2-digit' : 'numeric' }); } content += `<span>${displayValue}</span>`; // Replace content with action buttons for remove/more columns if (column.data === 'remove' && this.configuration.remove) { content = `<button mat-icon-button mat-tooltip="${this.configuration.remove.tooltip}"> <mat-icon>${this.configuration.remove.icon}</mat-icon> </button>`; } else if (column.data === 'more' && this.configuration.more) { content = `<button mat-icon-button mat-tooltip="${this.configuration.more.tooltip}"> <mat-icon>${this.configuration.more.icon}</mat-icon> </button>`; } return `<div class="align-icon-text">${content}</div>`; }; return columnDef; }); }
3. Fix Server-Side dtOptions Configuration
Update your ajax callback to return actual data, and use the centralized column definitions. Also, explicitly configure the responsive renderer for reliability:
if (this.configuration.ajax) { const pageSize = this.configuration.pageSize || 10; this.dtOptions = { processing: true, ajax: (dtParameters: any, callback) => { let URL = this.configuration.ajax.URL; const currentPage = (dtParameters.start + dtParameters.length) / pageSize; URL = `${URL}?page=${currentPage - 1}&pageSize=${pageSize}`; this.crud.get(URL, this.configuration.ajax.headers).subscribe((res) => { const tableData = this.getObjectByPath(res); callback({ recordsTotal: res['paging']['total'], recordsFiltered: res['paging']['total'], data: tableData // Return actual data here (not empty array!) }); }); }, dom: 'Bfrtip', bLengthChange: false, bFilter: true, bInfo: false, bAutoWidth: false, pageLength: pageSize, order: [], serverSide: true, pagingType: 'full_numbers', columns: this.buildColumnDefs(), responsive: { details: { renderer: $.fn.dataTable.Responsive.renderer.tableAll() } }, language: { search: 'Filter: ' }, buttons: [ { extend: 'print', exportOptions: { columns: "thead th:not(.noExport)" } }, { extend: 'excel', exportOptions: { columns: "thead th:not(.noExport)" } }, { extend: 'csv', exportOptions: { columns: "thead th:not(.noExport)" } } ], // Bind action button clicks using rowCallback rowCallback: (row: Node, data: any) => { $(row).find('button[mat-icon-button]').each((_, btn) => { const iconText = $(btn).find('mat-icon').text(); if (iconText === this.configuration.remove?.icon) { $(btn).off('click').on('click', () => this.deleteRow(data)); } else if (iconText === this.configuration.more?.icon) { $(btn).off('click').on('click', () => this.showMore(data)); } }); return row; } }; }
4. Update Local Data Mode to Use Same Column Definitions
Keep consistency by applying the same column rendering logic to local data mode:
} else if (this.configuration.data) { if (typeof this.configuration.data === 'object' && !(this.configuration.data instanceof Array)) { this.configuration.data = [this.configuration.data]; } const pageSize = this.configuration.pageSize || 10; this.dtOptions = { data: this.configuration.data, bLengthChange: false, bFilter: true, bInfo: false, bAutoWidth: false, pageLength: pageSize, order: [], dom: 'Bfrtip', pagingType: 'full_numbers', columns: this.buildColumnDefs(), responsive: true, language: { search: 'Filter: ' }, buttons: [ { extend: 'print', exportOptions: { columns: "thead th:not(.noExport)" } }, { extend: 'excel', exportOptions: { columns: "thead th:not(.noExport)" } }, { extend: 'csv', exportOptions: { columns: "thead th:not(.noExport)" } } ], rowCallback: (row: Node, data: any) => { // Reuse the same action button binding logic from server-side mode $(row).find('button[mat-icon-button]').each((_, btn) => { const iconText = $(btn).find('mat-icon').text(); if (iconText === this.configuration.remove?.icon) { $(btn).off('click').on('click', () => this.deleteRow(data)); } else if (iconText === this.configuration.more?.icon) { $(btn).off('click').on('click', () => this.showMore(data)); } }); return row; } }; }
Key Notes
- Data Consistency: By letting DataTables render rows, the library can properly track column visibility and trigger responsive expansion.
- Ajax Fix: Returning actual data from your server callback is essential—without it, DataTables had no rows to render at all.
- Event Binding: Using
rowCallbackensures action buttons work correctly even when rows are re-rendered (e.g., pagination, filtering).
内容的提问来源于stack exchange,提问作者Paraskevas Louka

