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 8 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
299 changes: 271 additions & 28 deletions src/components/smart/Table.vue
JoelJacobStephen marked this conversation as resolved.
Show resolved Hide resolved
@@ -1,27 +1,79 @@
<template>
<div class="overflow-auto border shadow-md rounded-md border-dividerDark">
<table class="w-full">
<thead>
<slot name="head">
<tr
class="text-sm text-left border-b border-dividerDark bg-primaryLight text-secondary"
>
<div v-if="pagination" class="mb-3 flex justify-end">
JoelJacobStephen marked this conversation as resolved.
Show resolved Hide resolved
<div class="flex w-min">
<HoppButtonSecondary
outline
filled
:icon="IconLeft"
:disabled="page === 1"
@click="changePage(PageDirection.Previous)"
/>

<div class="flex h-full w-10 items-center justify-center">
<p>{{ page }}</p>
JoelJacobStephen marked this conversation as resolved.
Show resolved Hide resolved
</div>

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

<div class="overflow-auto rounded-md border border-dividerDark shadow-md">
<div v-if="searchBar" class="flex w-full items-center bg-primary">
<icon-lucide-search class="ml-3 text-xs" />
<input
v-model="searchQuery"
class="h-full w-full bg-primary p-3"
:placeholder="searchBar.placeholder ?? 'Search...'"
/>
</div>
JoelJacobStephen marked this conversation as resolved.
Show resolved Hide resolved

<div v-if="isSpinnerEnabled" class="mx-auto my-3 h-5 w-5 text-center">
<HoppSmartSpinner />
</div>

<table v-else-if="list" class="w-full">
<thead v-if="list.length > 0">
<tr
class="border-b border-dividerDark bg-primaryLight text-left text-sm text-secondary"
>
<th v-if="checkbox" class="w-5 pl-6 pt-1">
JoelJacobStephen marked this conversation as resolved.
Show resolved Hide resolved
<input
ref="selectAllCheckbox"
type="checkbox"
:checked="areAllRowsSelected"
@click.stop="toggleAllRows"
/>
</th>
<slot name="head">
<th v-for="th in headings" scope="col" class="px-6 py-3">
JoelJacobStephen marked this conversation as resolved.
Show resolved Hide resolved
JoelJacobStephen marked this conversation as resolved.
Show resolved Hide resolved
{{ th.label ?? th.key }}
</th>
</tr>
</slot>
</slot>
</tr>
</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 }"
>
<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="checkbox" class="my-auto pl-6">
JoelJacobStephen marked this conversation as resolved.
Show resolved Hide resolved
<input
type="checkbox"
:checked="isRowSelected(rowData)"
@click.stop="toggleRow(rowData)"
/>
</td>
<slot name="body" :row="rowData">
<td
v-for="cellHeading in headings"
:key="cellHeading.key"
JoelJacobStephen marked this conversation as resolved.
Show resolved Hide resolved
Expand All @@ -38,32 +90,223 @@
</div>
</slot>
</td>
</tr>
</slot>
</slot>
</tr>
</tbody>
</table>
</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"

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[]
/** Whether to show the search bar */
searchBar?: {
/** Whether to debounce the search query event */
debounce?: number
placeholder?: string
}
/** Whether to show the checkbox column
* This will be overriden if custom implementation for body slot is provided
*/
checkbox?: boolean

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 the spinner */
spinner?: {
enabled: boolean
duration?: number
}
}>(),
{
showYBorder: false,
search: undefined,
checkbox: false,
sort: undefined,
selectedRows: undefined,
},
)

const emit = defineEmits<{
(event: "onRowClicked", item: Item): void
(event: "update:list", list: Item[]): void
(event: "search", query: string): 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
if (
(isPrevious && page.value > 1) ||
(!isPrevious && page.value < props.pagination!.totalPages)
) {
page.value += isPrevious ? -1 : 1
}

emit("pageNumber", page.value)
}
JoelJacobStephen marked this conversation as resolved.
Show resolved Hide resolved

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

// Spinner functionality
const isSpinnerEnabled = ref(false)
const showSpinner = (duration: number = 500) => {
isSpinnerEnabled.value = true
setTimeout(() => {
isSpinnerEnabled.value = false
}, duration)
}

watch(
() => props.spinner,
() => {
if (props.spinner?.enabled === true) {
showSpinner(props.spinner.duration)
}
},
)
JoelJacobStephen marked this conversation as resolved.
Show resolved Hide resolved

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

watch(workingList.value, (updatedList) => {
if (props.checkbox) {
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)
JoelJacobStephen marked this conversation as resolved.
Show resolved Hide resolved
}

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

const toggleAllRows = () => {
const isChecked = selectAllCheckbox.value?.checked
workingList.value.forEach((item) => (item.selected = isChecked))

if (isChecked) {
workingList.value.forEach((item) => {
const { selected, ...data } = item
if (!isRowSelected(item)) selectedRows.value!.push(data)
})
} else {
workingList.value.forEach((item) => {
const { selected, ...data } = item
const index =
selectedRows.value?.findIndex((row) => isEqual(row, data)) ?? -1
selectedRows.value!.splice(index, 1)
})
}
}
JoelJacobStephen marked this conversation as resolved.
Show resolved Hide resolved

const areAllRowsSelected = computed(() => {
if (workingList.value.length === 0 || selectedRows.value?.length === 0)
return false

let count = 0
workingList.value.forEach((item) => {
const { selected, ...data } = item
if (selectedRows.value?.findIndex((row) => isEqual(row, data)) !== -1) {
count += 1
}
})
return count === workingList.value.length
})
JoelJacobStephen marked this conversation as resolved.
Show resolved Hide resolved

// 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 = 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)
})
}
JoelJacobStephen marked this conversation as resolved.
Show resolved Hide resolved

watch(workingList.value, () => {
if (props.sort) {
sortList(props.sort.key, props.sort.direction)
}
})
JoelJacobStephen marked this conversation as resolved.
Show resolved Hide resolved

// Searchbar functionality with optional debouncer
const searchQuery = ref("")
let debounceTimeout: number

const debounce = (func: () => void, delay: number) => {
clearTimeout(debounceTimeout)
debounceTimeout = setTimeout(func, delay)
}
JoelJacobStephen marked this conversation as resolved.
Show resolved Hide resolved

watch(searchQuery, () => {
if (props.searchBar?.debounce) {
debounce(() => {
emit("search", searchQuery.value)
}, props.searchBar.debounce)
} else {
emit("search", searchQuery.value)
}
})
</script>