<template>
<table :class="tableClass">
<thead>
<tr>
<datatable-header
v-for="(head_column, i) in normalizedColumns"
:key="i"
:column="head_column"
:settings="settings"
:direction="getSortDirectionForColumn(head_column)"
@change="setSortDirectionForColumn" />
</tr>
</thead>
<tbody>
<slot
v-for="(row, i) in displayedRows"
:row="row"
:columns="normalizedColumns">
<tr
:key="i"
:class="getRowClasses(row)">
<datatable-cell
v-for="(column, j) in normalizedColumns"
:key="j"
:column="column"
:row="row" />
</tr>
</slot>
<tr v-if="displayedRows.length == 0">
<td :colspan="normalizedColumns.length">
<slot name="no-results" />
</td>
</tr>
</tbody>
<tfoot v-if="$slots.footer || $scopedSlots.footer">
<slot
name="footer"
:rows="displayedRows" />
</tfoot>
</table>
</template>
<script>
import Column from './classes/column.js';
import { ensurePromise } from './utils';
/**
* @typedef {Object} DataFnParams
* @description Parameters passed to the `data` function, to handle by custom logic.
* @property {string | stringp[]} filter: The string(s) used to filter entries.
* @property {string | null} sortBy: The name of the field we are sorting on.
* @property {'asc' | 'desc' | null} sortDir: The direction of the sort.
* @property {number} perPage: The number of items per page.
* @property {number} page: The current page index.
* @tutorial ajax-data
*/
/**
* The main component of the module, used to display a datatable.
*
* @module datatable
*
* @vue-prop {string} [name] - The name of the datatable. It should be unique per page.
* @vue-prop {ColumnDef[]} columns - List of columns definitions displayed by this datatable.
* @vue-prop {Array.<*>|Function} data - The list of items to display, or a getter function.
* @vue-prop {string} [filter] - Value to match in rows for display filtering.
* @vue-prop {(string | Array.<string> | Function)} [rowClasses] - Class(es) or getter function to get row classes.
*
* @vue-data {Column | null} sortBy - Column used for data sorting.
* @vue-data {'asc' | 'desc' | null} sortDir - Direction of the sort. A null value is equivalent to 'asc'.
* @vue-data {number} totalRows - Total number of rows contained by this data table.
* @vue-data {number} page - Current page index.
* @vue-data {number | null} perPage - Maximum number of rows displayed per page.
* @vue-data {Row[]} displayedRows - Array of rows displayed by the table.
* @vue-data {datatable-pager[]} pagers - Array of pagers that are linked to this table.
*
* @vue-computed {Array.<*>} rows - Array of rows currently managed by the datatable.
* @vue-computed {Settings} settings - Reference to the {@link Settings} object linked to this datatable instance.
* @vue-computed {Handler} handler - Reference to the {@link Handler} object linked to this datatable instance.
* @vue-computed {Column[]} normalizedColumns - Array of columns definitions casted as {@link Column} objects.
* @vue-computed {string} tableClass - Base CSS class to apply to the `<table>` element.
*/
export default {
props: {
name: {
type: String,
default: 'default',
},
waitForPager: {
type: Boolean,
default: false,
},
columns: {
type: Array,
required: true,
},
data: {
type: null,
required: true,
},
filter: {
type: [ String, Array ],
default: null,
},
rowClasses: {
type: [ String, Array, Function ],
default: null,
},
},
data: () => ( {
sortBy: null,
sortDir: null,
totalRows: 0,
page: 1,
perPage: null,
displayedRows: [],
pagers: [],
} ),
computed: {
settings(){
return this.$options.settings;
},
handler(){
return this.$options.handler;
},
normalizedColumns(){
return this.columns.map( column => new Column( column ) );
},
tableClass(){
return this.settings.get( 'table.class' );
},
},
created(){
this.$datatables[this.name] = this;
this.$root.$emit( 'table.ready', this.name );
this.$watch( 'data', this.processRows, {deep: true, immediate: false} );
this.$watch( 'columns', this.processRows, {deep: true, immediate: false} );
// Defer to next tick, so a pager component created just after have the time to link itself with this table before start watching.
this.$nextTick( () => {
if ( this.waitForPager && this.pagers.length === 0 ){
this.$on( 'table.pager-bound', () => this.initWatchCriterions() );
} else {
this.initWatchCriterions();
}
} );
},
methods: {
/**
* Get the sort direction for a specific column.
*
* @param {Column} columnDefinition - The column to check sorting direction for.
* @returns {'asc' | 'desc' | null} The sort direction for the specified column.
*/
getSortDirectionForColumn( columnDefinition ){
if ( this.sortBy !== columnDefinition ){
return null;
}
return this.sortDir;
},
/**
* Defines the sort direction for a specific column.
*
* @param {'asc' | 'desc' | null} direction - The direction of the sort.
* @param {Column} column - The column to check sorting direction for.
* @returns {void} Nothing.
*/
setSortDirectionForColumn( direction, column ){
this.sortBy = column;
this.sortDir = direction;
},
/**
* Using data (or its return value if it is a function), filter, sort, paginate & display rows in the table.
*
* @returns {Promise<void>} Nothing.
* @see DataFnParams Parameters provided to the `data` function.
* @tutorial ajax-data
*/
processRows(){
if ( typeof this.data === 'function' ){
const params = {
filter: this.filter,
sortBy: this.sortBy ? this.sortBy.field : null,
sortDir: this.sortDir,
perPage: this.perPage,
page: this.page,
};
return ensurePromise( this.data( params ) )
.then( tableContent => ensurePromise( this.setTableContent( tableContent ) ) );
}
const outObj = { source: this.data };
return ensurePromise( this.handler.filterHandler( this.data, this.filter, this.normalizedColumns ) )
.then( filteredData => ensurePromise( this.handler.sortHandler( outObj.filtered = filteredData, this.sortBy, this.sortDir ) ) )
.then( sortedData => ensurePromise( this.handler.paginateHandler( outObj.sorted = sortedData, this.perPage, this.page ) ) )
.then( pagedData => ensurePromise( this.handler.displayHandler( Object.assign( {paged: pagedData}, outObj ) ) ) )
.then( tableContent => ensurePromise( this.setTableContent( tableContent ) ) );
},
setTableContent( { rows, totalRowCount } = { rows: undefined, totalRowCount: undefined } ){
this.setRows( rows );
this.setTotalRowCount( totalRowCount );
},
/**
* Set the displayed rows.
*
* @param {Row[]} rows - The rows to display.
* @returns {void} Nothing.
*/
setRows( rows ){
if ( typeof rows !== 'undefined' && rows !== null ){
this.displayedRows = rows;
}
},
/**
* Set the displayed rows count.
*
* @param {number} value - The number of displayed rows.
* @returns {void} Nothing.
*/
setTotalRowCount( value ){
if ( typeof value !== 'undefined' && value !== null ){
this.totalRows = value;
}
},
/**
* Get the classes to add on the row
*
* @param {Row} row - The row to get classes for.
* @returns {string} The classes string to set on the row.
*/
getRowClasses( row ){
let rowClasses = this.rowClasses;
if ( rowClasses === null ){
rowClasses = this.settings.get( 'table.row.class' );
}
if ( typeof rowClasses === 'function' ){
return rowClasses( row );
}
return rowClasses;
},
/**
* Starts the watching of following properties: `filter`, `perPage`, `page`, `sortBy`, `sortDir`.
* When a change is detected, the component runs {@link datatable#processRows}.
* Because the watch is immediate, {@link datatable#processRows} is run immediately when this method is called.
*
* @see datatable#processRows
* @see https://vuejs.org/v2/api/#vm-watch
* @returns {void} Nothing.
*/
initWatchCriterions(){
for ( const prop of [ 'filter', 'perPage', 'page', 'sortBy', 'sortDir' ] ){
this.$watch( prop, this.processRows, { immediate: false, deep: true } );
}
this.processRows();
},
},
handler: null,
settings: null,
};
</script>