Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: new sort, pagination, spinner and checkbox functionality added to table component #5

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
9b838c9
feat: new sort, search and checkbox functionality added to table
JoelJacobStephen Jan 31, 2024
61f49d2
refactor: updated story to reflect new functionality in smart table
JoelJacobStephen Jan 31, 2024
b83ac18
fix: toggle all checkbox now works in all conditions
JoelJacobStephen Feb 1, 2024
afa9324
feat: introduced new debounce functionality to smart table
JoelJacobStephen Feb 2, 2024
9419a65
feat: new pagination and better checkbox functionalities
JoelJacobStephen Feb 9, 2024
c37768c
refactor: updated story to reflect the new added pagination
JoelJacobStephen Feb 9, 2024
28f9ec3
fix: fixed toggling issue in storyh
JoelJacobStephen Feb 9, 2024
4c40ba6
feat: new spinner functionality added to table
JoelJacobStephen Feb 15, 2024
82fb664
refactor: converted watchers into eager watchers and changed few styles
JoelJacobStephen Feb 26, 2024
31832a0
style: updated style classes of checkboxes
JoelJacobStephen Feb 27, 2024
e74e159
refactor: improvements to styles and logic of table component
JoelJacobStephen Feb 28, 2024
d361c9a
refactor: updated story with demo for spinner and pagination
JoelJacobStephen Feb 28, 2024
973ee66
fix: spinner being shown on toggle
JoelJacobStephen Feb 28, 2024
847ad9f
refactor: removed search and spinner duration functionality
JoelJacobStephen Feb 29, 2024
c3c3473
feat: new story variant for table and renamed spinner to loading
JoelJacobStephen Mar 1, 2024
53c54f9
feat: associate loading state with table body and add an empty state
jamesgeorge007 Mar 1, 2024
dba62f1
refactor: remove checkbox prop
jamesgeorge007 Mar 1, 2024
e0f17a5
refactor: remove unnecessary `div` wrapper
jamesgeorge007 Mar 4, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
309 changes: 264 additions & 45 deletions src/components/smart/Table.vue
JoelJacobStephen marked this conversation as resolved.
Show resolved Hide resolved
@@ -1,69 +1,288 @@
<template>
<div class="overflow-auto border shadow-md rounded-md border-dividerDark">
<table class="w-full">
<thead>
<slot name="head">
<div class="flex flex-1 flex-col">
<div v-if="pagination" class="mb-3 flex items-center justify-end">
<HoppButtonSecondary
outline
filled
:icon="IconLeft"
:disabled="page === 1"
@click="changePage(PageDirection.Previous)"
/>

<span class="flex h-full w-10 items-center justify-center">{{
page
}}</span>

<HoppButtonSecondary
outline
filled
:icon="IconRight"
:disabled="page === pagination.totalPages"
@click="changePage(PageDirection.Next)"
/>
</div>

<div class="overflow-auto rounded-md border border-dividerDark shadow-md">
<!-- An Extension Slot to extend the table functionality such as search -->
<slot name="extension"></slot>

<table class="w-full table-fixed">
anwarulislam marked this conversation as resolved.
Show resolved Hide resolved
<thead>
<tr
class="text-sm text-left border-b border-dividerDark bg-primaryLight text-secondary"
class="border-b border-dividerDark bg-primaryLight text-left text-sm text-secondary"
>
<th v-for="th in headings" scope="col" class="px-6 py-3">
{{ th.label ?? th.key }}
<th v-if="selectedRows" class="w-24">
anwarulislam marked this conversation as resolved.
Show resolved Hide resolved
<input
ref="selectAllCheckbox"
type="checkbox"
:checked="areAllRowsSelected"
:disabled="loading"
class="flex h-full w-full items-center justify-center"
@click.stop="toggleAllRows"
/>
</th>
<slot name="head">
<th
v-for="th in headings"
:key="th.key"
scope="col"
class="px-6 py-3"
>
{{ th.label ?? th.key }}
</th>
</slot>
</tr>
</slot>
</thead>
</thead>

<tbody class="divide-y divide-divider">
<!-- We are using slot props for future proofing so that in future, we can implement features like filtering -->
<slot name="body" :list="list">
<tr
v-for="(rowData, rowIndex) in list"
:key="rowIndex"
class="rounded-xl text-secondaryDark hover:cursor-pointer hover:bg-divider"
:class="{ 'divide-x divide-divider': showYBorder }"
>
<td
v-for="cellHeading in headings"
:key="cellHeading.key"
@click="!cellHeading.preventClick && onRowClicked(rowData)"
class="max-w-[10rem] py-1 pl-6"
>
<!-- Dynamic column slot -->
<slot :name="cellHeading.key" :item="rowData">
<!-- Generic implementation of the column -->
<div class="flex flex-col truncate">
<span class="truncate">
{{ rowData[cellHeading.key] ?? "-" }}
</span>
<tbody class="divide-y divide-divider">
<tr v-if="loading">
<slot name="loading-state">
<td :colspan="columnSpan">
<div class="mx-auto my-3 h-5 w-5 text-center">
<HoppSmartSpinner />
</div>
</slot>
</td>
</td>
</slot>
</tr>

<tr v-else-if="!list.length">
<slot name="empty-state">
<td :colspan="columnSpan" class="py-3 text-center">
<p>No data available</p>
</td>
</slot>
</tr>
</slot>
</tbody>
</table>

