Implemented TableState

pull/104/head
cagataycivici 2019-10-22 10:36:04 +03:00
parent 5395bc19de
commit 360ad46404
4 changed files with 504 additions and 6 deletions

View File

@ -295,6 +295,14 @@ export default {
expandedRowGroups: {
type: Array,
default: null
},
stateStorage: {
type: String,
default: 'session'
},
stateKey: {
type: String,
default: null
}
},
data() {
@ -307,7 +315,7 @@ export default {
d_multiSortMeta: this.multiSortMeta ? [...this.multiSortMeta] : [],
d_selectionKeys: null,
d_expandedRowKeys: null,
columnOrder: null
d_columnOrder: null
};
},
rowTouched: false,
@ -324,6 +332,9 @@ export default {
draggedRowIndex: null,
droppedRowIndex: null,
rowDragging: null,
columnWidthsState: null,
tableWidthState: null,
columnWidthsRestored: false,
watch: {
first(newValue) {
this.d_first = newValue;
@ -351,6 +362,11 @@ export default {
}
}
},
beforeMount() {
if (this.isStateful()) {
this.restoreState();
}
},
mounted() {
this.allChildren = this.$children;
@ -361,12 +377,22 @@ export default {
columnOrder.push(child.columnKey||child.field);
}
}
this.columnOrder = columnOrder;
this.d_columnOrder = columnOrder;
}
},
beforeDestroy() {
this.unbindColumnResizeEvents();
},
updated() {
if (this.isStateful()) {
this.saveState();
if (this.resizableColumns && !this.columnWidthsRestored) {
this.restoreColumnWidths();
this.columnWidthsRestored = true;
}
}
},
methods: {
resolveFieldData(rowData, field) {
return ObjectUtils.resolveFieldData(rowData, field);
@ -1015,6 +1041,10 @@ export default {
DomHandler.removeClass(this.$el, 'p-unselectable-text');
this.unbindColumnResizeEvents();
if (this.isStateful()) {
this.saveState();
}
},
bindColumnResizeEvents() {
if (!this.documentColumnResizeListener) {
@ -1114,7 +1144,7 @@ export default {
}
if (allowDrop) {
ObjectUtils.reorderArray(this.columnOrder, dragIndex, dropIndex);
ObjectUtils.reorderArray(this.d_columnOrder, dragIndex, dropIndex);
this.$emit('column-reorder', {
originalEvent: event,
@ -1368,6 +1398,165 @@ export default {
else {
return null;
}
},
isStateful() {
return this.stateKey != null;
},
getStorage() {
switch(this.stateStorage) {
case 'local':
return window.localStorage;
case 'session':
return window.sessionStorage;
default:
throw new Error(this.stateStorage + ' is not a valid value for the state storage, supported values are "local" and "session".');
}
},
saveState() {
const storage = this.getStorage();
let state = {};
if (this.paginator) {
state.first = this.d_first;
state.rows = this.d_rows;
}
if (this.d_sortField) {
state.sortField = this.d_sortField;
state.sortOrder = this.d_sortOrder;
}
if (this.d_multiSortMeta) {
state.multiSortMeta = this.d_multiSortMeta;
}
if (this.hasFilters) {
state.filters = this.filters;
}
if (this.resizableColumns) {
this.saveColumnWidths(state);
}
if (this.reorderableColumns) {
state.columnOrder = this.d_columnOrder;
}
if (this.expandedRows) {
state.expandedRows = this.expandedRows;
state.expandedRowKeys = this.d_expandedRowKeys;
}
if (this.expandedRowGroups) {
state.expandedRowGroups = this.expandedRowGroups;
}
if (this.selection) {
state.selection = this.selection;
state.selectionKeys = this.d_selectionKeys;
}
if (Object.keys(state).length) {
storage.setItem(this.stateKey, JSON.stringify(state));
}
},
restoreState() {
const storage = this.getStorage();
const stateString = storage.getItem(this.stateKey);
if (stateString) {
let restoredState = JSON.parse(stateString);
if (this.paginator) {
this.d_first = restoredState.first;
this.d_rows = restoredState.rows;
}
if (restoredState.sortField) {
this.d_sortField = restoredState.sortField;
this.d_sortOrder = restoredState.sortOrder;
}
if (restoredState.multiSortMeta) {
this.d_multiSortMeta = restoredState.multiSortMeta;
}
if (restoredState.filters) {
this.$emit('update:filters', restoredState.filters);
}
if (this.resizableColumns) {
this.columnWidthsState = restoredState.columnWidths;
this.tableWidthState = restoredState.tableWidth;
}
if (this.reorderableColumns) {
this.d_columnOrder = restoredState.columnOrder;
}
if (restoredState.expandedRows) {
this.d_expandedRowKeys = restoredState.expandedRowKeys;
this.$emit('update:expandedRows', restoredState.expandedRows);
}
if (restoredState.expandedRowGroups) {
this.$emit('update:expandedRowGroups', restoredState.expandedRowGroups);
}
if (this.selection) {
this.selection = restoredState.selection;
this.d_selectionKeys = restoredState.d_selectionKeys;
this.$emit('update:selection', this.selection);
}
}
},
saveColumnWidths(state) {
let widths = [];
let headers = DomHandler.find(this.$el, '.p-datatable-thead > tr > th');
headers.forEach(header => widths.push(DomHandler.getOuterWidth(header)));
state.columnWidths = widths.join(',');
if (this.columnResizeMode === 'expand') {
/*TODO: Keep state when scrollable tables are implemented
state.tableWidth = this.scrollable ? DomHandler.findSingle(this.container, '.p-datatable-scrollable-header-table').style.width :
DomHandler.getOuterWidth(this.table) + 'px';*/
state.tableWidth = DomHandler.getOuterWidth(this.$refs.table) + 'px'
}
},
restoreColumnWidths() {
//TODO: Restore state when scrollable tables are implemented
if (this.columnWidthsState) {
let widths = this.columnWidthsState.split(',');
if (this.columnResizeMode === 'expand' && this.tableWidthState) {
this.$refs.table.style.width = this.tableWidthState;
this.$el.style.width = this.tableWidthState;
/*if (this.scrollable) {
this.setScrollableItemsWidthOnExpandResize(null, this.tableWidthState, 0);
}
else {
this.$refs.table.style.width = this.tableWidthState;
this.$el.style.width = this.tableWidthState;
}*/
}
/*
if (this.props.scrollable) {
let headerCols = DomHandler.find(this.container, '.p-datatable-scrollable-header-table > colgroup > col');
let bodyCols = DomHandler.find(this.container, '.p-datatable-scrollable-body-table > colgroup > col');
headerCols.map((col, index) => col.style.width = widths[index] + 'px');
bodyCols.map((col, index) => col.style.width = widths[index] + 'px');
}
else {
let headers = DomHandler.find(this.table, '.p-datatable-thead > tr > th');
headers.map((header, index) => header.style.width = widths[index] + 'px');
}*/
let headers = DomHandler.find(this.$refs.table, '.p-datatable-thead > tr > th');
headers.forEach((header, index) => header.style.width = widths[index] + 'px');
}
}
},
computed: {
@ -1387,9 +1576,9 @@ export default {
if (this.allChildren) {
columns = this.allChildren.filter(child => child.$options._propKeys.indexOf('columnKey') !== -1);
if (this.reorderableColumns && this.columnOrder) {
if (this.reorderableColumns && this.d_columnOrder) {
let orderedColumns = [];
for (let columnKey of this.columnOrder) {
for (let columnKey of this.d_columnOrder) {
let column = this.findColumnByKey(columns, columnKey);
if (column) {
orderedColumns.push(column);

View File

@ -186,6 +186,11 @@ export default new Router({
name: 'datatablerowexpand',
component: () => import('./views/datatable/DataTableRowExpandDemo.vue')
},
{
path: '/datatable/state',
name: 'datatablestate',
component: () => import('./views/datatable/DataTableStateDemo.vue')
},
{
path: '/datatable/crud',
name: 'datatablecrud',

View File

@ -0,0 +1,303 @@
<template>
<div>
<DataTableSubMenu />
<div class="content-section introduction">
<div class="feature-intro">
<h1>DataTable - State</h1>
<p>Stateful table allows keeping the state such as page, sort and filtering either at local storage or session storage so that when the page is visited again, table would render the data using its last settings.</p>
</div>
</div>
<div class="content-section implementation">
<h3>Session Storage</h3>
<DataTable :value="cars" :paginator="true" :rows="10" :filters.sync="filters" :resizableColumns="true"
stateStorage="session" stateKey="dt-state-demo-session">
<template #header>
<div style="text-align: right">
<i class="pi pi-search" style="margin: 4px 4px 0px 0px;"></i>
<InputText v-model="filters['global']" placeholder="Global Search" size="50" />
</div>
</template>
<Column field="vin" header="Vin" filterMatchMode="startsWith" :sortable="true">
<template #filter>
<InputText type="text" v-model="filters['vin']" class="p-column-filter" placeholder="Starts with" />
</template>
</Column>
<Column field="year" header="Year" filterMatchMode="contains" :sortable="true">
<template #filter>
<InputText type="text" v-model="filters['year']" class="p-column-filter" placeholder="Contains" />
</template>
</Column>
<Column field="brand" header="Brand" filterMatchMode="equals" :sortable="true">
<template #filter>
<Dropdown v-model="filters['brand']" :options="brands" optionLabel="brand" optionValue="value" placeholder="Select a Brand" class="p-column-filter" :showClear="true">
<template #option="slotProps">
<div class="p-clearfix p-dropdown-car-option">
<img :alt="slotProps.option.brand" :src="'demo/images/car/' + slotProps.option.brand + '.png'" />
<span>{{slotProps.option.brand}}</span>
</div>
</template>
</Dropdown>
</template>
</Column>
<Column field="color" header="Color" filterMatchMode="in" :sortable="true">
<template #filter>
<MultiSelect v-model="filters['color']" :options="colors" optionLabel="name" optionValue="value" placeholder="Select a Color" />
</template>
</Column>
<template #empty>
No records found.
</template>
</DataTable>
<h3>Local Storage</h3>
<DataTable :value="cars" :paginator="true" :rows="10" :filters.sync="filters" :resizableColumns="true"
stateStorage="local" stateKey="dt-state-demo-local">
<template #header>
<div style="text-align: right">
<i class="pi pi-search" style="margin: 4px 4px 0px 0px;"></i>
<InputText v-model="filters['global']" placeholder="Global Search" size="50" />
</div>
</template>
<Column field="vin" header="Vin" filterMatchMode="startsWith" :sortable="true">
<template #filter>
<InputText type="text" v-model="filters['vin']" class="p-column-filter" placeholder="Starts with" />
</template>
</Column>
<Column field="year" header="Year" filterMatchMode="contains" :sortable="true">
<template #filter>
<InputText type="text" v-model="filters['year']" class="p-column-filter" placeholder="Contains" />
</template>
</Column>
<Column field="brand" header="Brand" filterMatchMode="equals" :sortable="true">
<template #filter>
<Dropdown v-model="filters['brand']" :options="brands" optionLabel="brand" optionValue="value" placeholder="Select a Brand" class="p-column-filter" :showClear="true">
<template #option="slotProps">
<div class="p-clearfix p-dropdown-car-option">
<img :alt="slotProps.option.brand" :src="'demo/images/car/' + slotProps.option.brand + '.png'" />
<span>{{slotProps.option.brand}}</span>
</div>
</template>
</Dropdown>
</template>
</Column>
<Column field="color" header="Color" filterMatchMode="in" :sortable="true">
<template #filter>
<MultiSelect v-model="filters['color']" :options="colors" optionLabel="name" optionValue="value" placeholder="Select a Color" />
</template>
</Column>
<template #empty>
No records found.
</template>
</DataTable>
</div>
<div class="content-section documentation">
<TabView>
<TabPanel header="Source">
<CodeHighlight>
<template v-pre>
&lt;h3&gt;Session Storage&lt;/h3&gt;
&lt;DataTable :value="cars" :paginator="true" :rows="10" :filters.sync="filters" :resizableColumns="true"
stateStorage="session" stateKey="dt-state-demo-session"&gt;
&lt;template #header&gt;
&lt;div style="text-align: right"&gt;
&lt;i class="pi pi-search" style="margin: 4px 4px 0px 0px;"&gt;&lt;/i&gt;
&lt;InputText v-model="filters['global']" placeholder="Global Search" size="50" /&gt;
&lt;/div&gt;
&lt;/template&gt;
&lt;Column field="vin" header="Vin" filterMatchMode="startsWith" :sortable="true"&gt;
&lt;template #filter&gt;
&lt;InputText type="text" v-model="filters['vin']" class="p-column-filter" placeholder="Starts with" /&gt;
&lt;/template&gt;
&lt;/Column&gt;
&lt;Column field="year" header="Year" filterMatchMode="contains" :sortable="true"&gt;
&lt;template #filter&gt;
&lt;InputText type="text" v-model="filters['year']" class="p-column-filter" placeholder="Contains" /&gt;
&lt;/template&gt;
&lt;/Column&gt;
&lt;Column field="brand" header="Brand" filterMatchMode="equals" :sortable="true"&gt;
&lt;template #filter&gt;
&lt;Dropdown v-model="filters['brand']" :options="brands" optionLabel="brand" optionValue="value" placeholder="Select a Brand" class="p-column-filter" :showClear="true"&gt;
&lt;template #option="slotProps"&gt;
&lt;div class="p-clearfix p-dropdown-car-option"&gt;
&lt;img :alt="slotProps.option.brand" :src="'demo/images/car/' + slotProps.option.brand + '.png'" /&gt;
&lt;span&gt;&#123;&#123;slotProps.option.brand&#125;&#125;&lt;/span&gt;
&lt;/div&gt;
&lt;/template&gt;
&lt;/Dropdown&gt;
&lt;/template&gt;
&lt;/Column&gt;
&lt;Column field="color" header="Color" filterMatchMode="in" :sortable="true"&gt;
&lt;template #filter&gt;
&lt;MultiSelect v-model="filters['color']" :options="colors" optionLabel="name" optionValue="value" placeholder="Select a Color" /&gt;
&lt;/template&gt;
&lt;/Column&gt;
&lt;template #empty&gt;
No records found.
&lt;/template&gt;
&lt;/DataTable&gt;
&lt;h3&gt;Local Storage&lt;/h3&gt;
&lt;DataTable :value="cars" :paginator="true" :rows="10" :filters.sync="filters" :resizableColumns="true"
stateStorage="local" stateKey="dt-state-demo-local"&gt;
&lt;template #header&gt;
&lt;div style="text-align: right"&gt;
&lt;i class="pi pi-search" style="margin: 4px 4px 0px 0px;"&gt;&lt;/i&gt;
&lt;InputText v-model="filters['global']" placeholder="Global Search" size="50" /&gt;
&lt;/div&gt;
&lt;/template&gt;
&lt;Column field="vin" header="Vin" filterMatchMode="startsWith" :sortable="true"&gt;
&lt;template #filter&gt;
&lt;InputText type="text" v-model="filters['vin']" class="p-column-filter" placeholder="Starts with" /&gt;
&lt;/template&gt;
&lt;/Column&gt;
&lt;Column field="year" header="Year" filterMatchMode="contains" :sortable="true"&gt;
&lt;template #filter&gt;
&lt;InputText type="text" v-model="filters['year']" class="p-column-filter" placeholder="Contains" /&gt;
&lt;/template&gt;
&lt;/Column&gt;
&lt;Column field="brand" header="Brand" filterMatchMode="equals" :sortable="true"&gt;
&lt;template #filter&gt;
&lt;Dropdown v-model="filters['brand']" :options="brands" optionLabel="brand" optionValue="value" placeholder="Select a Brand" class="p-column-filter" :showClear="true"&gt;
&lt;template #option="slotProps"&gt;
&lt;div class="p-clearfix p-dropdown-car-option"&gt;
&lt;img :alt="slotProps.option.brand" :src="'demo/images/car/' + slotProps.option.brand + '.png'" /&gt;
&lt;span&gt;&#123;&#123;slotProps.option.brand&#125;&#125;&lt;/span&gt;
&lt;/div&gt;
&lt;/template&gt;
&lt;/Dropdown&gt;
&lt;/template&gt;
&lt;/Column&gt;
&lt;Column field="color" header="Color" filterMatchMode="in" :sortable="true"&gt;
&lt;template #filter&gt;
&lt;MultiSelect v-model="filters['color']" :options="colors" optionLabel="name" optionValue="value" placeholder="Select a Color" /&gt;
&lt;/template&gt;
&lt;/Column&gt;
&lt;template #empty&gt;
No records found.
&lt;/template&gt;
&lt;/DataTable&gt;
</template>
</CodeHighlight>
<CodeHighlight lang="javascript">
import CarService from '../../service/CarService';
export default {
data() {
return {
filters: {},
brands: [
{brand: 'Audi', value: 'Audi'},
{brand: 'BMW', value: 'BMW'},
{brand: 'Fiat', value: 'Fiat'},
{brand: 'Honda', value: 'Honda'},
{brand: 'Jaguar', value: 'Jaguar'},
{brand: 'Mercedes', value: 'Mercedes'},
{brand: 'Renault', value: 'Renault'},
{brand: 'Volkswagen', value: 'Volkswagen'},
{brand: 'Volvo', value: 'Volvo'}
],
colors: [
{name: 'White', value: 'White'},
{name: 'Green', value: 'Green'},
{name: 'Silver', value: 'Silver'},
{name: 'Black', value: 'Black'},
{name: 'Red', value: 'Red'},
{name: 'Maroon', value: 'Maroon'},
{name: 'Brown', value: 'Brown'},
{name: 'Orange', value: 'Orange'},
{name: 'Blue', value: 'Blue'}
],
cars: null
}
},
carService: null,
created() {
this.carService = new CarService();
},
mounted() {
this.carService.getCarsMedium().then(data => this.cars = data);
}
}
</CodeHighlight>
</TabPanel>
</TabView>
</div>
</div>
</template>
<script>
import CarService from '../../service/CarService';
import DataTableSubMenu from './DataTableSubMenu';
export default {
data() {
return {
filters: {},
brands: [
{brand: 'Audi', value: 'Audi'},
{brand: 'BMW', value: 'BMW'},
{brand: 'Fiat', value: 'Fiat'},
{brand: 'Honda', value: 'Honda'},
{brand: 'Jaguar', value: 'Jaguar'},
{brand: 'Mercedes', value: 'Mercedes'},
{brand: 'Renault', value: 'Renault'},
{brand: 'Volkswagen', value: 'Volkswagen'},
{brand: 'Volvo', value: 'Volvo'}
],
colors: [
{name: 'White', value: 'White'},
{name: 'Green', value: 'Green'},
{name: 'Silver', value: 'Silver'},
{name: 'Black', value: 'Black'},
{name: 'Red', value: 'Red'},
{name: 'Maroon', value: 'Maroon'},
{name: 'Brown', value: 'Brown'},
{name: 'Orange', value: 'Orange'},
{name: 'Blue', value: 'Blue'}
],
cars: null
}
},
carService: null,
created() {
this.carService = new CarService();
},
mounted() {
this.carService.getCarsMedium().then(data => this.cars = data);
},
components: {
'DataTableSubMenu': DataTableSubMenu
}
}
</script>
<style lang="scss" scoped>
/deep/ .p-dropdown-label {
text-align: left;
}
.p-column-filter {
margin-top: .5em;
}
.p-multiselect {
width: 100%;
margin-top: .5em;
}
.p-dropdown-car-option {
img {
vertical-align: middle;
margin-right: .5em;
width: 24px;
}
span {
float: right;
margin-top: .125em;
}
}
</style>

View File

@ -16,6 +16,7 @@
<li><router-link to="/datatable/rowgroup">&#9679; RowGroup</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/state">&#9679; State</router-link></li>
<li><router-link to="/datatable/crud">&#9679; Crud</router-link></li>
</ul>
</div>