Skip to content

Instantly share code, notes, and snippets.

@dimchanske
Created March 27, 2019 05:46
Show Gist options
  • Save dimchanske/b85148970a85fd55ac2dfe4e38107dfc to your computer and use it in GitHub Desktop.
Save dimchanske/b85148970a85fd55ac2dfe4e38107dfc to your computer and use it in GitHub Desktop.
VueJS Table Component
<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