Name | Description |
---|---|
public constructor()
|
Create a new instance of the Datatable class.
Parameters:
|
public reload()
|
Reload the table data from the Ajax data source.
Parameters:
Return:
|
public adjustColumns()
|
Adjust column layout. |
public filter()
|
Filter row by the specified string.
Parameters:
|
public getContainer()
|
Returns a table wrapper element.
Parameters:
Return:
|
public getFilterContainer()
|
Returns a table filter container element.
Parameters:
Return:
|
public createRow()
|
Create a row.
Parameters:
Return:
|
public deleteRow()
|
Delete row.
Parameters:
Return:
|
public updateRow()
|
Update row.
Parameters:
Return:
|
public getRowData()
|
Gets the data for a single row or all rows in the DataTable.
You can access the column data in a row using the name specified in the data property during column definition.
Parameters:
Return:
|
public getRowCount()
|
Get the number of rows.
Parameters:
Return:
|
public getRowNodes()
|
Get row HTML elements.
Return:
|
public getRowObject()
|
Get the DataTable API instance containing the selected rows.
Parameters:
Return:
|
public column()
|
Select the column found by a the column selector.
Parameters:
Return:
|
public clear()
|
Clear the table of all data.
Return:
|
protected ajaxErrorHook()
|
Hook function called when an error occurs in an Ajax request to retrieve table data that can be implemented in a subclass. This method receives an HTTP status code and an XMLHttpRequest object. Parameters:
|
Name | Description |
---|---|
public api: DataTables.Api
|
DataTables.Api instance. This is read-only. |
Name | Position | Office | Age | Start date | Salary |
---|---|---|---|---|---|
Tiger Nixon | System Architect | Edinburgh | 61 | 2011/04/25 | 320800 |
Garrett Winters | Accountant | Tokyo | 63 | 2011/07/25 | 170750 |
Ashton Cox | Junior Technical Author | San Francisco | 66 | 2009/01/12 | 86000 |
Cedric Kelly | Senior Javascript Developer | Edinburgh | 22 | 2012/03/29 | 433060 |
<!--begin::Wrapper-->
<div class="d-flex flex-stack flex-wrap mb-5">
<!--begin::Search-->
<div class="d-flex align-items-center position-relative my-1 mb-2 mb-md-0">
<!--begin::Svg Icon | path: icons/duotune/general/gen021.svg-->
<span class="svg-icon svg-icon-1 position-absolute ms-4">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect opacity="0.5" x="17.0365" y="15.1223" width="8.15546" height="2" rx="1" transform="rotate(45 17.0365 15.1223)" fill="currentColor" />
<path d="M11 19C6.55556 19 3 15.4444 3 11C3 6.55556 6.55556 3 11 3C15.4444 3 19 6.55556 19 11C19 15.4444 15.4444 19 11 19ZM11 5C7.53333 5 5 7.53333 5 11C5 14.4667 7.53333 17 11 17C14.4667 17 17 14.4667 17 11C17 7.53333 14.4667 5 11 5Z" fill="currentColor" />
</svg>
</span>
<!--end::Svg Icon-->
<input data-ref="basicTableKeyword" data-on-search-basic-table type="text" class="form-control form-control-solid w-300px ps-15" placeholder="Search by name" maxlength="70" />
</div>
<!--end::Search-->
</div>
<!--end::Wrapper-->
<!--begin::Table-->
<table data-ref="basicTable" class="table table-row-bordered gy-5">
<thead>
<tr class="text-start text-gray-700 fw-bold fs-7 gs-0">
<th>Name</th>
<th>Position</th>
<th>Office</th>
<th>Age</th>
<th>Start date</th>
<th>Salary</th>
</tr>
</thead>
<tbody>
<tr>
<td>Tiger Nixon</td>
<td>System Architect</td>
<td>Edinburgh</td>
<td>61</td>
<td>2011/04/25</td>
<td>320800</td>
</tr>
<tr>
<td>Garrett Winters</td>
<td>Accountant</td>
<td>Tokyo</td>
<td>63</td>
<td>2011/07/25</td>
<td>170750</td>
</tr>
<tr>
<td>Ashton Cox</td>
<td>Junior Technical Author</td>
<td>San Francisco</td>
<td>66</td>
<td>2009/01/12</td>
<td>86000</td>
</tr>
<tr>
<td>Cedric Kelly</td>
<td>Senior Javascript Developer</td>
<td>Edinburgh</td>
<td>22</td>
<td>2012/03/29</td>
<td>433060</td>
</tr>
</tbody>
</table>
<!--end::Table-->
import {components} from 'metronic-extension';
// Initialize the DataTable.
const initBasicTable = () => {
let targetIndex = 0;
return new components.Datatable(ref.basicTable, {
columnDefs: [
{targets: targetIndex++, name: 'name'},
{targets: targetIndex++, name: 'position'},
{targets: targetIndex++, name: 'office'},
{targets: targetIndex++, name: 'age'},
{targets: targetIndex++, name: 'startDate'},
{targets: targetIndex++, name: 'salary'},
],
pageLength: 3
});
}
// Initialize the form.
const initBasicTableSearchForm = () => {
$('body').on('input', '[data-on-search-basic-table]', () => {
// Filter data by keyword.
basicTable.filter('name:name', ref.basicTableKeyword.val());
});
}
// Get references to elements with data-ref attributes.
const ref = components.selectRef();
// Initialize the component and set up event listeners.
const basicTable = initBasicTable();
initBasicTableSearchForm();
Name | Position | Office | Age | Start date | Salary |
---|
<!--begin::Wrapper-->
<div class="d-flex flex-stack flex-wrap mb-5">
<!--begin::Search-->
<div class="d-flex align-items-center position-relative my-1 mb-2 mb-md-0">
<!--begin::Svg Icon | path: icons/duotune/general/gen021.svg-->
<span class="svg-icon svg-icon-1 position-absolute ms-4">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect opacity="0.5" x="17.0365" y="15.1223" width="8.15546" height="2" rx="1" transform="rotate(45 17.0365 15.1223)" fill="currentColor" />
<path d="M11 19C6.55556 19 3 15.4444 3 11C3 6.55556 6.55556 3 11 3C15.4444 3 19 6.55556 19 11C19 15.4444 15.4444 19 11 19ZM11 5C7.53333 5 5 7.53333 5 11C5 14.4667 7.53333 17 11 17C14.4667 17 17 14.4667 17 11C17 7.53333 14.4667 5 11 5Z" fill="currentColor" />
</svg>
</span>
<!--end::Svg Icon-->
<input data-ref="serverSideProcessingTableKeyword" data-on-search-server-side-processing-table type="text" class="form-control form-control-solid w-300px ps-15" placeholder="Search by name" maxlength="70" />
</div>
<!--end::Search-->
</div>
<!--end::Wrapper-->
<!--begin::Table-->
<table data-ref="serverSideProcessingTable" class="table table-row-bordered gy-5">
<thead>
<tr class="text-start text-gray-700 fw-bold fs-7 gs-0">
<th>Name</th>
<th>Position</th>
<th>Office</th>
<th>Age</th>
<th>Start date</th>
<th>Salary</th>
</tr>
</thead>
<tbody></tbody>
</table>
<!--end::Table-->
import {components} from 'metronic-extension';
// Initialize the DataTable.
const initServerSideProcessingTable = () => {
let targetIndex = 0;
return new components.Datatable(ref.serverSideProcessingTable, {
ajax: {
url: '/api/persons/pages',
data: d => {
// Set filter parameters.
d.search = {keyword: ref.serverSideProcessingTableKeyword.val()};
}
},
columnDefs: [
{targets: targetIndex++, data: 'name'},
{targets: targetIndex++, data: 'position'},
{targets: targetIndex++, data: 'office'},
{targets: targetIndex++, data: 'age'},
{targets: targetIndex++, data: 'startDate'},
{targets: targetIndex++, data: 'salary'},
],
pageLength: 3
});
}
// Initialize the form.
const initServerSideProcessingTableSearchForm = () => {
$('body').on('input', '[data-on-search-server-side-processing-table]', () => {
// Reload when the filter is changed.
// The filter information is set in the parameters sent to the server from the ajax.data optional function.
serverSideProcessingTable.reload();
})
}
// Get references to elements with data-ref attributes.
const ref = components.selectRef();
// Initialize the component and set up event listeners.
const serverSideProcessingTable = initServerSideProcessingTable();
initServerSideProcessingTableSearchForm();
{
"data": [
{
"id": 5,
"name": "Airi Satou",
"position": "Accountant",
"office": "Tokyo",
"age": 33,
"startDate": "2008-11-28",
"salary": 162700
},
{
"id": 25,
"name": "Angelica Ramos",
"position": "Chief Executive Officer (CEO)",
"office": "London",
"age": 47,
"startDate": "2009-10-09",
"salary": 1200000
},
{
"id": 3,
"name": "Ashton Cox",
"position": "Junior Technical Author",
"office": "San Francisco",
"age": 66,
"startDate": "2009-01-12",
"salary": 86000
}
],
"recordsTotal": 57,
"recordsFiltered": 57,
"draw": "1"
}
const router = require('express').Router();
const {query, validationResult} = require('express-validator');
const PersonModel = require('../../models/PersonModel');
// Get person page data.
router.get('/pages', [
// Validate parameters.
query('start').not().isEmpty().isInt({min: 0}),
query('length').not().isEmpty().isInt({min: 1}),
query('order').not().isEmpty().isIn(['name', 'position', 'office', 'age', 'startDate', 'salary']),
query('dir').not().isEmpty().isIn(['asc', 'desc']),
query('search.keyword').trim().optional({nullable: true, checkFalsy: true}).isLength({max: 70}),
query('draw').not().isEmpty().isInt({min: 1}),
], async (req, res) => {
// Check validation results.
const result = validationResult(req);
if (!result.isEmpty())
// If the parameter is invalid, a 400 error is returned.
return void res.status(400).end();
// Get page data.
const data = await PersonModel.paginate(req.query);
// Set the received drawing count as-is in the response.
data.draw = req.query.draw;
res.json(data);
});
module.exports = router;
const Model = require('express-sweet').database.Model;
const {merge} = require('deep-fusion');
module.exports = class extends Model {
static get table() {
return 'person';
}
static get attributes() {
return {
id: {
type: this.DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
name: this.DataTypes.STRING,
position: this.DataTypes.STRING,
office: this.DataTypes.STRING,
age: this.DataTypes.INTEGER,
startDate: this.DataTypes.DATE,
salary: this.DataTypes.DECIMAL(10, 2),
};
}
/**
* Get page data.
*/
static async paginate(paginationOptions) {
// Initialize options.
paginationOptions = merge({
start: 0,
length: 30,
order: 'name',
dir: 'asc',
search: {
keyword: null,
},
}, paginationOptions);
// Where clause for filtering data.
const whereClause = {};
if (paginationOptions.search.keyword)
whereClause.name = {[super.Op.like]: `%${paginationOptions.search.keyword}%`};
// Calculate total records.
const recordsTotal = await super.count();
// Calculate filtered records.
const recordsFiltered = await super.count({where: whereClause});
// Fetch paginated data.
const data = await super.findAll({
where: whereClause,
offset: parseInt(paginationOptions.start, 10),
limit: parseInt(paginationOptions.length, 10),
order: [
[super.col(paginationOptions.order), paginationOptions.dir],
],
raw: true,
});
return {data, recordsTotal, recordsFiltered};
}
}
Name | Position | Office | Age | Start date | Salary |
---|---|---|---|---|---|
Tiger Nixon | System Architect | Edinburgh | 61 | 2011/04/25 | 320800 |
Garrett Winters | Accountant | Tokyo | 63 | 2011/07/25 | 170750 |
Ashton Cox | Junior Technical Author | San Francisco | 66 | 2009/01/12 | 86000 |
Cedric Kelly | Senior Javascript Developer | Edinburgh | 22 | 2012/03/29 | 433060 |
<!--begin::Table-->
<table data-ref="columnVisibilityTable" class="table table-row-bordered gy-5">
<thead>
<tr class="text-start text-gray-700 fw-bold fs-7 gs-0">
<th>Name</th>
<th>Position</th>
<th>Office</th>
<th>Age</th>
<th>Start date</th>
<th>Salary</th>
</tr>
</thead>
<tbody>
<tr>
<td>Tiger Nixon</td>
<td>System Architect</td>
<td>Edinburgh</td>
<td>61</td>
<td>2011/04/25</td>
<td>320800</td>
</tr>
<tr>
<td>Garrett Winters</td>
<td>Accountant</td>
<td>Tokyo</td>
<td>63</td>
<td>2011/07/25</td>
<td>170750</td>
</tr>
<tr>
<td>Ashton Cox</td>
<td>Junior Technical Author</td>
<td>San Francisco</td>
<td>66</td>
<td>2009/01/12</td>
<td>86000</td>
</tr>
<tr>
<td>Cedric Kelly</td>
<td>Senior Javascript Developer</td>
<td>Edinburgh</td>
<td>22</td>
<td>2012/03/29</td>
<td>433060</td>
</tr>
</tbody>
</table>
<!--end::Table-->
import {components} from 'metronic-extension';
// Initialize the DataTable.
const initColumnVisibilityTable = () => {
let targetIndex = 0;
return new components.Datatable(ref.columnVisibilityTable, {
dom: `<'row align-items-center'<'col-auto'B><'col dataTables_pager'p>><'row'<'col-12'tr>><'row'<'col-12 dataTables_pager'p>>`,
columnDefs: [
{targets: targetIndex++, data: 'name'},
{targets: targetIndex++, data: 'position'},
{targets: targetIndex++, data: 'office'},
{targets: targetIndex++, data: 'age'},
{targets: targetIndex++, data: 'startDate'},
{targets: targetIndex++, data: 'salary'},
],
pageLength: 3,
buttons: [
// Button to toggle column visibility.
{
extend: 'colvis',
text: 'Show / hide columns',
// Columns selector that defines the columns to include in the column visibility button set.
// CSS selectors, column indexes, etc. can be used to specify columns to switch visibility.
columns: ':eq(1),:eq(2),:eq(3),:eq(4),:eq(5)',
// columns: [1,2,3,4,5],
}
],
// Save the column visibility in the browser.
stateSave: true,
/* Enable/Disable saving for various datatables elements. Delete any items you do not wish to save.
- data.columns.search: This option allows for the saving of the search applied to individual columns to be enabled or disabled.
- data.columns.visible: This option allows for the saving of the visibility of the columns to be enabled or disabled.
- data.length: This option allows for the saving of the page length to be enabled or disabled.
- data.order: This option allows for the saving of the tables column sorting to be enabled or disabled.
- data.paging: This option allows for the saving of the paging to be enabled or disabled.
- data.scroller: This option allows for the saving of the scroller position to be enabled or disabled.
- data.search: This option allows for the saving of the search to be enabled or disabled.
- data.searchBuilder: This option allows for the saving of the searchBuilder state to be enabled or disabled.
- data.searchPanes: This option allows for the saving of the searchPanes state to be enabled or- disabled.
- data.select: This option allows for the saving of the select state to be enabled or disabled.
*/
stateSaveParams: (_, data) => {
delete data.columns.search;
// delete data.columns.visible;
delete data.length;
delete data.order;
delete data.paging;
delete data.scroller;
delete data.search;
delete data.searchBuilder;
delete data.searchPanes;
delete data.select;
}
});
}
// Get references to elements with data-ref attributes.
const ref = components.selectRef();
// Initialize the component and set up event listeners.
const columnVisibilityTable = initColumnVisibilityTable();
Name | Position | Office | Age | Start date | Salary |
---|---|---|---|---|---|
Tiger Nixon | System Architect | Edinburgh | 61 | 2011/04/25 | 320800 |
Garrett Winters | Accountant | Tokyo | 63 | 2011/07/25 | 170750 |
Ashton Cox | Junior Technical Author | San Francisco | 66 | 2009/01/12 | 86000 |
Cedric Kelly | Senior Javascript Developer | Edinburgh | 22 | 2012/03/29 | 433060 |
<!--begin::Table-->
<table data-ref="columnVisibilityWithIconButtonTable" class="table table-row-bordered gy-5">
<thead>
<tr class="text-start text-gray-700 fw-bold fs-7 gs-0">
<th>Name</th>
<th>Position</th>
<th>Office</th>
<th>Age</th>
<th>Start date</th>
<th>Salary</th>
</tr>
</thead>
<tbody>
<tr>
<td>Tiger Nixon</td>
<td>System Architect</td>
<td>Edinburgh</td>
<td>61</td>
<td>2011/04/25</td>
<td>320800</td>
</tr>
<tr>
<td>Garrett Winters</td>
<td>Accountant</td>
<td>Tokyo</td>
<td>63</td>
<td>2011/07/25</td>
<td>170750</td>
</tr>
<tr>
<td>Ashton Cox</td>
<td>Junior Technical Author</td>
<td>San Francisco</td>
<td>66</td>
<td>2009/01/12</td>
<td>86000</td>
</tr>
<tr>
<td>Cedric Kelly</td>
<td>Senior Javascript Developer</td>
<td>Edinburgh</td>
<td>22</td>
<td>2012/03/29</td>
<td>433060</td>
</tr>
</tbody>
</table>
<!--end::Table-->
import {components} from 'metronic-extension';
// Initialize the DataTable.
const initColumnVisibilityWithIconButtonTable = () => {
let targetIndex = 0;
return new components.Datatable(ref.columnVisibilityWithIconButtonTable, {
dom: `<'row align-items-center'<'col dataTables_pager'p><'col-auto'B>><'row'<'col-12'tr>><'row'<'col-12 dataTables_pager'p>>`,
columnDefs: [
{targets: targetIndex++, data: 'name'},
{targets: targetIndex++, data: 'position'},
{targets: targetIndex++, data: 'office'},
{targets: targetIndex++, data: 'age'},
{targets: targetIndex++, data: 'startDate'},
{targets: targetIndex++, data: 'salary'},
],
pageLength: 3,
buttons: {
dom: {
button: {
// Disable the default button class (btn btn-secondary) to set the icon button (btn-icon).
className: null,
},
buttonLiner: {
// By default, a span element is added within the button, so disable it.
tag: null,
}
},
buttons: [
// Button to toggle column visibility.
{
extend: 'colvis',
columns: ':eq(1),:eq(2),:eq(3),:eq(4),:eq(5)',
text: '<i class="ki-solid ki-gear fs-1"></i>',
// Icon button style. Switch between them as you wish.
// className: 'btn btn-sm btn-icon btn-color-primary btn-active-light-primary',
className: 'btn btn-icon btn-color-gray-500 btn-active-color-primary',
// Display column drop-downs inside the table container. If this is not set, the column dropdowns will overflow the table container.
align: 'container',
}
],
},
// Save the column visibility in the browser.
stateSave: true,
/* Enable/Disable saving for various datatables elements. Delete any items you do not wish to save.
- data.columns.search: This option allows for the saving of the search applied to individual columns to be enabled or disabled.
- data.columns.visible: This option allows for the saving of the visibility of the columns to be enabled or disabled.
- data.length: This option allows for the saving of the page length to be enabled or disabled.
- data.order: This option allows for the saving of the tables column sorting to be enabled or disabled.
- data.paging: This option allows for the saving of the paging to be enabled or disabled.
- data.scroller: This option allows for the saving of the scroller position to be enabled or disabled.
- data.search: This option allows for the saving of the search to be enabled or disabled.
- data.searchBuilder: This option allows for the saving of the searchBuilder state to be enabled or disabled.
- data.searchPanes: This option allows for the saving of the searchPanes state to be enabled or- disabled.
- data.select: This option allows for the saving of the select state to be enabled or disabled.
*/
stateSaveParams: (_, data) => {
delete data.columns.search;
// delete data.columns.visible;
delete data.length;
delete data.order;
delete data.paging;
delete data.scroller;
delete data.search;
delete data.searchBuilder;
delete data.searchPanes;
delete data.select;
}
});
}
// Get references to elements with data-ref attributes.
const ref = components.selectRef();
// Initialize the component and set up event listeners.
const columnVisibilityWithIconButtonTable = initColumnVisibilityWithIconButtonTable();
firstAjax
option to false in the DataTable
constructor disables the initial Ajax call.
Name | Position | Office | Age | Start date | Salary |
---|
<!--begin::Wrapper-->
<div class="d-flex flex-stack flex-wrap mb-5">
<!--begin::Toolbar-->
<div class="d-flex justify-content-end">
<button data-on-load-disable-first-ajax-call-table type="button" class="btn btn-primary">Call Ajax to load data</button>
</div>
<!--end::Toolbar-->
</div>
<!--end::Wrapper-->
<!--begin::Table-->
<table data-ref="disableFirstAjaxCallTable" class="table table-row-bordered gy-5">
<thead>
<tr class="text-start text-gray-700 fw-bold fs-7 gs-0">
<th>Name</th>
<th>Position</th>
<th>Office</th>
<th>Age</th>
<th>Start date</th>
<th>Salary</th>
</tr>
</thead>
<tbody></tbody>
</table>
<!--end::Table-->
import {components} from 'metronic-extension';
// Initialize the DataTable.
const initDisableFirstAjaxCallTable = () => {
let targetIndex = 0;
return new components.Datatable(ref.disableFirstAjaxCallTable, {
firstAjax: false,
ajax: '/api/persons/pages',
columnDefs: [
{targets: targetIndex++, data: 'name'},
{targets: targetIndex++, data: 'position'},
{targets: targetIndex++, data: 'office'},
{targets: targetIndex++, data: 'age'},
{targets: targetIndex++, data: 'startDate'},
{targets: targetIndex++, data: 'salary'},
],
pageLength: 3
});
}
// Initialize the form.
const initDisableFirstAjaxCallTableForm = () => {
$('body').on('click', '[data-on-load-disable-first-ajax-call-table]', () => disableFirstAjaxCallTable.reload());
}
// Get references to elements with data-ref attributes.
const ref = components.selectRef();
// Initialize the component and set up event listeners.
const disableFirstAjaxCallTable = initDisableFirstAjaxCallTable();
initDisableFirstAjaxCallTableForm();
const router = require('express').Router();
const {query, validationResult} = require('express-validator');
const PersonModel = require('../../models/PersonModel');
// Get person page data.
router.get('/pages', [
// Validate parameters.
query('start').not().isEmpty().isInt({min: 0}),
query('length').not().isEmpty().isInt({min: 1}),
query('order').not().isEmpty().isIn(['name', 'position', 'office', 'age', 'startDate', 'salary']),
query('dir').not().isEmpty().isIn(['asc', 'desc']),
query('search.keyword').trim().optional({nullable: true, checkFalsy: true}).isLength({max: 70}),
query('draw').not().isEmpty().isInt({min: 1}),
], async (req, res) => {
// Check validation results.
const result = validationResult(req);
if (!result.isEmpty())
// If the parameter is invalid, a 400 error is returned.
return void res.status(400).end();
// Get page data.
const data = await PersonModel.paginate(req.query);
// Set the received drawing count as-is in the response.
data.draw = req.query.draw;
res.json(data);
});
module.exports = router;
const Model = require('express-sweet').database.Model;
const {merge} = require('deep-fusion');
module.exports = class extends Model {
static get table() {
return 'person';
}
static get attributes() {
return {
id: {
type: this.DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
name: this.DataTypes.STRING,
position: this.DataTypes.STRING,
office: this.DataTypes.STRING,
age: this.DataTypes.INTEGER,
startDate: this.DataTypes.DATE,
salary: this.DataTypes.DECIMAL(10, 2),
};
}
/**
* Get page data.
*/
static async paginate(paginationOptions) {
// Initialize options.
paginationOptions = merge({
start: 0,
length: 30,
order: 'name',
dir: 'asc',
search: {
keyword: null,
},
}, paginationOptions);
// Where clause for filtering data.
const whereClause = {};
if (paginationOptions.search.keyword)
whereClause.name = {[super.Op.like]: `%${paginationOptions.search.keyword}%`};
// Calculate total records.
const recordsTotal = await super.count();
// Calculate filtered records.
const recordsFiltered = await super.count({where: whereClause});
// Fetch paginated data.
const data = await super.findAll({
where: whereClause,
offset: parseInt(paginationOptions.start, 10),
limit: parseInt(paginationOptions.length, 10),
order: [
[super.col(paginationOptions.order), paginationOptions.dir],
],
raw: true,
});
return {data, recordsTotal, recordsFiltered};
}
}
Order ID | Customer Name | Order Date | Shipping Address | Payment Status | Delivery Date | Order Status | Order Type | Discount | Order Notes | |
---|---|---|---|---|---|---|---|---|---|---|
name
|
Quantity
1
|
Unit Price
1
|
||||||||
1001 | John Smith | 2024-12-01 | 123 Main St, City | Paid | 2024-12-05 | Delivered | Online | 0 | No special instructions | |
1002 | Emily Johnson | 2024-12-15 | 456 Oak Rd, Town | Pending | 2024-12-18 | Processing | In-Store | 5 | Requires gift wrapping | |
1003 | Alice Johnson | 2024-12-03 | 789 Oak St, Suburb | Unpaid | 2024-12-07 | Processing | Online | 10% | Deliver during business hours | |
1004 | Michael Brown | 2024-12-20 | 789 Pine Ave, City | Paid | 2024-12-22 | Shipped | Online | 10 | N/A |
<!--begin::Table-->
<table data-ref="subtable" class="table table-row-bordered gy-5">
<thead>
<tr class="text-start text-gray-700 fw-bold fs-7 gs-0">
<th>Order ID</th>
<th>Customer Name</th>
<th>Order Date</th>
<th>Shipping Address</th>
<th>Payment Status</th>
<th>Delivery Date</th>
<th>Order Status</th>
<th>Order Type</th>
<th>Discount</th>
<th>Order Notes</th>
<th class="text-end"></th>
</tr>
</thead>
<tbody>
<!--begin::SubTable template-->
<template id="subtable-template">
<tr>
<td colspan="2">
<div class="d-flex align-items-center gap-3">
<!--begin:Icon-->
<span class="symbol symbol-40px">
<span class="symbol-label bg-transparent">
<i class="ki-duotone ki-plus-square fs-2x">
<span class="path1"></span>
<span class="path2"></span>
<span class="path3"></span>
</i>
</span>
</span>
<!--end:Icon-->
<!--begin:Info-->
<span class="fs-7 fw-bold" data-subtable-field="name">name</span>
<!--end:Info-->
</div>
</td>
<td class="text-end">
<div class="text-gray-900 fs-7">Quantity</div>
<div class="fs-7 fw-bold" data-subtable-field="quantity">1</div>
</td>
<td class="text-end">
<div class="text-gray-900 fs-7">Unit Price</div>
<div class="fs-7 fw-bold" data-subtable-field="unit_price">1</div>
</td>
</tr>
</template>
<!--end::SubTable template-->
<tr>
<td>1001</td>
<td>John Smith</td>
<td>2024-12-01</td>
<td>123 Main St, City</td>
<td>Paid</td>
<td>2024-12-05</td>
<td>Delivered</td>
<td>Online</td>
<td>0</td>
<td>No special instructions</td>
<td class="text-end">
<button data-on-expand-row type="button" class="btn btn-sm btn-icon btn-light btn-active-light-primary toggle h-25px w-25px">
<i class="ki-duotone ki-plus fs-3 m-0 toggle-off"></i>
<i class="ki-duotone ki-minus fs-3 m-0 toggle-on"></i>
</button>
</td>
</tr>
<tr>
<td>1002</td>
<td>Emily Johnson</td>
<td>2024-12-15</td>
<td>456 Oak Rd, Town</td>
<td>Pending</td>
<td>2024-12-18</td>
<td>Processing</td>
<td>In-Store</td>
<td>5</td>
<td>Requires gift wrapping</td>
<td class="text-end">
<button data-on-expand-row type="button" class="btn btn-sm btn-icon btn-light btn-active-light-primary toggle h-25px w-25px">
<i class="ki-duotone ki-plus fs-3 m-0 toggle-off"></i>
<i class="ki-duotone ki-minus fs-3 m-0 toggle-on"></i>
</button>
</td>
</tr>
<!-- This row has no subtable data -->
<tr>
<td>1003</td>
<td>Alice Johnson</td>
<td>2024-12-03</td>
<td>789 Oak St, Suburb</td>
<td>Unpaid</td>
<td>2024-12-07</td>
<td>Processing</td>
<td>Online</td>
<td>10%</td>
<td>Deliver during business hours</td>
<td class="text-end">
<button data-on-expand-row type="button" class="btn btn-sm btn-icon btn-light btn-active-light-primary toggle h-25px w-25px">
<i class="ki-duotone ki-plus fs-3 m-0 toggle-off"></i>
<i class="ki-duotone ki-minus fs-3 m-0 toggle-on"></i>
</button>
</td>
</tr>
<tr>
<td>1004</td>
<td>Michael Brown</td>
<td>2024-12-20</td>
<td>789 Pine Ave, City</td>
<td>Paid</td>
<td>2024-12-22</td>
<td>Shipped</td>
<td>Online</td>
<td>10</td>
<td>N/A</td>
<td class="text-end">
<button data-on-expand-row type="button" class="btn btn-sm btn-icon btn-light btn-active-light-primary toggle h-25px w-25px">
<i class="ki-duotone ki-plus fs-3 m-0 toggle-off"></i>
<i class="ki-duotone ki-minus fs-3 m-0 toggle-on"></i>
</button>
</td>
</tr>
</tbody>
</table>
<!--end::Table-->
import {components} from 'metronic-extension';
// Get references to elements with data-ref attributes.
const ref = components.selectRef();
// Initialize the DataTable.
const initSubtable = () => {
let targetIndex = 0;
return new components.Datatable(ref.subtable, {
responsive: true,
columnDefs: [
{targets: targetIndex++, responsivePriority: 1, data: 'orderId', name: 'orderId'},
{targets: targetIndex++, responsivePriority: 2, data: 'customerName', name: 'customerName'},
{targets: targetIndex++, data: 'orderDate', name: 'orderDate'},
{targets: targetIndex++, data: 'shippingAddress', name: 'shippingAddress'},
{targets: targetIndex++, data: 'paymentStatus', name: 'paymentStatus'},
{targets: targetIndex++, data: 'deliveryDate', name: 'deliveryDate'},
{targets: targetIndex++, data: 'orderStatus', name: 'orderStatus'},
{targets: targetIndex++, data: 'orderType', name: 'orderType'},
{targets: targetIndex++, data: 'discount', name: 'discount'},
{targets: targetIndex++, data: 'orderNotes', name: 'orderNotes'},
{
targets: targetIndex++,
responsivePriority: 3,
data: 'showSubtable',
name: 'showSubtable',
render: (data, type, row) => {
// Check if subtable data exists for this row.
const hasSubtableData = subtableData.some(item => item.orderId.toString() === row.orderId);
return hasSubtableData ? data : '';
},
},
],
pageLength: 4,
drawCallback: settings => {
resetSubtable();
},
});
}
// Handles the row expand button click event.
const handleRowExpand = () => {
$(subtableContainer).on('click', '[data-on-expand-row]', event => {
event.stopImmediatePropagation();
event.preventDefault();
const expandButton = event.currentTarget;
const row = expandButton.closest('tr');
const rowClasses = ['isOpen', 'border-bottom-0'];
// Toggle subtable visibility.
if (row.classList.contains('isOpen')) {
// Close subtable.
removeSubtableRows(row);
row.classList.remove(...rowClasses);
expandButton.classList.remove('active');
} else {
// Open subtable.
const orderId = subtable.getRowData(row).orderId;
const subtableRows = subtableData.filter(data => data.orderId == orderId);
const subtableRowCount = subtableRows.length;
for (let [index, data] of Object.entries(subtableRows))
populateSubtableRow(data, row, parseInt(index, 10), subtableRowCount);
row.classList.add(...rowClasses);
expandButton.classList.add('active');
}
})
}
// Removes subtable rows associated with the given parent row, excluding rows with the 'child' class.
const removeSubtableRows = parentRow => {
let nextRow = parentRow.nextSibling;
while (nextRow) {
// Store the next sibling before potentially removing nextRow.
const nextSibling = nextRow.nextSibling;
if (nextRow.getAttribute('data-is-subtable-row') === 'true')
// Remove the subtable row.
nextRow.parentNode.removeChild(nextRow);
else if (!nextRow.classList.contains('child'))
// Stop if the next row is not a subtable row and doesn't have the 'child' class.
break;
// Move to the next sibling.
nextRow = nextSibling; // Update nextRow to the stored next sibling.
}
}
// Populates a subtable row with data and inserts it into the table.
const populateSubtableRow = (data, parentRow, index, subtableRowCount) => {
// Clone the template row.
const subRowFragment = subtableTemplate.content.cloneNode(true);
const subRow = subRowFragment.children[0];
// Set an attribute to identify the subtable row.
subRow.setAttribute('data-is-subtable-row', 'true');
// Populate data fields in the subtable row.
const nameElement = subRow.querySelector('[data-subtable-field="name"]');
const quantityElement = subRow.querySelector('[data-subtable-field="quantity"]');
const unitPriceElement = subRow.querySelector('[data-subtable-field="unit_price"]');
nameElement.innerText = data.name;
quantityElement.innerText = data.quantity;
unitPriceElement.innerText = data.unitPrice;
// Apply rounded corners based on row position.
applyRoundedCorners(subRow, index, subtableRowCount);
// Insert the new subtable row into the table.
const tbody = subtableContainer.querySelector('tbody');
tbody.insertBefore(subRow, parentRow.nextSibling);
}
// Applies rounded corners to the first and last rows of the subtable.
const applyRoundedCorners = (subRow, index, subtableRowCount) => {
const columns = subRow.querySelectorAll('td');
const firstColumn = columns[0];
const lastColumn = columns[columns.length - 1];
if (subtableRowCount === 1) {
// Single row.
firstColumn.classList.add('rounded', 'rounded-end-0');
lastColumn.classList.add('rounded', 'rounded-start-0');
subRow.classList.add('border-bottom-0');
} else {
// Multiple rows.
if (index === 0) { // First row.
firstColumn.classList.add('rounded-start', 'rounded-top-0');
lastColumn.classList.add('rounded-end', 'rounded-top-0');
subRow.classList.add('border-bottom-0');
}
if (index === subtableRowCount - 1) { // Last row.
firstColumn.classList.add('rounded-start', 'rounded-bottom-0');
lastColumn.classList.add('rounded-end', 'rounded-bottom-0');
}
}
}
// Resets the subtable by removing all subtable rows and resetting row styles.
const resetSubtable = () => {
const subtableRows = subtableContainer.querySelectorAll('[data-is-subtable-row="true"]');
for (const subtableRow of subtableRows)
subtableRow.parentNode.removeChild(subtableRow);
const rows = subtableContainer.querySelectorAll('tbody tr');
for (const row of rows) {
row.classList.remove('isOpen');
const expandButton = row.querySelector('button[data-on-expand-row]');
if (expandButton) {
expandButton.classList.remove('active');
}
}
}
// Sample subtable data.
const subtableData = [
{
"orderId": 1001,
"name": "Wireless Mouse",
"quantity": 1,
"unitPrice": 25.99
},
{
"orderId": 1001,
"name": "Keyboard",
"quantity": 2,
"unitPrice": 45.00
},
{
"orderId": 1002,
"name": "Desk Lamp",
"quantity": 1,
"unitPrice": 34.50
},
{
"orderId": 1002,
"name": "Notebook",
"quantity": 3,
"unitPrice": 12.00
},
{
"orderId": 1002,
"name": "USB Hub",
"quantity": 2,
"unitPrice": 18.75
},
{
"orderId": 1004,
"name": "Office Chair",
"quantity": 1,
"unitPrice": 120.00,
}
];
// DOM element references.
const subtableContainer = ref.subtable.get(0);
const subtableTemplate = document.getElementById('subtable-template');// Get subtable template.
// Initialize the component and set up event listeners.
const subtable = initSubtable();
handleRowExpand();