Skip to content

Instantly share code, notes, and snippets.

@scbj
Created June 27, 2025 14:37
Show Gist options
  • Select an option

  • Save scbj/2aba180f0ed71bfe052f3d74e9ea1aea to your computer and use it in GitHub Desktop.

Select an option

Save scbj/2aba180f0ed71bfe052f3d74e9ea1aea to your computer and use it in GitHub Desktop.
<template>
<table class="jk-table" :aria-label="ariaLabel" :aria-rowcount="items.length">
<thead>
<tr>
<th
v-for="(column, index) in columns"
:key="index"
class="jk-table__header-cell-container"
>
<div
class="jk-table__header-cell"
:class="
determineCellJustificationModifiers(
'jk-table__header-cell',
column
)
"
>
<div class="jk-table__header-cell-content">
<span v-text="column.label" />
<JkButton
icon="sort-ascending"
no-background
@click="onSortClick(column)"
/>
</div>
<div
class="jk-table__header-cell-resize-handle"
:class="determineResizeHandleModifiers(column)"
@mousedown="onResizeHandleMouseDown($event, column)"
/>
</div>
</th>
</tr>
</thead>
<tbody>
<tr
v-for="(item, index) in displayedItems"
:key="index"
:class="determineRowModifiers(item)"
@click="onRowClick(item)"
>
<td
v-for="(column, columnIndex) in columns"
:key="columnIndex"
:class="{
...determineCellJustificationModifiers(
'jk-table__row-cell',
column
),
...determineRowCellModifiers(index)
}"
>
<slot
v-if="isColumnWithSlot(column)"
:name="`cell:${column.slotName}`"
:item="item"
/>
<span v-else>{{ item[column.property] }}</span>
</td>
</tr>
<tr v-if="hasMoreItems" class="jk-table__load-more">
<LoadMore
:has-more-items="hasMoreItems"
@load-more-requested="loadNextPage"
/>
</tr>
</tbody>
</table>
</template>
<script lang="ts">
import { defineComponent, PropType, toRef } from 'vue'
import JkButton from '@/components/jk/button/JkButton.vue'
import {
Column,
ColumnProps,
ColumnWithSlot
} from '@/components/jk/table-2/types'
import { useResizableTableColumns } from '@/components/jk/table-2/use-resizable-table-columns'
import LoadMore from '@/components/LoadMore.vue'
import { Modifiers } from '@/components/types'
import { useInfiniteScroll } from '@/composables/use-infinite-scroll'
enum Event {
CurrentChange = 'current-change',
Sort = 'sort'
}
export default defineComponent({
name: 'JkTable2',
components: {
JkButton,
LoadMore
},
props: {
ariaLabel: {
type: String,
required: true
},
columns: {
type: Array as PropType<Column[]>,
required: true
},
items: {
type: Array as PropType<unknown[]>,
required: true
},
rowHeight: {
type: String,
required: true
}
},
setup(props) {
const columns = toRef(props, 'columns')
const {
columnBeingResizedLabel,
columnsTemplate,
onResizeHandleMouseDown
} = useResizableTableColumns(columns)
const items = toRef(props, 'items')
const { displayedItems, hasMoreItems, loadNextPage } = useInfiniteScroll(
items,
{ pageLength: 40 }
)
return {
columnBeingResizedLabel,
columnsTemplate,
onResizeHandleMouseDown,
displayedItems,
hasMoreItems,
loadNextPage
}
},
data() {
return {
currentItem: undefined as unknown
}
},
watch: {
currentItem(item: unknown, oldItem: unknown) {
this.$emit(Event.CurrentChange, item, oldItem)
}
},
methods: {
determineCellJustificationModifiers(
className: string,
column: ColumnProps
): Modifiers {
return {
[`${className}--center`]: column.justify === 'center',
[`${className}--right`]: column.justify === 'right',
[`${className}--stretch`]: column.justify === 'stretch',
[`${className}--monospace`]: column.fontFamily === 'monospace'
}
},
determineResizeHandleModifiers(column: ColumnProps): Modifiers {
return {
'jk-table__header-cell-resize-handle--active':
column.label === this.columnBeingResizedLabel
}
},
determineRowCellModifiers(index: number): Modifiers {
return {
'jk-table__row-cell--last': index === this.items.length - 1
}
},
determineRowModifiers(item: unknown): Modifiers {
return {
'jk-table__row--current': this.currentItem === item
}
},
isColumnWithSlot(column: Column): column is ColumnWithSlot {
return 'slotName' in column
},
onRowClick(item: unknown) {
this.currentItem = this.currentItem === item ? null : item
},
onSortClick(column: Column) {
const field = this.isColumnWithSlot(column)
? column.slotName
: column.property
this.$emit(Event.Sort, field)
}
}
})
</script>
<style lang="scss" scoped>
$border-color: var(--grayscale-medium-high-color);
$border-top-radius: $jk-table-2-border-top-radius;
$header-background-color: var(--grayscale-high-color);
$cell-padding: 2rem;
$resize-handle-background-color: $jk-table-2-resize-handle-background-color;
$resize-handle-active-box-shadow-background-color: $jk-table-2-resize-handle-active-box-shadow-background-color;
$resize-handle-width: 1px;
$row-hover-background-color: $jk-table-2-row-hover-background-color;
$row-active-background-color: $jk-table-2-row-active-background-color;
.jk-table {
color: var(--grayscale-medium-low-color);
border: 1px solid $border-color;
border-radius: $border-top-radius $border-top-radius 0 0;
min-width: 100%;
width: auto;
overflow: auto;
border-collapse: collapse;
display: grid;
grid-template-columns: v-bind(columnsTemplate);
grid-auto-rows: v-bind(rowHeight);
text-align: left;
}
thead,
tbody,
tr {
display: contents;
}
td {
display: flex;
align-items: center;
padding: $cell-padding;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
transition: background-color $animation-duration-enter $animation-easing;
&:not(.jk-table__row-cell--last) {
border-bottom: 1px solid $border-color;
}
&.jk-table__row-cell--center {
justify-content: center;
}
&.jk-table__row-cell--right {
justify-content: right;
}
&.jk-table__row-cell--stretch {
justify-content: stretch;
}
&.jk-table__row-cell--monospace {
font-family: monospace;
}
&:hover,
&:hover ~ td,
&:has(~ td:hover) {
background: $row-hover-background-color;
transition: none;
}
}
th {
position: relative;
}
.jk-table__header-cell-container {
z-index: 1;
position: sticky;
top: 0;
left: 0;
background: var(--grayscale-high-color);
}
.jk-table__header-cell {
--resize-handle-opacity: 0;
--resize-handle-transition: none;
--resize-handle-box-shadow: none;
position: relative;
user-select: none;
display: flex;
align-items: center;
height: v-bind(rowHeight);
&.jk-table__header-cell--center {
text-align: center;
}
&.jk-table__header-cell--right {
justify-content: right;
text-align: right;
}
&:last-of-type .jk-table__header-cell-resize-handle {
transform: none;
&::after {
left: calc(100% - $resize-handle-width);
}
}
}
.jk-table__header-cell-content {
padding: $cell-padding;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.jk-table__header-cell-resize-handle {
position: absolute;
top: 0;
right: 0;
bottom: 0;
background: transparent;
width: 16px;
cursor: col-resize;
transform: translateX(50%);
z-index: 1;
&.jk-table__header-cell-resize-handle--active {
--resize-handle-opacity: 1;
--resize-handle-transition: opacity 0.3s linear 0s;
--resize-handle-box-shadow: #{$resize-handle-active-box-shadow-background-color};
}
&::after {
content: '';
position: absolute;
top: 0px;
bottom: 0px;
width: $resize-handle-width;
background: $resize-handle-background-color;
opacity: var(--resize-handle-opacity);
left: 50%;
transition: var(--resize-handle-transition);
box-shadow: var(--resize-handle-box-shadow);
}
&:hover::after {
opacity: 1;
}
}
.jk-table__row--current td,
.jk-table__row--current td:hover,
.jk-table__row--current td:hover ~ td,
.jk-table__row--current td:has(~ td:hover) {
background-color: $row-active-background-color;
}
.jk-table__load-more {
grid-column: 1 / -1;
display: flex;
align-items: center;
justify-content: center;
}
</style>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment