Created
March 27, 2019 05:46
-
-
Save dimchanske/b85148970a85fd55ac2dfe4e38107dfc to your computer and use it in GitHub Desktop.
VueJS Table Component
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<template> | |
<div> | |
<div class="table-header"> | |
<!-- Global buttons --> | |
<div class="global-buttons" v-if="globalButtons && globalButtons.length"> | |
<button | |
class="uk-button uk-button-primary" | |
v-for="(button, index) in globalButtons" | |
v-bind:key="index" | |
@click="typeof button.clickHandler === 'function' && button.clickHandler()" | |
> | |
{{ button.text }} | |
</button> | |
</div> | |
<!-- Search input --> | |
<search-component v-if="withSearch" @search="onSearch" /> | |
</div> | |
<table data-uk-table class="uk-table uk-table-divider uk-table-hover uk-table-small uk-table-middle"> | |
<thead> | |
<tr> | |
<th | |
v-for="(column, index) in columns" | |
v-bind:style="{ width: column.width }" | |
v-bind:key="index" | |
v-bind:class="{ sortable: isSortableColumn(column) }" | |
@click="changeSort(column)" | |
> | |
<!-- Column name --> | |
{{ column.name }} | |
<!-- Sorting triangle icon --> | |
<span v-if="isSortedByColumn(column)" v-bind:data-uk-icon="sortingIcon" class="sort-icon"></span> | |
</th> | |
<!-- Line buttons --> | |
<th v-if="rowButtons && rowButtons.length" class="uk-table-shrink"> | |
{{ 'table.actions_header' | translate }} | |
</th> | |
</tr> | |
</thead> | |
<!-- Data loaded --> | |
<tbody v-if="rows"> | |
<template v-if="rows.length"> | |
<tr | |
v-for="(row, rowIndex) in rows" | |
v-bind:key="rowIndex" | |
v-bind:class="row.meta && row.meta.class" | |
@click="onClickRow(row)" | |
> | |
<!-- Table data --> | |
<td | |
v-for="(column, columnIndex) in columns" | |
v-bind:key="columnIndex" | |
v-html="(column.transform && column.transform(row)) || row[column.property]" | |
></td> | |
<!-- Line buttons --> | |
<td v-if="rowButtons && rowButtons.length" class="uk-flex uk-flex-right"> | |
<span | |
v-for="(rowButton, rowButtonIndex) in filteredRowButtons[rowIndex]" | |
v-bind:key="rowButtonIndex" | |
v-bind:title="rowButton.title" | |
v-bind:uk-icon="`icon: ${rowButton.icon}`" | |
class="row-button" | |
@click.stop="onClickRowButton(row, rowButton)" | |
></span> | |
</td> | |
</tr> | |
</template> | |
<!-- If empty data --> | |
<tr v-else> | |
<td v-bind:colspan="columnsCount" class="uk-text-center"> | |
No records | |
</td> | |
</tr> | |
</tbody> | |
<!-- If loading --> | |
<tbody v-if="!rows"> | |
<tr class="loading-row"> | |
<td v-bind:colspan="columnsCount"> | |
<div class="loader"></div> | |
</td> | |
</tr> | |
</tbody> | |
</table> | |
<!-- Pagination for table --> | |
<pagination-component | |
v-bind:count="currentCount" | |
v-bind:limit="limit" | |
v-bind:starting-page="startingPage" | |
@change-page="onChangePage" | |
></pagination-component> | |
</div> | |
</template> | |
<script lang="ts"> | |
import { Component, Prop, Vue } from 'vue-property-decorator'; | |
import PaginationComponent from '@/components/common/PaginationComponent.vue'; | |
import SearchComponent from '@/components/common/SearchComponent.vue'; | |
import ButtonComponent from '@/components/common/ButtonComponent.vue'; | |
/** | |
* Table types | |
*/ | |
export type SortingTuple = [string, 'ASC' | 'DESC']; | |
export interface TableColumn { | |
property: string; | |
name: string; | |
width?: string; | |
disableSort?: boolean; | |
transform?: (row: any) => any; | |
} | |
export interface RowButton { | |
title: string; | |
icon: string; | |
clickHandler: (row: object) => void; | |
condition?: (row: object) => boolean; | |
} | |
export interface GlobalButton { | |
text: string; | |
clickHandler: () => void; | |
} | |
/** | |
* Table component. | |
* | |
* Events: | |
* 1) change-sort - emits changed sort tuple | |
* 2) change-page - emits changed page number | |
* 3) search - emits changed search value if withSearch flag is set | |
*/ | |
@Component({ | |
components: { | |
PaginationComponent, | |
SearchComponent, | |
ButtonComponent, | |
}, | |
}) | |
export default class TableComponent extends Vue { | |
/** | |
* Table columns data | |
*/ | |
@Prop() | |
public columns!: any; | |
/** | |
* Table rows data | |
*/ | |
@Prop() | |
public rows!: object[]; | |
/** | |
* Limit rows for pagination | |
*/ | |
@Prop({ default: 25 }) | |
public limit!: number; | |
/** | |
* Total count for pagination | |
*/ | |
@Prop() | |
public count!: number; | |
/** | |
* Sorting info array | |
*/ | |
@Prop() | |
public sort!: SortingTuple; | |
/** | |
* Initial active page for pagination | |
*/ | |
@Prop({ default: 1 }) | |
public startingPage!: number; | |
/** | |
* Array of buttons for each row of the table | |
*/ | |
@Prop() | |
public rowButtons!: RowButton[]; | |
/** | |
* Array of global buttons for table | |
*/ | |
@Prop() | |
public globalButtons!: GlobalButton[]; | |
/** | |
* Flag for rendering search component in table header | |
*/ | |
@Prop({ default: false }) | |
public withSearch!: boolean; | |
/** | |
* Sorting settings array | |
*/ | |
public currentSort: SortingTuple = this.sort || []; | |
/** | |
* Filtered set of row buttons of each row based on conditions provide with buttons | |
*/ | |
public get filteredRowButtons(): RowButton[][] { | |
return this.rows.map((row: any) => { | |
return this.rowButtons.filter((rowButton: RowButton) => !rowButton.condition || rowButton.condition(row)); | |
}); | |
} | |
/** | |
* Data count for pagination: either from parent or count manually | |
*/ | |
public get currentCount(): number { | |
return this.count || (this.rows && this.rows.length); | |
} | |
/** | |
* Return sort icon name for column or false. | |
*/ | |
public get sortingIcon(): string { | |
return this.currentSort[1] === 'ASC' ? 'triangle-up' : 'triangle-down'; | |
} | |
/** | |
* Columns count for the table | |
*/ | |
public get columnsCount(): number { | |
return this.rowButtons && this.rowButtons.length ? this.columns.length + 1 : this.columns.length; | |
} | |
/** | |
* Check if the table is sorted by a given column. | |
* Used to display the sorting arrow. | |
* @param column | |
*/ | |
public isSortedByColumn(column: TableColumn): boolean { | |
return !column.disableSort && this.currentSort && this.currentSort[0] === column.property; | |
} | |
/** | |
* Check if a given column can be sorted. | |
*/ | |
public isSortableColumn(column: TableColumn): boolean { | |
return Boolean(this.rows && this.rows.length && !column.disableSort); | |
} | |
/** | |
* Either change sorting column, or just the direction | |
* if the table is already sorted by that column. | |
*/ | |
public changeSort(column: TableColumn): void { | |
if (!this.isSortableColumn(column)) { | |
return; | |
} | |
if (!this.currentSort || this.currentSort[0] !== column.property) { | |
// Change sorting column, force DESC | |
this.currentSort = [column.property, 'DESC']; | |
} else { | |
// Change direction only | |
this.currentSort = [column.property, this.currentSort[1] === 'DESC' ? 'ASC' : 'DESC']; | |
} | |
this.$emit('change-sort', this.currentSort); | |
} | |
/** | |
* Handle updated page | |
*/ | |
public onChangePage(page: number): void { | |
this.$emit('change-page', page); | |
} | |
/** | |
* Handle clicked row | |
* @param row | |
*/ | |
public onClickRow(row: any): void { | |
this.$emit('click-row', row); | |
} | |
/** | |
* Handle clicked row button | |
*/ | |
public onClickRowButton(row: any, rowButton: RowButton): void { | |
if (typeof rowButton.clickHandler === 'function') { | |
rowButton.clickHandler(row); | |
} | |
} | |
/** | |
* Handle search | |
* @param searchValue | |
*/ | |
public onSearch(searchValue: string): void { | |
if (this.withSearch) { | |
this.$emit('search', searchValue); | |
} | |
} | |
} | |
</script> | |
<style lang="less" scoped> | |
@import '../../less/_variables'; | |
.table-header { | |
display: flex; | |
} | |
table { | |
margin-bottom: 0; | |
} | |
th { | |
color: @table-headings-color; | |
font-weight: bold; | |
text-transform: none; | |
font-size: 1rem; | |
position: relative; | |
&.sortable { | |
cursor: pointer; | |
&:hover { | |
background-color: @table-headings-hover-color; | |
} | |
} | |
.sort-icon { | |
position: absolute; | |
right: 10px; | |
} | |
} | |
thead > tr { | |
background-color: @header-background-color; | |
} | |
tr.loading-row { | |
/* Spinner animation */ | |
@keyframes lds-dual-ring { | |
0% { | |
transform: rotate(0deg); | |
} | |
100% { | |
transform: rotate(360deg); | |
} | |
} | |
.loader { | |
display: block; | |
width: 30px; | |
height: 30px; | |
border-radius: 50%; | |
border: 4px solid @header-background-color; | |
border-color: @header-background-color transparent @header-background-color transparent; | |
animation: lds-dual-ring 1.2s linear infinite; | |
margin: 0 auto; | |
} | |
/* Override hover */ | |
&:hover { | |
background: none; | |
} | |
} | |
.row-button { | |
cursor: pointer; | |
&:hover { | |
color: @global-primary-background; | |
} | |
&:not(:first-child) { | |
margin-left: 8px; | |
} | |
} | |
.global-buttons { | |
margin-right: 20px; | |
button:not(:first-child) { | |
margin-left: 10px; | |
} | |
} | |
</style> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment