Fixed #72 - Expandable Rows for DataTable

pull/104/head
cagataycivici 2019-10-21 09:21:15 +03:00
parent 0dc7524098
commit 3a54707947
6 changed files with 490 additions and 87 deletions

View File

@ -35,6 +35,9 @@ export declare class DataTable extends Vue {
resizableColumns?: boolean;
columnResizeMode?: string;
reorderableColumns?: boolean;
expandedRows?: any[];
expandedRowIcon?: string;
collapsedRowIcon?: string;
$emit(eventName: 'page', event: Event): this;
$emit(eventName: 'sort', event: Event): this;
$emit(eventName: 'filter', event: Event): this;
@ -43,6 +46,8 @@ export declare class DataTable extends Vue {
$emit(eventName: 'column-resize-end', event: Event): this;
$emit(eventName: 'column-reorder', event: Event): this;
$emit(eventName: 'row-reorder', event: Event): this;
$emit(eventName: 'row-expand', event: Event): this;
$emit(eventName: 'row-collapse', event: Event): this;
$slots: {
header: VNode[];
paginatorLeft: VNode[];

View File

@ -51,7 +51,8 @@
</thead>
<tbody class="p-datatable-tbody">
<template v-if="!empty">
<tr :class="getRowClass(rowData)" v-for="(rowData, index) of dataToRender" :key="getRowKey(rowData, index)"
<template v-for="(rowData, index) of dataToRender">
<tr :class="getRowClass(rowData)" :key="getRowKey(rowData, index)"
@click="onRowClick($event, rowData, index)" @touchend="onRowTouchEnd($event)" @keydown="onRowKeyDown($event, rowData, index)" :tabindex="selectionMode ? '0' : null"
@mousedown="onRowMouseDown($event)" @dragstart="onRowDragStart($event, index)" @dragover="onRowDragOver($event,index)" @dragleave="onRowDragLeave($event)" @dragend="onRowDragEnd($event)" @drop="onRowDrop($event)">
<td v-for="(col,i) of columns" :key="col.columnKey||col.field||i" :style="col.bodyStyle" :class="col.bodyClass">
@ -63,9 +64,21 @@
<template v-else-if="col.rowReorder">
<i :class="['p-datatable-reorderablerow-handle', col.rowReorderIcon]"></i>
</template>
<template v-else-if="col.expander">
<button class="p-row-toggler p-link" @click="toggleRow($event, rowData)">
<span :class="rowTogglerIcon(rowData)"></span>
</button>
</template>
<template v-else>{{resolveFieldData(rowData, col.field)}}</template>
</td>
</tr>
<tr class="p-datatable-row-expansion" v-if="expandedRows && isRowExpanded(rowData)" :key="getRowKey(rowData, index) + '_expansion'">
<td :colspan="columns.length">
<slot name="expansion" :data="rowData" :index="index">
</slot>
</td>
</tr>
</template>
</template>
<tr v-else class="p-datatable-emptymessage">
<td :colspan="columns.length">
@ -274,6 +287,18 @@ export default {
reorderableColumns: {
type: Boolean,
default: false
},
expandedRows: {
type: Array,
default: null
},
expandedRowIcon: {
type: String,
default: 'pi-chevron-down'
},
collapsedRowIcon: {
type: String,
default: 'pi-chevron-right'
}
},
data() {
@ -285,6 +310,7 @@ export default {
d_sortOrder: this.sortOrder,
d_multiSortMeta: this.multiSortMeta ? [...this.multiSortMeta] : [],
d_selectionKeys: null,
d_expandedRowKeys: null,
columnOrder: null
};
},
@ -322,6 +348,11 @@ export default {
if (this.dataKey) {
this.updateSelectionKeys(newValue);
}
},
expandedRows(newValue) {
if (this.dataKey) {
this.updateExpandedRowKeys(newValue);
}
}
},
mounted() {
@ -758,10 +789,13 @@ export default {
return false;
},
findIndexInSelection(rowData) {
return this.findIndex(rowData, this.selection);
},
findIndex(rowData, collection) {
let index = -1;
if (this.selection && this.selection.length) {
for (let i = 0; i < this.selection.length; i++) {
if (this.equals(rowData, this.selection[i])) {
if (collection && collection.length) {
for (let i = 0; i < collection.length; i++) {
if (this.equals(rowData, collection[i])) {
index = i;
break;
}
@ -781,9 +815,30 @@ export default {
this.d_selectionKeys[String(ObjectUtils.resolveFieldData(selection, this.dataKey))] = 1;
}
},
updateExpandedRowKeys(expandedRows) {
if (expandedRows && expandedRows.length) {
this.d_expandedRowKeys = {};
for (let data of expandedRows) {
this.d_expandedRowKeys[String(ObjectUtils.resolveFieldData(data, this.dataKey))] = 1;
}
}
else {
this.d_expandedRowKeys = null;
}
},
equals(data1, data2) {
return this.compareSelectionBy === 'equals' ? (data1 === data2) : ObjectUtils.equals(data1, data2, this.dataKey);
},
isRowExpanded(rowData) {
if (rowData && this.expandedRows) {
if (this.dataKey)
return this.d_expandedRowKeys ? this.d_expandedRowKeys[ObjectUtils.resolveFieldData(rowData, this.dataKey)] !== undefined : false;
else
return this.findIndex(rowData, this.expandedRows) > -1;
}
return false;
},
getRowKey(rowData, index) {
return this.dataKey ? ObjectUtils.resolveFieldData(rowData, this.dataKey): index;
},
@ -1172,7 +1227,38 @@ export default {
this.onRowDragLeave(event);
this.onRowDragEnd(event);
event.preventDefault();
},
toggleRow(event, rowData) {
let expanded;
let expandedRowIndex;
let _expandedRows = this.expandedRows ? [...this.expandedRows] : [];
if (this.dataKey) {
expanded = this.d_expandedRowKeys ? this.d_expandedRowKeys[ObjectUtils.resolveFieldData(rowData, this.dataKey)] !== undefined : false;
}
else {
expandedRowIndex = this.findIndex(rowData, this.expandedRows);
expanded = expandedRowIndex > -1;
}
if (expanded) {
if (expandedRowIndex == null) {
expandedRowIndex = this.findIndex(rowData, this.expandedRows);
}
_expandedRows.splice(expandedRowIndex, 1);
this.$emit('update:expandedRows', _expandedRows);
this.$emit('row-collapse', {originalEvent: event,data: rowData});
}
else {
_expandedRows.push(rowData);
this.$emit('update:expandedRows', _expandedRows);
this.$emit('row-expand', {originalEvent: event,data: rowData});
}
},
rowTogglerIcon(rowData) {
const icon = this.isRowExpanded(rowData) ? this.expandedRowIcon : this.collapsedRowIcon;
return ['p-row-toggler-icon pi pi-fw p-clickable', icon];
},
},
computed: {
containerClass() {

View File

@ -171,6 +171,11 @@ export default new Router({
name: 'datatablecolresize',
component: () => import('./views/datatable/DataTableColResizeDemo.vue')
},
{
path: '/datatable/rowexpand',
name: 'datatablerowexpand',
component: () => import('./views/datatable/DataTableRowExpandDemo.vue')
},
{
path: '/datatable/crud',
name: 'datatablecrud',

View File

@ -220,6 +220,12 @@ export default {
<td>null</td>
<td>Defines column based selection mode, options are "single" and "multiple".</td>
</tr>
<tr>
<td>expander</td>
<td>boolean</td>
<td>false</td>
<td>Displays an icon to toggle row expansion.</td>
</tr>
<tr>
<td>colspan</td>
<td>number</td>
@ -377,70 +383,6 @@ export default {
&lt;Column field="color" header="Color"&gt;&lt;/Column&gt;
&lt;/DataTable&gt;
</template>
</CodeHighlight>
<h3>Lazy Loading</h3>
<p>Lazy mode is handy to deal with large datasets, instead of loading the entire data, small chunks of data is loaded by invoking corresponding callbacks everytime paging, sorting and filtering happens. Sample belows imitates lazy paging by using an in memory list.
It is also important to assign the logical number of rows to totalRecords by doing a projection query for paginator configuration so that paginator displays the UI
assuming there are actually records of totalRecords size although in reality they aren't as in lazy mode, only the records that are displayed on the current page exist.</p>
<p>Lazy loading is implemented by handling pagination and sorting using <i>page</i> and <i>sort</i> events by making a remote query using the information
passed to the events such as first offset, number of rows and sort field for ordering. Filtering is handled differently as filter elements are defined using templates, use
the event you prefer on your form elements such as input, change, blur to make a remote call by passing the filters property to update the displayed data. Note that,
in lazy filtering, totalRecords should also be updated to align the data with the paginator.</p>
<p>Here is a sample paging implementation with in memory data, a more enhanced example with a backend is being worked on and will be available at a github repository.</p>
<CodeHighlight>
<template v-pre>
&lt;DataTable :value="cars" :lazy="true" :paginator="true" :rows="10"
:totalRecords="totalRecords" :loading="loading" @page="onPage($event)"&gt;
&lt;Column field="vin" header="Vin"&gt;&lt;/Column&gt;
&lt;Column field="year" header="Year"&gt;&lt;/Column&gt;
&lt;Column field="brand" header="Brand"&gt;&lt;/Column&gt;
&lt;Column field="color" header="Color"&gt;&lt;/Column&gt;
&lt;/DataTable&gt;
</template>
</CodeHighlight>
<CodeHighlight lang="javascript">
import CarService from '../../service/CarService';
export default {
data() {
return {
loading: false,
totalRecords: 0,
cars: null
}
},
datasource: null,
carService: null,
created() {
this.carService = new CarService();
},
mounted() {
this.loading = true;
setTimeout(() => {
this.carService.getCarsLarge().then(data => {
this.datasource = data;
this.totalRecords = data.length,
this.cars = this.datasource.slice(0, 10);
this.loading = false;
});
}, 1000);
},
methods: {
onPage(event) {
this.loading = true;
setTimeout(() => {
this.cars = this.datasource.slice(event.first, event.first + event.rows);
this.loading = false;
}, 1000);
}
}
}
</CodeHighlight>
<h3>Sorting</h3>
@ -512,6 +454,7 @@ data() {
}
}
</CodeHighlight>
<h3>Filtering</h3>
<p>Filtering is enabled by defining a filter template per column to populate the <i>filters</i> property of the DataTable. The <i>filters</i>
property should be an key-value object where keys are the field name and the value is the filter value. The filter template receives the column properties
@ -601,6 +544,143 @@ data() {
&lt;Column field="color" header="Color"&gt;&lt;/Column&gt;
&lt;/DataTable&gt;
</template>
</CodeHighlight>
<h3>Lazy Loading</h3>
<p>Lazy mode is handy to deal with large datasets, instead of loading the entire data, small chunks of data is loaded by invoking corresponding callbacks everytime paging, sorting and filtering happens. Sample belows imitates lazy paging by using an in memory list.
It is also important to assign the logical number of rows to totalRecords by doing a projection query for paginator configuration so that paginator displays the UI
assuming there are actually records of totalRecords size although in reality they aren't as in lazy mode, only the records that are displayed on the current page exist.</p>
<p>Lazy loading is implemented by handling pagination and sorting using <i>page</i> and <i>sort</i> events by making a remote query using the information
passed to the events such as first offset, number of rows and sort field for ordering. Filtering is handled differently as filter elements are defined using templates, use
the event you prefer on your form elements such as input, change, blur to make a remote call by passing the filters property to update the displayed data. Note that,
in lazy filtering, totalRecords should also be updated to align the data with the paginator.</p>
<p>Here is a sample paging implementation with in memory data, a more enhanced example with a backend is being worked on and will be available at a github repository.</p>
<CodeHighlight>
<template v-pre>
&lt;DataTable :value="cars" :lazy="true" :paginator="true" :rows="10"
:totalRecords="totalRecords" :loading="loading" @page="onPage($event)"&gt;
&lt;Column field="vin" header="Vin"&gt;&lt;/Column&gt;
&lt;Column field="year" header="Year"&gt;&lt;/Column&gt;
&lt;Column field="brand" header="Brand"&gt;&lt;/Column&gt;
&lt;Column field="color" header="Color"&gt;&lt;/Column&gt;
&lt;/DataTable&gt;
</template>
</CodeHighlight>
<CodeHighlight lang="javascript">
import CarService from '../../service/CarService';
export default {
data() {
return {
loading: false,
totalRecords: 0,
cars: null
}
},
datasource: null,
carService: null,
created() {
this.carService = new CarService();
},
mounted() {
this.loading = true;
setTimeout(() => {
this.carService.getCarsLarge().then(data => {
this.datasource = data;
this.totalRecords = data.length,
this.cars = this.datasource.slice(0, 10);
this.loading = false;
});
}, 1000);
},
methods: {
onPage(event) {
this.loading = true;
setTimeout(() => {
this.cars = this.datasource.slice(event.first, event.first + event.rows);
this.loading = false;
}, 1000);
}
}
}
</CodeHighlight>
<h3>Row Expansion</h3>
<p>Rows can be expanded to display additional content using the <i>expandedRows</i> property with the sync operator accompanied by a template named "expansion". <i>row-expand</i> and <i>row-collapse</i> are optional callbacks that are invoked when a row is expanded or toggled.</p>
<p>The <i>dataKey</i> property identifies a unique value of a row in the dataset, it is not mandatory in row expansion functionality however being able to define it increases the performance of the table signifantly.</p>
<CodeHighlight>
<template v-pre>
&lt;DataTable :value="cars" :expandedRows.sync="expandedRows" dataKey="vin"
@row-expand="onRowExpand" @row-collapse="onRowCollapse"&gt;
&lt;template #header&gt;
&lt;div class="table-header-container"&gt;
&lt;Button icon="pi pi-plus" label="Expand All" @click="expandAll" /&gt;
&lt;Button icon="pi pi-minus" label="Collapse All" @click="collapseAll" /&gt;
&lt;/div&gt;
&lt;/template&gt;
&lt;Column :expander="true" headerStyle="width: 3em" /&gt;
&lt;Column field="vin" header="Vin"&gt;&lt;/Column&gt;
&lt;Column field="year" header="Year"&gt;&lt;/Column&gt;
&lt;Column field="brand" header="Brand"&gt;&lt;/Column&gt;
&lt;Column field="color" header="Color"&gt;&lt;/Column&gt;
&lt;template #expansion="slotProps"&gt;
&lt;div class="car-details"&gt;
&lt;div&gt;
&lt;img :src="'demo/images/car/' + slotProps.data.brand + '.png'" :alt="slotProps.data.brand"/&gt;
&lt;div class="p-grid"&gt;
&lt;div class="p-col-12"&gt;Vin: &lt;b&gt;&#123;&#123;slotProps.data.vin&#125;&#125;&lt;/b&gt;&lt;/div&gt;
&lt;div class="p-col-12"&gt;Year: &lt;b&gt;&#123;&#123;slotProps.data.year&#125;&#125;&lt;/b&gt;&lt;/div&gt;
&lt;div class="p-col-12"&gt;Brand: &lt;b&gt;&#123;&#123;slotProps.data.brand&#125;&#125;&lt;/b&gt;&lt;/div&gt;
&lt;div class="p-col-12"&gt;Color: &lt;b&gt;&#123;&#123;slotProps.data.color&#125;&#125;&lt;/b&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;Button icon="pi pi-search"&gt;&lt;/Button&gt;
&lt;/div&gt;
&lt;/template&gt;
&lt;/DataTable&gt;
</template>
</CodeHighlight>
<CodeHighlight lang="javascript">
import CarService from '../../service/CarService';
export default {
data() {
return {
cars: null,
expandedRows: []
}
},
carService: null,
created() {
this.carService = new CarService();
},
mounted() {
this.carService.getCarsSmall().then(data => this.cars = data);
},
methods: {
onRowExpand(event) {
this.$toast.add({severity: 'info', summary: 'Row Expanded', detail: 'Vin: ' + event.data.vin, life: 3000});
},
onRowCollapse(event) {
this.$toast.add({severity: 'success', summary: 'Row Collapsed', detail: 'Vin: ' + event.data.vin, life: 3000});
},
expandAll() {
this.expandedRows = this.cars.filter(car => car.vin);
this.$toast.add({severity: 'success', summary: 'All Rows Expanded', life: 3000});
},
collapseAll() {
this.expandedRows = null;
this.$toast.add({severity: 'success', summary: 'All Rows Collapsed', life: 3000});
}
}
}
</CodeHighlight>
<h3>Column Resize</h3>
@ -1081,7 +1161,25 @@ export default {
<td>reorderableColumns</td>
<td>boolean</td>
<td>false</td>
<td>When enabled, columns can be reordered using drag and drop..</td>
<td>When enabled, columns can be reordered using drag and drop.</td>
</tr>
<tr>
<td>expandedRows</td>
<td>array</td>
<td>null</td>
<td>A collection of row data display as expanded.</td>
</tr>
<tr>
<td>expandedRowIcon</td>
<td>string</td>
<td>pi-chevron-down</td>
<td>Icon of the row toggler to display the row as expanded.</td>
</tr>
<tr>
<td>collapsedRowIcon</td>
<td>string</td>
<td>pi-chevron-right</td>
<td>Icon of the row toggler to display the row as collapsed.</td>
</tr>
</tbody>
</table>
@ -1162,6 +1260,18 @@ export default {
value: Reordered value</td>
<td>Callback to invoke when a row is reordered.</td>
</tr>
<tr>
<td>row-expand</td>
<td>event.originalEvent: Browser event<br />
event.data: Expanded row data.</td>
<td>Callback to invoke when a row is expanded.</td>
</tr>
<tr>
<td>row-collapse</td>
<td>event.originalEvent: Browser event<br />
event.data: Collapsed row data.</td>
<td>Callback to invoke when a row is collapsed.</td>
</tr>
</tbody>
</table>
</div>

View File

@ -0,0 +1,196 @@
<template>
<div>
<DataTableSubMenu />
<div class="content-section introduction">
<div class="feature-intro">
<h1>DataTabl - Row Expansion</h1>
<p>A row can be expanded to display additional content.</p>
</div>
</div>
<div class="content-section implementation">
<DataTable :value="cars" :expandedRows.sync="expandedRows" dataKey="vin"
@row-expand="onRowExpand" @row-collapse="onRowCollapse">
<template #header>
<div class="table-header-container">
<Button icon="pi pi-plus" label="Expand All" @click="expandAll" />
<Button icon="pi pi-minus" label="Collapse All" @click="collapseAll" />
</div>
</template>
<Column :expander="true" headerStyle="width: 3em" />
<Column field="vin" header="Vin"></Column>
<Column field="year" header="Year"></Column>
<Column field="brand" header="Brand"></Column>
<Column field="color" header="Color"></Column>
<template #expansion="slotProps">
<div class="car-details">
<div>
<img :src="'demo/images/car/' + slotProps.data.brand + '.png'" :alt="slotProps.data.brand"/>
<div class="p-grid">
<div class="p-col-12">Vin: <b>{{slotProps.data.vin}}</b></div>
<div class="p-col-12">Year: <b>{{slotProps.data.year}}</b></div>
<div class="p-col-12">Brand: <b>{{slotProps.data.brand}}</b></div>
<div class="p-col-12">Color: <b>{{slotProps.data.color}}</b></div>
</div>
</div>
<Button icon="pi pi-search"></Button>
</div>
</template>
</DataTable>
</div>
<div class="content-section documentation">
<TabView>
<TabPanel header="Source">
<CodeHighlight>
<template v-pre>
&lt;DataTable :value="cars" :expandedRows.sync="expandedRows" dataKey="vin"
@row-expand="onRowExpand" @row-collapse="onRowCollapse"&gt;
&lt;template #header&gt;
&lt;div class="table-header-container"&gt;
&lt;Button icon="pi pi-plus" label="Expand All" @click="expandAll" /&gt;
&lt;Button icon="pi pi-minus" label="Collapse All" @click="collapseAll" /&gt;
&lt;/div&gt;
&lt;/template&gt;
&lt;Column :expander="true" headerStyle="width: 3em" /&gt;
&lt;Column field="vin" header="Vin"&gt;&lt;/Column&gt;
&lt;Column field="year" header="Year"&gt;&lt;/Column&gt;
&lt;Column field="brand" header="Brand"&gt;&lt;/Column&gt;
&lt;Column field="color" header="Color"&gt;&lt;/Column&gt;
&lt;template #expansion="slotProps"&gt;
&lt;div class="car-details"&gt;
&lt;div&gt;
&lt;img :src="'demo/images/car/' + slotProps.data.brand + '.png'" :alt="slotProps.data.brand"/&gt;
&lt;div class="p-grid"&gt;
&lt;div class="p-col-12"&gt;Vin: &lt;b&gt;&#123;&#123;slotProps.data.vin&#125;&#125;&lt;/b&gt;&lt;/div&gt;
&lt;div class="p-col-12"&gt;Year: &lt;b&gt;&#123;&#123;slotProps.data.year&#125;&#125;&lt;/b&gt;&lt;/div&gt;
&lt;div class="p-col-12"&gt;Brand: &lt;b&gt;&#123;&#123;slotProps.data.brand&#125;&#125;&lt;/b&gt;&lt;/div&gt;
&lt;div class="p-col-12"&gt;Color: &lt;b&gt;&#123;&#123;slotProps.data.color&#125;&#125;&lt;/b&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;Button icon="pi pi-search"&gt;&lt;/Button&gt;
&lt;/div&gt;
&lt;/template&gt;
&lt;/DataTable&gt;
</template>
</CodeHighlight>
<CodeHighlight lang="javascript">
import CarService from '../../service/CarService';
export default {
data() {
return {
cars: null,
expandedRows: []
}
},
carService: null,
created() {
this.carService = new CarService();
},
mounted() {
this.carService.getCarsSmall().then(data => this.cars = data);
},
methods: {
onRowExpand(event) {
this.$toast.add({severity: 'info', summary: 'Row Expanded', detail: 'Vin: ' + event.data.vin, life: 3000});
},
onRowCollapse(event) {
this.$toast.add({severity: 'success', summary: 'Row Collapsed', detail: 'Vin: ' + event.data.vin, life: 3000});
},
expandAll() {
this.expandedRows = this.cars.filter(car => car.vin);
this.$toast.add({severity: 'success', summary: 'All Rows Expanded', life: 3000});
},
collapseAll() {
this.expandedRows = null;
this.$toast.add({severity: 'success', summary: 'All Rows Collapsed', life: 3000});
}
}
}
</CodeHighlight>
</TabPanel>
</TabView>
</div>
</div>
</template>
<script>
import CarService from '../../service/CarService';
import DataTableSubMenu from './DataTableSubMenu';
export default {
data() {
return {
cars: null,
expandedRows: []
}
},
carService: null,
created() {
this.carService = new CarService();
},
mounted() {
this.carService.getCarsSmall().then(data => this.cars = data);
},
methods: {
onRowExpand(event) {
this.$toast.add({severity: 'info', summary: 'Row Expanded', detail: 'Vin: ' + event.data.vin, life: 3000});
},
onRowCollapse(event) {
this.$toast.add({severity: 'success', summary: 'Row Collapsed', detail: 'Vin: ' + event.data.vin, life: 3000});
},
expandAll() {
this.expandedRows = this.cars.filter(car => car.vin);
this.$toast.add({severity: 'success', summary: 'All Rows Expanded', life: 3000});
},
collapseAll() {
this.expandedRows = null;
this.$toast.add({severity: 'success', summary: 'All Rows Collapsed', life: 3000});
}
},
components: {
'DataTableSubMenu': DataTableSubMenu
}
}
</script>
<style lang="scss" scoped>
.table-header-container {
text-align: left;
button {
min-width: 10em;
&:first-child {
margin-right: .5em;
}
}
}
.car-details {
display: flex;
justify-content: space-between;
align-items: center;
padding: 2em;
& > div {
display: flex;
align-items: center;
img {
margin-right: 14px;
}
}
}
@media (max-width: 1024px) {
.car-details {
img {
width: 75px;
}
}
}
</style>

View File

@ -3,16 +3,17 @@
<ul>
<li><router-link to="/datatable">&#9679; Documentation</router-link></li>
<li><router-link to="/datatable/templating">&#9679; Templating</router-link></li>
<li><router-link to="/datatable/colgroup">&#9679; ColGroup</router-link></li>
<li><router-link to="/datatable/paginator">&#9679; Paginator</router-link></li>
<li><router-link to="/datatable/sort">&#9679; Sort</router-link></li>
<li><router-link to="/datatable/filter">&#9679; Filter</router-link></li>
<li><router-link to="/datatable/selection">&#9679; Selection</router-link></li>
<li><router-link to="/datatable/lazy">&#9679; Lazy</router-link></li>
<li><router-link to="/datatable/colgroup">&#9679; ColGroup</router-link></li>
<li><router-link to="/datatable/rowexpand">&#9679; Expand</router-link></li>
<li><router-link to="/datatable/coltoggle">&#9679; ColToggle</router-link></li>
<li><router-link to="/datatable/responsive">&#9679; Responsive</router-link></li>
<li><router-link to="/datatable/reorder">&#9679; Reorder</router-link></li>
<li><router-link to="/datatable/colresize">&#9679; ColResize</router-link></li>
<li><router-link to="/datatable/reorder">&#9679; Reorder</router-link></li>
<li><router-link to="/datatable/responsive">&#9679; Responsive</router-link></li>
<li><router-link to="/datatable/export">&#9679; Export</router-link></li>
<li><router-link to="/datatable/crud">&#9679; Crud</router-link></li>
</ul>