<template v-else>
<tr
v-for="(rowData, rowIndex) in workingList"
:key="rowIndex"
class="rounded-xl text-secondaryDark hover:cursor-pointer hover:bg-divider"
:class="{ 'divide-x divide-divider': showYBorder }"
@click="onRowClicked(rowData)"
>
<td v-if="selectedRows">
<input
type="checkbox"
:checked="isRowSelected(rowData)"
class="flex h-full w-full items-center justify-center"
@click.stop="toggleRow(rowData)"
/>
</td>
<slot name="body" :row="rowData">
<td
v-for="cellHeading in headings"
:key="cellHeading.key"
class="px-4 py-2"
@click="!cellHeading.preventClick && onRowClicked(rowData)"
>
<!-- Dynamic column slot -->
<slot :name="cellHeading.key" :item="rowData">
<!-- Generic implementation of the column -->
<div class="flex flex-col truncate">
<span class="truncate">
{{ rowData[cellHeading.key] ?? "-" }}
</span>
</div>
</slot>
</td>
</slot>
</tr>
</template>
</tbody>
</table>
</div>
</div>
</template>

<script lang="ts" setup generic="Item extends Record<string, unknown>">
<script lang="ts" setup>
import { useVModel } from "@vueuse/core"
import { isEqual } from "lodash-es"
import { computed, ref, watch } from "vue"

import IconLeft from "~icons/lucide/arrow-left"
import IconRight from "~icons/lucide/arrow-right"

import { HoppSmartSpinner } from ".."
import { HoppButtonSecondary } from "../button"

export type CellHeading = {
key: string
label?: string
preventClick?: boolean
}

defineProps<{
/** Whether to show the vertical border between columns */
showYBorder?: boolean
/** The list of items to be displayed in the table */
list?: Item[]
/** The headings of the table */
headings?: CellHeading[]
}>()
export type Item = Record<string, unknown>

const props = withDefaults(
defineProps<{
/** Whether to show the vertical border between columns */
showYBorder?: boolean
/** The list of items to be displayed in the table */
list: Item[]
/** The headings of the table */
headings?: CellHeading[]

selectedRows?: Item[]
/** Whether to enable sorting */
sort?: {
/** The key to sort the list by */
key: string
direction: Direction
}

/** Whether to enable pagination */
pagination?: {
totalPages: number
}

/** Whether to show a loading spinner */
loading?: boolean
}>(),
{
showYBorder: false,
sort: undefined,
selectedRows: undefined,
loading: false,
},
)

const emit = defineEmits<{
(event: "onRowClicked", item: Item): void
(event: "update:list", list: Item[]): void
(event: "update:selectedRows", selectedRows: Item[]): void
(event: "pageNumber", page: number): void
}>()

// Pagination functionality
const page = ref(1)

enum PageDirection {
Previous,
Next,
}

const changePage = (direction: PageDirection) => {
const isPrevious = direction === PageDirection.Previous

const isValidPreviousAction = isPrevious && page.value > 1
const isValidNextAction =
!isPrevious && page.value < props.pagination!.totalPages

if (isValidNextAction || isValidPreviousAction) {
page.value += isPrevious ? -1 : 1

emit("pageNumber", page.value)
}
}

// The working version of the list that is used to perform operations upon
const workingList = useVModel(props, "list", emit)

// Checkbox functionality
const selectedRows = useVModel(props, "selectedRows", emit)

watch(workingList.value, (updatedList) => {
if (props.selectedRows) {
updatedList = updatedList.map((item) => ({
...item,
selected: false,
}))
}
})

const onRowClicked = (item: Item) => emit("onRowClicked", item)

const isRowSelected = (item: Item) => {
const { selected, ...data } = item
return selectedRows.value?.some((row) => isEqual(row, data))
}

const toggleRow = (item: Item) => {
item.selected = !item.selected
const { selected, ...data } = item

const index = selectedRows.value?.findIndex((row) => isEqual(row, data)) ?? -1

if (item.selected && !isRowSelected(data)) {
selectedRows.value!.push(data)
} else if (index !== -1) {
selectedRows.value?.splice(index, 1)
}
}

const selectAllCheckbox = ref<HTMLInputElement | null>(null)

const toggleAllRows = () => {
const isChecked = selectAllCheckbox.value?.checked
workingList.value.forEach((item) => {
item.selected = isChecked
const { selected, ...data } = item
if (isChecked) {
if (!isRowSelected(item)) {
selectedRows.value!.push(data)
}
return
}
const index =
selectedRows.value?.findIndex((row) => isEqual(row, data)) ?? -1
selectedRows.value!.splice(index, 1)
})
}

const areAllRowsSelected = computed(() => {
if (workingList.value.length === 0 || selectedRows.value?.length === 0)
return false
return workingList.value.every((item) => {
const { selected, ...data } = item
return selectedRows.value?.some((row) => isEqual(row, data))
})
})

// Sort List by key and direction which can set to ascending or descending
export type Direction = "ascending" | "descending"

const sortList = (key: string, direction: Direction) => {
workingList.value.sort((a, b) => {
const valueA = a[key] as string
const valueB = b[key] as string
return direction === "ascending"
? valueA.localeCompare(valueB)
: valueB.localeCompare(valueA)
})
}

watch(
() => props.sort?.direction,
() => {
if (props.sort) {
sortList(props.sort.key, props.sort.direction)
}
},
{ immediate: true },
)

const columnSpan = computed(
() => (props.headings?.length ?? 0) + (props.selectedRows ? 1 : 0),
)
anwarulislam marked this conversation as resolved.
Show resolved Hide resolved
</script>