Skip to content

Accessibility: Work in progress

Translations: Work in progress

Table

A flexible and powerful data table component that supports sorting, filtering, pagination, row selection, and customizable columns.

Basic Usage

Using cellFormatter we can render custom content inside the table cells.

ID

First Name

Age

Progress

Rating

DOB

html
<HLDataTableWrapper id="basic-table-wrapper" max-width="100%">
  <HLDataTable
    id="basic-table"
    ref="tableInstance"
    :columns="columns"
    :page-size="6"
    :data="data"
    :horizontal-borders="false"
    :vertical-borders="false"
    max-height="900px"
  />
</HLDataTableWrapper>
ts
import { HLDataTable, HLDataTableWrapper, HLProgress, HLSpace, HLIcon } from '@platform-ui/highrise'
import { Star01Icon } from '@gohighlevel/ghl-icons/24/outline'
ts
const columns = [
  {
    id: 'id',
    header: 'ID',
    accessorKey: 'id',
    size: 100,
    meta: {
      headerAlign: 'start',
    },
  },
  {
    id: 'firstName',
    header: 'First Name',
    sortingFn: 'alphanumeric',
    accessorKey: 'firstName',
    size: 150,
    meta: {
      align: 'left',
      headerAlign: 'left',
    },
  },
  {
    id: 'age',
    header: 'Age',
    accessorKey: 'age',
    sortingFn: 'alphanumeric',
    meta: {
      align: 'right',
      headerAlign: 'right',
    },
  },
  {
    id: 'progress',
    header: 'Progress',
    accessorKey: 'progress',
    sortingFn: 'alphanumeric',
    size: 250,
    cellFormatter: ({ row }) => {
      return h(HLProgress, {
        id: 'progress',
        percentage: row.original.progress,
        type: 'line',
        dashboardSize: 'sm',
        valuePlacement: 'outside',
      })
    },
    meta: {
      headerAlign: 'center',
    },
  },
  {
    id: 'rating',
    accessorKey: 'rating',
    size: 200,
    header: 'Rating',
    meta: {
      headerAlign: 'start',
    },
    cellFormatter: ({ row }) => {
      const rating = row.original.rating
      const stars = []
      for (let i = 0; i < 5; i++) {
        const fill = i < rating ? 'var(--primary-600)' : 'var(--gray-400)'
        stars.push(h(HLIcon, { size: 16, color: fill }, Star01Icon as any))
      }
      return h(HLSpace, { align: 'center', wrapItem: false, size: 4 }, stars)
    },
  },
  {
    id: 'DOB',
    accessorKey: 'DOB',
    meta: {
      align: 'right',
    },
    header: 'DOB',
  },
]
ts
const data = ref([
  {
    id: 1,
    firstName: 'John',
    lastName: 'Doe',
    age: 30,
    progress: 50,
    rating: 4,
    DOB: '2014-01-01',
  },
  {
    id: 2,
    firstName: 'Jane',
    lastName: 'Doe',
    age: 25,
    progress: 75,
    rating: 3,
    DOB: '2024-01-01',
  },
  {
    id: 3,
    firstName: 'Jim',
    lastName: 'Beam',
    age: 40,
    progress: 25,
    rating: 2,
    DOB: '2024-01-01',
  },
])

Table with Breadcrumb and Pagination

Pagination can also be used in the table header by passing the header slot to the HLDataTableWrapper component. The breadcrumb can be removed if not needed.

ID

First Name

Age

Progress

Rating

DOB

html
<div class="h-[40vh]">
  <HLDataTableWrapper id="data-table-wrapper-default" max-width="100%" :fillParentHeight="true">
    <template #footer>
      <div class="flex justify-between items-center">
        <HLBreadcrumb size="md" :breadcrumbs="breadcrumbs" />
        <HLPagination
          v-if="tableInstanceBreadcrumbPagination"
          id="searchable-table-pagination"
          :item-count="tableInstanceBreadcrumbPagination.table.getFilteredRowModel().rows.length"
          :per-page="50"
          :current-page="tableInstanceBreadcrumbPagination.table?.getState().pagination.pageIndex + 1"
          :pages-to-display="7"
          :per-page-dropdown-options="paginationOptions"
          size="sm"
          per-page-text="Rows per page"
          @update:per-page="updatePageSizeBreadcrumbPagination"
          @update:page="
              page => {
                tableInstanceBreadcrumbPagination.table?.setPageIndex(page - 1)
              }
            "
        >
          <template #prev> Previous </template>
          <template #next> Next </template>
        </HLPagination>
      </div>
    </template>
    <HLDataTable
      id="data-table-default"
      :columns="columns"
      :data="data"
      max-height="auto"
      :page-size="50"
      ref="tableInstanceBreadcrumbPagination"
    >
    </HLDataTable>
  </HLDataTableWrapper>
</div>
ts
const breadcrumbs = [
  {
    label: 'BC1',
    href: 'https://google.com',
  },
  {
    label: 'BC2',
    href: 'https://google.com',
  },
  {
    label: 'BC3',
    href: 'https://google.com',
  },
]
ts
const paginationOptions = [
  {
    key: 50,
    label: 50,
  },
  {
    key: 100,
    label: 100,
  },
  {
    key: 200,
    label: 200,
  },
  {
    key: 300,
    label: 300,
  },
]
const updatePageSize = value => {
  tableInstance.value.table?.setPageSize(value)
}

Responsive Columns

Make the table columns have same % of width, by setting the responsive-column-width prop to true.

First Name

Age

Rating

DOB

Vue
html
<HLDataTableWrapper id="basic-table-wrapper" max-width="800px" responsive-column-width>
  <HLDataTable
    id="responsive-table"
    ref="tableInstance"
    :columns="columnsResponsive"
    :data="data"
    :column-hover="true"
    :horizontal-borders="false"
    :vertical-borders="false"
    max-height="900px"
  >
  </HLDataTable>
</HLDataTableWrapper>

Search and filter the table data by setting the show-global-search,show-header and prop to true. Also, handle the @update:global-filter event to filter the table data.

Search
html
<HLButton class="mb-2" @click="updateGlobalFilterForTableWithSearch('')">Clear Filter</HLButton>
<HLDataTableWrapper
  id="row-expand-table-wrapper"
  max-width="1000px"
  :show-header="true"
  :show-global-search="true"
  :global-filter="filterString"
  @update:global-filter="updateGlobalFilter"
>
  <HLDataTable ref="tableInstance" id="table-with-search" :columns="columns" :data="data" max-height="auto" :page-size="6"> </HLDataTable>
</HLDataTableWrapper>
ts
const filterString = ref('')
const tableInstance = ref<any>(null)
const updateGlobalFilter = globalFilter => {
  filterString.value = globalFilter
  tableInstance.value.table?.setGlobalFilter(globalFilter) // set global filter to the table in current rows or make API call to get the filtered data from the server
}

Column Hover

Highlight the entire column when hovering over it.

ID

First Name

Age

Progress

Rating

DOB

Vue
html
<HLDataTableWrapper id="basic-table-wrapper" max-width="1000px">
  <HLDataTable
    id="basic-table"
    ref="tableInstance"
    :columns="columns"
    :data="data"
    :column-hover="true"
    :horizontal-borders="false"
    :vertical-borders="false"
    max-height="900px"
  >
  </HLDataTable>
</HLDataTableWrapper>

Column Resize

Drag the column header to adjust the column width.

ID

First Name

Age

Progress

Rating

DOB

Vue
html
<HLDataTableWrapper id="basic-table-wrapper" max-width="1000px">
  <HLDataTable id="basic-table" ref="tableInstance" :columns="columns" :data="data" :column-resizing="true" max-height="900px">
  </HLDataTable>
</HLDataTableWrapper>

Infinite Scroll

ID

First Name

Age

Progress

Rating

DOB

html
<HLDataTableWrapper id="infinite-scroll-table-wrapper" max-width="1000px">
  <HLDataTable
    ref="infiniteScrollInstance"
    :columns="columns"
    :data="infiniteScrollData"
    :loading="infiniteScrollLoading"
    row-unique-id="id"
    @scroll="handleScroll"
    max-height="500px"
  />
</HLDataTableWrapper>
ts
import { ref } from 'vue'
import { HLDataTableWrapper, HLDataTable } from '@platform-ui/highrise'

const infiniteScrollLoading = ref(false)
const infiniteScrollInstance = ref(null)
const infiniteScrollData = ref([]) // your initial data
const handleScroll = async event => {
  const scrollPosition = event.target.scrollTop + event.target.clientHeight
  if (scrollPosition >= event.target.scrollHeight - 10) {
    infiniteScrollLoading.value = true
    const result = await fetchMoreData() // your API call to fetch more data
    infiniteScrollData.value = [...infiniteScrollData.value, ...result]
    infiniteScrollInstance.value.table?.setPageSize(infiniteScrollData.value.length)
    infiniteScrollLoading.value = false
  }
}

const columns = [
  {
    id: 'id',
    header: 'ID',
    accessorKey: 'id',
    size: 100,
    cellFormatter: ({ row }) => {
      return row.original.id
    },
    meta: {
      headerAlign: 'start',
    },
  },
  {
    id: 'firstName',
    header: 'First Name',
    sortingFn: 'alphanumeric',
    accessorKey: 'firstName',
    size: 150,
    meta: {
      align: 'left',
      headerAlign: 'start',
    },
  },
  {
    id: 'age',
    header: 'Age',
    accessorKey: 'age',
    filterFn: 'greaterThan',
    sortingFn: 'alphanumeric',
    meta: {
      align: 'right',
      headerAlign: 'end',
    },
  },
  {
    id: 'progress',
    header: 'Progress',
    accessorKey: 'progress',
    sortingFn: 'alphanumeric',
    size: 300,
    cellFormatter: ({ row }) => {
      return h(HLProgress, {
        id: 'progress',
        percentage: row.original.progress,
        type: 'line',
        dashboardSize: 'sm',
        valuePlacement: 'outside',
      })
    },
  },
  {
    id: 'rating',
    accessorKey: 'rating',
    size: 200,
    header: 'Rating',
    meta: {
      headerAlign: 'start',
    },
    cellFormatter: ({ row }) => {
      const rating = row.original.rating
      const stars = []
      for (let i = 0; i < 5; i++) {
        const fill = i < rating ? 'var(--primary-600)' : 'var(--gray-400)'
        stars.push(h(HLIcon, { size: '16', color: fill }, Star01Icon as any))
      }
      return h(HLSpace, { align: 'center', wrapItem: false, size: 4 }, { default: () => stars })
    },
  },
  {
    id: 'DOB',
    accessorKey: 'DOB',
    meta: {
      align: 'right',
      headerAlign: 'end',
    },
    size: 200,
    header: 'DOB',
  },
]

Column Re-Order

Reorder columns by dragging and dropping the column headers.

ID

First Name

Age

Progress

Rating

DOB

Vue
html
<HLDataTableWrapper id="basic-table-wrapper" max-width="1000px">
  <HLDataTable id="basic-table" ref="tableInstance" :columns="columns" :data="data" :column-reordering="true" max-height="900px">
  </HLDataTable>
</HLDataTableWrapper>

Sticky Columns

ID

First Name

Age

Progress

Rating

DOB

Vue
html
<HLDataTableWrapper id="sticky-column-table-wrapper" max-width="1000px">
  <HLDataTable
    ref="tableInstance"
    :columns="columns"
    :data="data"
    :striped="true"
    :horizontal-borders="true"
    :vertical-borders="true"
    max-height="900px"
    :freezed-columns="{ left: ['id', 'firstName'] }"
  >
  </HLDataTable>
</HLDataTableWrapper>

Column Sort

By clicking on the column header, you can sort the data in ascending or descending order.

Id

First Name

Age

Progress

Rating

DOB

html
<template>
  <HLDataTableWrapper id="data-table-wrapper" max-width="1000px">
    <HLDataTable
      id="data-table"
      ref="tableInstanceColumnSort"
      :columns="sortTableColumns"
      :data="data"
      :striped="true"
      :horizontal-borders="true"
      :vertical-borders="true"
      max-height="900px"
      row-height="40px"
      :page-size="6"
      header-row-height="40px"
      @update:column-clicked="handleColumnClickSort"
      @no-data="handleNoData"
      :row-hover="true"
      :column-hover="true"
    >
      <template #no-data>
        <HLEmpty
          id="empty-state"
          size="md"
          title="No data available to display. This is a placeholder slot"
          description="This is a placeholder"
          positive-text="Refresh"
          negative-text="Try again"
          icon="info"
        />
      </template>
    </HLDataTable>
  </HLDataTableWrapper>
</template>
<script setup lang="ts">
  import { ref } from 'vue'
  import { HLDataTableWrapper, HLDataTable, HLEmpty } from '@platform-ui/highrise'
  import SimpleSort from './SimpleSort.vue'
  import data from './data.ts'
  const sortTableSortedColumn = ref(null)
  const sortTableSortDir = ref(null)
  const tableInstanceColumnSort = ref<any>(null)
  const sortTableColumns = [
    {
      id: 'id',
      header: 'Id',
      accessorKey: 'id',
      size: 50,
      meta: {
        align: 'center',
      },
    },
    {
      id: 'firstName',
      header: {
        text: 'First Name',
        icon: UserCircleIcon,
        filterComponent: h(SimpleSort, {
          sortDir: sortTableSortDir,
          sortedColumn: sortTableSortedColumn,
          columnName: 'firstName',
        }),
      },
      sortingFn: 'alphanumeric',
      accessorKey: 'firstName',
      size: 150,
      cellFormatter: ({ row }) => {
        if (typeof row.original.firstName === 'string') {
          return row.original.firstName
        } else {
          return h(row.original.firstName)
        }
      },
      meta: {
        align: 'left',
      },
    },
    {
      id: 'age',
      header: {
        text: 'Age',
        icon: BarChartCircle01Icon,
        filterComponent: h(SimpleSort, {
          sortDir: sortTableSortDir,
          sortedColumn: sortTableSortedColumn,
          columnName: 'age',
        }),
      },
      accessorKey: 'age',
      sortingFn: 'alphanumeric',
      meta: {
        align: 'right',
        headerAlign: 'right',
      },
    },
    {
      id: 'progress',
      header: { text: 'Progress', icon: UserCircleIcon },
      accessorKey: 'progress',
      sortingFn: 'alphanumeric',
      size: 250,
      cellFormatter: ({ row }) => {
        return h(HLProgress, {
          id: 'progress',
          percentage: row.original.progress,
          type: 'line',
          dashboardSize: 'sm',
          valuePlacement: 'outside',
        })
      },
      meta: {
        headerAlign: 'left',
      },
    },
    {
      id: 'rating',
      accessorKey: 'rating',
      header: { text: 'Rating', icon: Star01Icon },
      cellFormatter: ({ row }) => {
        const rating = row.original.rating
        const stars = []
        for (let i = 0; i < 5; i++) {
          const fill = i < rating ? 'var(--yellow-500)' : 'var(--gray-400)'
          stars.push(h(HLIcon, { size: '16', color: fill }, Star01Icon))
        }
        return h(
          HLTooltip,
          { trigger: 'hover', variant: 'dark', placement: 'top-start' },
          {
            trigger: () => h('div', { class: 'flex items-center gap-2 cursor-pointer' }, stars),
            default: () =>
              h(HLSpace, { align: 'center', wrapItem: false, size: 4 }, () => [
                h(HLIcon, { size: '20', color: 'white' }, InfoCircleIcon),
                h(HLText, { size: 'sm', weight: 'semibold' }, row.original.rating),
              ]),
          }
        )
      },
      meta: {
        headerAlign: 'left',
      },
    },
    {
      id: 'DOB',
      accessorKey: 'DOB',
      meta: {
        align: 'right',
        headerAlign: 'right',
      },
      header: {
        text: 'DOB',
        icon: CalendarIcon,
        filterComponent: h(SimpleSort, {
          sortDir: sortTableSortDir,
          sortedColumn: sortTableSortedColumn,
          columnName: 'DOB',
        }),
      },
    },
  ]

  const handleColumnClickSort = columnId => {
    if (columnId === 'firstName' || columnId === 'DOB' || columnId === 'age') {
      if (tableInstanceColumnSort.value.table.getState()?.sorting[0]?.id === columnId) {
        const desc = tableInstanceColumnSort.value.table.getState()?.sorting[0]?.desc
        if (desc) {
          sortTableSortedColumn.value = ''
          sortTableSortDir.value = null
          tableInstanceColumnSort.value.table.setSorting([])
        } else {
          sortTableSortedColumn.value = columnId
          sortTableSortDir.value = 'desc'
          tableInstanceColumnSort.value.table.setSorting([{ id: columnId, desc: true }])
        }
      } else {
        sortTableSortedColumn.value = columnId
        sortTableSortDir.value = 'asc'
        tableInstanceColumnSort.value.table.setSorting([{ id: columnId, desc: false }])
      }
    }
  }
  function setSortTableSort(id, desc) {
    sortTableSortedColumn.value = id
    sortTableSortDir.value = desc ? 'desc' : 'asc'
    tableInstanceColumnSort.value.table.setSorting([{ id, desc }])
  }
  function resetSortTableSort() {
    sortTableSortedColumn.value = null
    sortTableSortDir.value = null
    tableInstanceColumnSort.value.table.setSorting([])
  }
</script>
html
<script setup lang="ts">
  import { ArrowDownIcon } from '@gohighlevel/ghl-icons/24/outline'
  import { Ref } from 'vue'
  import { HLIcon } from '@platform-ui/highrise'

  interface Props {
    sortDir: Ref<'asc' | 'desc' | null>
    sortedColumn: Ref<string | null>
    columnName: string
  }
  const props = defineProps<Props>()
</script>
<template>
  <HLIcon
    v-if="sortedColumn.value === columnName"
    size="14"
    color="var(--primary-600)"
    :class="[sortDir.value === 'asc' ? 'rotate-180' : '']"
  >
    <ArrowDownIcon />
  </HLIcon>
</template>
ts
const data = [
  {
    id: 1,
    firstName: 'Felton',
    lastName: 'Ratke',
    age: 32,
    rating: 2,
    status: 'curatio',
    progress: 59,
    toggle: false,
    DOB: '2025/06/28',
  },
  {
    id: 2,
    firstName: 'Marlon',
    lastName: 'Graham',
    age: 54,
    rating: 2,
    status: 'adiuvo',
    progress: 32,
    toggle: false,
    DOB: '2024/05/01',
  },
  {
    id: 3,
    firstName: 'Woodrow',
    lastName: 'Cronin',
    age: 49,
    rating: 1,
    status: 'solio',
    progress: 8,
    toggle: false,
    DOB: '2025/05/31',
  },
  {
    id: 4,
    firstName: 'Abe',
    lastName: 'Padberg',
    age: 61,
    rating: 2,
    status: 'trans',
    progress: 41,
    toggle: false,
    DOB: '2024/10/26',
  },
  {
    id: 5,
    firstName: 'Stephany',
    lastName: 'Berge',
    age: 69,
    rating: 5,
    status: 'eius',
    progress: 30,
    toggle: true,
    DOB: '2026/02/18',
  },
]

Column Filter and Sort

NameFilter and AgeFilter are custom components used for filtering and sorting the data.

Id

First Name

Age

Progress

Rating

DOB

html
<script setup lang="ts">
  import { BarChartCircle01Icon, Star01Icon, UserCircleIcon } from '@gohighlevel/ghl-icons/24/outline'
  import { DataTableColumn, HLDataTable, HLDataTableWrapper, HLIcon, HLProgress, HLSpace } from '@platform-ui/highrise'
  import { h, ref } from 'vue'
  import NameFilter from './NameFilter.vue'

  const sortedColumn = ref<string | null>(null)
  const sortDir = ref<'asc' | 'desc' | null>(null)
  const tableInstance = ref<any>(null)
  const data = ref([]) // TODO: replace with actual data
  const columns: DataTableColumn[] = [
    {
      id: 'id',
      header: 'ID',
      accessorKey: 'id',
      size: 100,
      meta: {
        headerAlign: 'start',
      },
    },
    {
      id: 'firstName',
      header: {
        text: 'First Name',
        icon: h(UserCircleIcon),
        filterComponent: h(NameFilter, {
          onSetFilter: setFilter,
          onClearFilter: clearFilter,
          onSetSort: setSort,
          sortedColumn,
          sortDir,
          id: 'first-name-filter',
          onResetSort: resetSort,
        }),
      },
      sortingFn: 'alphanumeric',
      accessorKey: 'firstName',
      size: 150,
      meta: {
        align: 'start',
      },
    },
    {
      id: 'age',
      header: {
        text: 'Age',
        icon: h(BarChartCircle01Icon),
        filterComponent: h(AgeFilter, {
          onSetFilter: setFilter,
          onClearFilter: clearFilter,
          onSetSort: setSort,
          sortedColumn,
          sortDir,
          id: 'age-filter',
          onResetSort: resetSort,
        }),
      },
      accessorKey: 'age',
      sortingFn: 'alphanumeric',
      meta: {
        align: 'end',
        headerAlign: 'end',
      },
    },
    {
      id: 'progress',
      header: 'Progress',
      accessorKey: 'progress',
      sortingFn: 'alphanumeric',
      size: 250,
      cellFormatter: ({ row }: any) => {
        return h(HLProgress, {
          id: 'progress',
          percentage: row.original.progress,
          type: 'line',
          dashboardSize: 'sm',
          valuePlacement: 'outside',
        })
      },
      meta: {
        headerAlign: 'center',
      },
    },
    {
      id: 'rating',
      accessorKey: 'rating',
      size: 200,
      header: 'Rating',
      meta: {
        headerAlign: 'start',
      },
      cellFormatter: ({ row }: any) => {
        const rating = row.original.rating
        const stars = []
        for (let i = 0; i < 5; i++) {
          const fill = i < rating ? 'var(--primary-600)' : 'var(--gray-400)'
          stars.push(h(HLIcon, { size: 16, color: fill }, Star01Icon as any))
        }
        return h(HLSpace, { align: 'center', wrapItem: false, size: 4 }, stars)
      },
    },
    {
      id: 'DOB',
      accessorKey: 'DOB',
      meta: {
        align: 'end',
        headerAlign: 'end',
      },
      header: 'DOB',
    },
  ]

  function setSort(id: string, dir: 'asc' | 'desc') {
    sortedColumn.value = id
    sortDir.value = dir ? 'desc' : 'asc'
    tableInstance.value?.table.setSorting([{ id, dir }])
  }
  function resetSort() {
    sortedColumn.value = null
    sortDir.value = null
    tableInstance.value?.table.setSorting([])
  }
  function setFilter(id: string, filterFn: any, value: any) {
    const column = tableInstance.value?.table.getColumn(id)
    column.columnDef.filterFn = filterFn
    column.setFilterValue(value)
  }
  function clearFilter(id: string) {
    const column = tableInstance.value?.table.getColumn(id)
    column.setFilterValue(undefined)
  }
</script>
<template>
  <HLDataTableWrapper id="column-filter-table-wrapper">
    <HLDataTable ref="tableInstance" id="column-filter-table" :columns="columns" :data="data" />
  </HLDataTableWrapper>
</template>
html
<script setup lang="ts">
  import { CheckIcon, SearchSmIcon } from '@gohighlevel/ghl-icons/24/outline'
  import {
    HLAccordion,
    HLAccordionItem,
    HLButton,
    HLCheckbox,
    HLCheckboxGroup,
    HLIcon,
    HLInput,
    HLSelect,
    HLSpace,
    HLText,
  } from '@platform-ui/highrise'
  import { nextTick, Ref, ref } from 'vue'
  import FilterCompoent from './FilterComponent.vue'

  const emit = defineEmits(['setSort', 'setFilter', 'clearFilter', 'resetSort'])
  const filterIconColor = ref('var(--gray-400)')
  const nameInputValue = ref('')
  const isFilterApplied = ref(false)
  defineProps<{
    id: string
    sortedColumn: Ref<string | null>
    sortDir: Ref<'asc' | 'desc' | null>
  }>()
  const dropdownOptions = [
    {
      value: 'none',
      label: 'None',
      inputCount: 0,
    },
    {
      value: 'isEmpty',
      label: 'Is Empty',
      inputCount: 0,
    },
    {
      value: 'isNotEmpty',
      label: 'Is Not Empty',
      inputCount: 0,
    },
    {
      value: 'contains',
      label: 'Text contains',
      inputCount: 1,
    },
    {
      value: 'notContains',
      label: 'Text does not contain',
      inputCount: 1,
    },
    {
      value: 'startsWith',
      label: 'Text starts with',
      inputCount: 1,
    },
    {
      value: 'endsWith',
      label: 'Text ends with',
      inputCount: 1,
    },
    {
      value: 'equals',
      label: 'Text is exactly',
      inputCount: 1,
    },
  ]

  const selectedDropdownOption = ref(dropdownOptions[0])

  const handleSort = (dir: 'asc' | 'desc') => {
    emit('setSort', 'firstName', dir == 'desc')
  }
  function handleResetSort() {
    emit('resetSort', 'firstName')
  }
  function handleClear() {
    nameInputValue.value = ''
    isFilterApplied.value = false
    filterIconColor.value = 'var(--gray-400)'
    checkboxValue.value = []
    emit('clearFilter', 'firstName')
  }
  function handleApply() {
    if (checkboxValue.value.length) {
      emit('setFilter', 'firstName', 'containsInArray', checkboxValue.value)
      isFilterApplied.value = true
      filterIconColor.value = 'var(--primary-600)'
      return
    }
    if (selectedDropdownOption.value.value === 'none') {
      handleClear()
      return
    }
    isFilterApplied.value = true
    filterIconColor.value = 'var(--primary-600)'
    emit('setFilter', 'firstName', selectedDropdownOption.value.value, nameInputValue.value)
  }
  const checkboxOptions = [
    { label: '(Blank)', value: '' },
    { label: 'Mark', value: 'Mark' },
    { label: 'Jhon', value: 'Jhon' },
    { label: 'Abdul', value: 'Abdul' },
    { label: 'Victor', value: 'Victor' },
  ]
  const checkboxValue = ref<string[]>([])
  const checkboxInputValue = ref('')
  const nameInputValueRef = ref<any>(null)
  const updateSelectedDropdownOption = (key: string, option: any) => {
    selectedDropdownOption.value = option
    nextTick(() => {
      if (option.inputCount) {
        nameInputValueRef.value?.focus()
      }
    })
  }
</script>
<template>
  <FilterCompoent
    :id="`${id}-name-filter`"
    column-name="firstName"
    :sorted-column="sortedColumn"
    :is-filter-applied="isFilterApplied"
    :sort-dir="sortDir"
    @reset-sort="handleResetSort"
    @set-sort="handleSort"
    @clear-filter="handleClear"
    @apply-click="handleApply"
  >
    <div class="p-2 flex flex-col gap-2">
      <div
        class="data-table-filter-item flex"
        :sort-selected="
          sortedColumn.value == 'firstName' && sortDir.value == 'asc'
        "
        :class="{
          'sort-selected':
            sortedColumn.value == 'firstName' && sortDir.value == 'asc',
        }"
        @click="handleSort('asc')"
      >
        <HLText size="md" :weight="'medium'">Sort Ascending A-Z </HLText>
        <HLIcon v-if="sortedColumn.value == 'firstName' && sortDir.value == 'asc'" size="16" color="var(--primary-700)">
          <CheckIcon />
        </HLIcon>
      </div>
      <div
        class="data-table-filter-item flex"
        :class="{
          'sort-selected':
            sortedColumn.value == 'firstName' && sortDir.value == 'desc',
        }"
        @click="handleSort('desc')"
      >
        <HLText size="md" :weight="'medium'">Sort Descending Z-A </HLText>
        <HLIcon v-if="sortedColumn.value == 'firstName' && sortDir.value == 'desc'" size="16" color="var(--primary-700)">
          <CheckIcon />
        </HLIcon>
      </div>
      <HLAccordion size="sm" :border="false" :zero-padding="true">
        <HLAccordionItem id="1" title="Filter by Condition" name="1" size="sm" :hover-effect="false">
          <div class="flex flex-col gap-1 pt-1">
            <HLSelect
              id="select-default"
              size="xs"
              :options="dropdownOptions"
              :value="selectedDropdownOption.value"
              :option-height="26"
              @update:value="updateSelectedDropdownOption"
            ></HLSelect>
            <HLInput
              v-if="selectedDropdownOption.inputCount"
              id="name-filter"
              ref="nameInputValueRef"
              v-model:model-value="nameInputValue"
              placeholder="Filter by name"
              size="xs"
              clearable
              @update:value="nameInputValue = $event"
            />
          </div>
        </HLAccordionItem>
      </HLAccordion>

      <HLAccordion size="sm" :border="false" :zero-padding="true" :default-expanded-names="['1']">
        <HLAccordionItem id="1" title="Filter by Values" name="1" :hover-effect="false">
          <div class="flex flex-col gap-2 pt-2">
            <HLSpace :size="12" align="center" :wrap-item="false">
              <HLButton
                id="select-all-checkbox"
                size="3xs"
                variant="text"
                color="blue"
                @click="checkboxValue = checkboxOptions.map(i => i.value)"
              >
                Select All {{ checkboxOptions.length }}
              </HLButton>
              <HLButton id="select-all-checkbox" size="3xs" variant="text" color="gray" @click="checkboxValue = []"> Clear </HLButton>
              <HLText size="xs" :weight="'medium'" style="margin-left: auto">Displaying {{ checkboxOptions.length }}</HLText>
            </HLSpace>
            <HLInput
              id="name-filter"
              v-model:model-value="checkboxInputValue"
              :prefix-icon="SearchSmIcon as any"
              placeholder="Filter by placeholder"
              size="2xs"
              clearable
              @update:value="checkboxInputValue = $event"
            />
            <HLCheckboxGroup
              id="checkbox-group-1"
              :value="checkboxValue"
              size="xs"
              style="padding: 4px"
              @update:value="e => (checkboxValue = e as string[])"
            >
              <HLSpace :vertical="true">
                <HLCheckbox
                  v-for="(i, index) in checkboxOptions.filter(i =>
                    i.label.includes(checkboxInputValue)
                  )"
                  :id="`${id}-checkbox-${index}`"
                  :key="index"
                  :value="i.value"
                  size="xs"
                >
                  {{ i.label }}
                </HLCheckbox>
              </HLSpace>
            </HLCheckboxGroup>
          </div>
        </HLAccordionItem>
      </HLAccordion>
    </div>
  </FilterCompoent>
</template>
html
<script setup lang="ts">
  import { ArrowDownIcon, ArrowUpIcon, FilterLinesIcon } from '@gohighlevel/ghl-icons/24/outline'
  import { HLButton, HLIcon, HLPopover } from '@platform-ui/highrise'
  import { Ref, ref } from 'vue'

  const emit = defineEmits(['setSort', 'clearFilter', 'applyClick', 'resetSort'])
  const filterIconColor = ref('var(--gray-400)')
  const props = withDefaults(
    defineProps<{
      id: string
      sortedColumn: Ref<string | null>
      columnName: string
      isFilterApplied: boolean
      showSort?: boolean
      sortDir: Ref<'asc' | 'desc' | null>
    }>(),
    {
      showSort: true,
    }
  )

  function handleClear() {
    filterIconColor.value = 'var(--gray-400)'
    emit('clearFilter', props.columnName)
  }
  function handleApply() {
    filterIconColor.value = 'var(--primary-600)'
    emit('applyClick', props.columnName)
  }
  function handleShow(show: boolean) {
    if (show) {
      filterIconColor.value = 'var(--primary-600)'
    } else if (!props.isFilterApplied) {
      filterIconColor.value = 'var(--gray-400)'
    }
  }
  function toggleSort() {
    if (props.sortDir.value == 'asc') {
      emit('setSort', props.columnName, true)
    } else if (props.sortDir.value == 'desc') {
      emit('resetSort', props.columnName)
    }
  }
</script>
<template>
  <div>
    <HLIcon v-if="sortedColumn.value == columnName" size="14" color="var(--primary-600)" style="cursor: pointer" @click="toggleSort()">
      <ArrowUpIcon v-if="sortDir.value == 'asc'" />
      <ArrowDownIcon v-if="sortDir.value == 'desc'" />
    </HLIcon>
    <HLPopover :id="id" trigger="click" placement="bottom-end" :show-arrow="false" @show="handleShow">
      <template #trigger>
        <HLIcon size="14" :color="filterIconColor">
          <FilterLinesIcon />
        </HLIcon>
      </template>
      <template #default>
        <slot></slot>
      </template>
      <template #footer>
        <div class="flex gap-2 justify-end p-2">
          <HLButton :id="`${id}-filter-clear`" variant="ghost" color="gray" size="3xs" @click="handleClear"> Clear </HLButton>
          <HLButton :id="`${id}-filter-apply`" variant="ghost" color="blue" size="3xs" @click="handleApply"> Apply </HLButton>
        </div>
      </template>
    </HLPopover>
  </div>
</template>

Empty State

Display an empty state when the table has no data using the no-data slot.

ID

First Name

Progress

Rating

DOB

No data available to display. This is a placeholder slot

This is a placeholder

html
<HLDataTableWrapper>
  <HLDataTable
    ref="tableInstance"
    :columns="columns"
    :data="data"
    :striped="true"
    :horizontal-borders="true"
    :vertical-borders="true"
    max-height="900px"
  >
    <template #no-data>
      <HLEmpty
        id="empty-state"
        size="md"
        title="No data available to display. This is a placeholder slot"
        description="This is a placeholder"
        positive-text="Refresh"
        negative-text="Try again"
      />
    </template>
  </HLDataTable>
</HLDataTableWrapper>
ts
import { HLDataTable, HLDataTableWrapper, HLEmpty } from '@platform-ui/highrise'

Error State

Display an error state when the table encounters an error using the no-data slot.

ID

First Name

Progress

Rating

DOB

Something went wrong while fetching your appointments

You can try again now or after 10 minutes

html
<HLDataTableWrapper id="error-state-table-wrapper" max-width="1000px">
  <HLDataTable
    ref="tableInstance"
    :columns="columns"
    :data="data"
    :striped="true"
    :horizontal-borders="true"
    :vertical-borders="true"
    max-height="900px"
  >
    <template #no-data>
      <HLEmpty
        id="empty-state"
        size="md"
        title="No data available to display. This is a placeholder slot"
        description="This is a placeholder"
        positive-text="Refresh"
        negative-text="Try again"
      />
    </template>
  </HLDataTable>
</HLDataTableWrapper>
ts
import { HLDataTable, HLDataTableWrapper, HLEmpty } from '@platform-ui/highrise'

Highlighted Rows

Highlight specific rows in the table using the highlighted-rows prop.

ID

First Name

Age

Progress

Rating

DOB

Vue
html
<HLContentWrap>
  <HLDataTableWrapper id="highlighted-rows-table-wrapper" max-width="1000px">
    <HLDataTable
      ref="tableInstance"
      :columns="columns"
      :data="data"
      :horizontal-borders="true"
      :vertical-borders="true"
      :row-hover="false"
      :column-hover="false"
      max-height="900px"
      :highlighted-rows="[1,5,8]"
    >
    </HLDataTable>
  </HLDataTableWrapper>
</HLContentWrap>

Row Expand

Expand a row to show more information by handling the @table-row-clicked event.

ID

First Name

Age

Progress

Rating

DOB

html
<HLContentWrap>
  <HLDataTableWrapper>
    <HLDataTable
      ref="tableInstance"
      :columns="columns"
      :data="data"
      :horizontal-borders="true"
      :vertical-borders="true"
      :row-hover="false"
      :column-hover="false"
      max-height="900px"
      @table-row-clicked="handleTableRowClick"
      :expanded-row-renderer="ExpandedRow"
    >
    </HLDataTable>
  </HLDataTableWrapper>
</HLContentWrap>
ts
const columns = [
  {
    id: 'expand',
    header: '',
    cellFormatter: ({ row }) => {
      return h(
        HLButton,
        {
          variant: 'ghost',
          size: '3xs',
          label: 'Expand',
        },
        { icon: row.getIsExpanded() ? ChevronUpIcon : ChevronDownIcon }
      )
    },
    size: 32,
    meta: {
      align: 'center',
    },
  },
  {
    id: 'id',
    header: 'ID',
    accessorKey: 'id',
    size: 100,
    cell: ({ row }) => {
      return row.original.id
    },
  },
  {
    id: 'firstName',
    header: 'First Name',
    sortingFn: 'alphanumeric',
    accessorKey: 'firstName',
    size: 150,
    meta: {
      align: 'left',
      sticky: true,
    },
  },
]

const data = ref([
  {
    id: 1,
    firstName: 'John',
    lastName: 'Doe',
    age: 30,
    progress: 50,
    DOB: '2014-01-01',
    expandData: ['This is expanded content for John Doe'],
  },
  {
    id: 2,
    firstName: 'Jane',
    lastName: 'Doe',
    age: 25,
    progress: 75,
    DOB: '2024-01-01',
    expandData: ['This is expanded content for Jane Doe'],
  },
  {
    id: 3,
    firstName: 'Jim',
    lastName: 'Beam',
    age: 40,
    progress: 25,
    DOB: '2024-01-01',
    expandData: ['This is expanded content for Jim Beam'],
  },
])
const handleTableRowClick = (row, rowInfo) => {
  rowInfo.getToggleExpandedHandler()()
}

Row Reorder

Reorder rows by dragging and dropping them.

ID

First Name

Age

Progress

Rating

DOB

Vue
html
<HLContentWrap>
  <HLDataTableWrapper id="row-re-reorder-example">
    <HLDataTable
      id="row-re-reorder-table"
      ref="tableInstance"
      :columns="columns"
      :data="data"
      :horizontal-borders="true"
      :vertical-borders="true"
      max-height="900px"
      :row-reordering="true"
    >
    </HLDataTable>
  </HLDataTableWrapper>
</HLContentWrap>

Row Selection

Select rows in the table by checking and can be used when there is CTAs to perform actions on the selected rows.

ID

First Name

Age

Progress

Rating

DOB

html
<HLDataTableWrapper>
  <HLDataTable
    ref="tableInstance"
    :columns="columns"
    :data="data"
    :highlight-selected-row="true"
    :selected-rows="selectedRows"
    :row-hover="true"
    max-height="900px"
    row-reordering="true"
  >
  </HLDataTable>
</HLDataTableWrapper>
ts
const selectedRows = ref<string[]>([])

const toggleRowSelection = (rowId: string, isChecked: boolean) => {
  if (isChecked) {
    selectedRows.value.push(rowId)
  } else {
    selectedRows.value = selectedRows.value.filter(id => id !== rowId)
  }
}

const toggleAllRowsSelection = (isChecked: boolean) => {
  if (isChecked) {
    selectedRows.value = data.value.map(row => row.id)
  } else {
    selectedRows.value = []
  }
}
const columns = [
  {
    id: 'id',
    header: 'ID',
    accessorKey: 'id',
    size: 50,
    meta: {
      align: 'center',
    },
  },
  {
    id: 'checkbox',
    header: ({ table }) => {
      return h(HLCheckbox, {
        checked: selectedRows.value.length === data.value.length,
        indeterminate: selectedRows.value.length > 0 && selectedRows.value.length < data.value.length,
        id: 'jkl',
        onUpdateChecked: e => toggleAllRowsSelection(e),
        style: {
          justifyContent: 'center',
        },
      })
    },
    cellFormatter: ({ row }) => {
      return h(HLCheckbox, {
        checked: selectedRows.value.includes(row.original.id),
        id: `select-${row.original.id}`,
        onUpdateChecked: () => toggleRowSelection(row.original.id, !selectedRows.value.includes(row.original.id)),
      })
    },
    size: 50,
    meta: {
      align: 'center',
    },
  },
  {
    id: 'firstName',
    header: 'First Name',
    sortingFn: 'alphanumeric',
    accessorKey: 'firstName',
    size: 150,
    meta: {
      align: 'left',
    },
  },
]

Row Striped

Add striped rows to the table to improve readability.

ID

First Name

Age

Progress

Rating

DOB

Vue
html
<HLDataTableWrapper>
  <HLDataTable
    ref="tableInstance"
    :columns="columns"
    :data="data"
    :striped="true"
    :horizontal-borders="true"
    :vertical-borders="true"
    max-height="900px"
  >
  </HLDataTable>
</HLDataTableWrapper>

Table Cell Borders

ID

First Name

Age

Progress

Rating

DOB

TS
ts
const data = ref([
  {
    id: 1,
    firstName: { text: 'John', state: 'var(--success-500)' },
    lastName: 'Doe',
    age: 30,
    progress: 50,
    DOB: '2014-01-01',
  },
  {
    id: 2,
    firstName: { text: 'Jane', state: 'var(--error-500)' },
    lastName: 'Doe',
    age: 25,
    progress: 75,
    DOB: '2024-01-01',
  },
  {
    id: 3,
    firstName: { text: 'Jim', state: 'var(--warning-500)' },
    lastName: 'Beam',
    age: 40,
    progress: 25,
    DOB: '2024-01-01',
  },
])

Table Cell Icon

Add an icon to the table cell by setting the prefixIcon in data.

Last Name

ID

First Name

Age

Progress

Rating

DOB

ts
const data = ref([
  {
    id: 1,
    firstName: 'John',
    lastName: { text: 'Doe', prefixIcon: Wallet01Icon },
    age: 30,
    progress: 50,
    DOB: '2014-01-01',
  },
  {
    id: 2,
    firstName: 'Jane',
    lastName: { text: 'Doe', prefixIcon: Wallet02Icon },
    age: 25,
    progress: 75,
    DOB: '2024-01-01',
  },
  {
    id: 3,
    firstName: 'Jim',
    lastName: { text: 'Beam', prefixIcon: Wallet03Icon },
    age: 40,
    progress: 25,
    DOB: '2024-01-01',
  },
])
ts
import { Wallet01Icon, Wallet02Icon, Wallet03Icon } from '@gohighlevel/ghl-icons/24/outline'

Advanced Usage of Table

Search

Id

Toggle

First Name

Age

Progress

Radio

Rating

DOB

html
<div v-if="showAlert" class="hr-table-header-alerts pb-2">
  <HLAlert
    id="hr-table-header-alert"
    closable
    :title="alertTitle"
    role="alert"
    height="auto"
    width="fit-content"
    color="green"
    @close="showAlert = false"
  >
  </HLAlert>
</div>
<HLDataTableWrapper
  id="data-table-wrapper"
  :column-options="columnOptions"
  :page-size="pageSize"
  :no-of-rows-selected="selectedRows?.length"
  :layout-options="preSetLayouts"
  :freezed-column="freezedColumn"
  :table-height-selected-option="tableHeightSelectedOption"
  :user-driven-value="userDrivenValue"
  :show-global-search="true"
  :show-layout-options="true"
  :show-header="true"
  :selected-table-layout="tableLayout"
  :show-row-layout-option="true"
  :show-column-layout-option="!selectedRows?.length"
  :show-table-height-layout-option="!selectedRows?.length"
  @update:column-reorder="handleReorder"
  @update:column-checked="handleChecked"
  @update:column-freeze="handleFreeze"
  @update:column-unfreeze="handleUnfreeze"
  @update:page-size="updateCustomPageSize"
  @update:table-height-selected-option="setTableHeightSelectedOption"
  @update:row-deselect-all="handleDeselectAll"
  @update:global-filter="handleGlobalFilterChange"
  @update:table-layout="handleSaveLayout"
  @update:table-layout-as="handleSaveAsLayout"
  @update:cancel="handleCancelLayout"
  @update:switch-table-layout="handleSwitchLayout"
  @update:delete-table-layout="handleDeleteLayout"
  @update:current-layout="updateCurrentLayout"
>
  <template #header-content-right>
    <HLButton v-if="selectedRows?.length" id="header-button-cta" size="2xs" variant="secondary" color="gray">
      Download
      <template #iconLeft>
        <Download01Icon />
      </template>
    </HLButton>
    <HLButton v-if="selectedRows?.length" id="header-button-cta" size="2xs" variant="secondary" color="gray">
      Share
      <template #iconLeft>
        <Share01Icon />
      </template>
    </HLButton>
    <HLButton v-if="selectedRows?.length" id="header-button-cta" size="2xs" variant="secondary" color="red">
      Delete
      <template #iconLeft>
        <Trash01Icon />
      </template>
    </HLButton>
    <HLDropdown
      v-if="selectedRows?.length"
      id="header-button-cta"
      width="32px"
      trigger="click"
      placement="bottom"
      :options="[{
        label: 'Download',
        icon: Download01Icon,
      },
      {
        label: 'Share',
        icon: Share01Icon,
      }]"
      :show-arrow="false"
    >
      <HLButton id="header-button-cta" size="2xs" variant="text" color="gray">
        <template #icon>
          <DotsVerticalIcon />
        </template>
      </HLButton>
    </HLDropdown>
    <HLButton v-if="!selectedRows?.length" id="header-button-cta" size="2xs" variant="primary" color="blue"> Button CTA </HLButton>
  </template>
  <template v-if="selectedRows?.length" #header-content-left>
    <HLText size="sm" weight="regular"> {{selectedRows?.length}} rows selected </HLText>
    <HLButton id="header-button-select-all" size="2xs" variant="text" color="blue" @click="handleSelectAll">
      Select All ({{data?.length}})
    </HLButton>
    <HLButton id="header-button-deselect-all" size="2xs" variant="ghost" @click="handleDeselectAll" style="font-size: 14px !important;">
      <template #icon>
        <CloseIcon />
      </template>
    </HLButton>
  </template>
  <HLDataTable
    id="data-table"
    ref="tableInstance"
    :columns="SearchTableColumns"
    :data="searchTableData"
    :striped="true"
    :horizontal-borders="true"
    :vertical-borders="true"
    max-height="900px"
    row-height="40px"
    header-row-height="40px"
    :current-layout="currentLayout"
    @update:column-checked="handleChecked"
    @update:column-order="handleColumnOrder"
    @update:column-clicked="handleColumnClick"
    :selected-rows="selectedRows"
    @no-data="handleNoData"
    :column-order="columnOrder"
    :freezed-columns="{left: freezedColumns}"
    :row-hover="true"
    :column-hover="true"
    :highlight-selected-row="true"
    :row-reordering="true"
    :column-reordering="true"
  >
    <template #no-data>
      <HLEmpty
        id="empty-state"
        size="md"
        title="No data available to display. This is a placeholder slot"
        description="This is a placeholder"
        positive-text="Refresh"
        negative-text="Try again"
        icon="info"
      />
    </template>
  </HLDataTable>
  <template #footer v-if="!noData">
    <HLSpace v-if="table && tableHeightSelectedOption !== 'all'" :size="4" align="center" justify="end">
      <HLPagination
        id="searchable-table-pagination"
        :item-count="table.getFilteredRowModel().rows?.length"
        :per-page="table.getState().pagination.pageSize"
        :current-page="table.getState().pagination.pageIndex + 1"
        :pages-to-display="7"
        :per-page-dropdown-options="paginationOptions"
        size="sm"
        per-page-text="Rows per page"
        @update:per-page="updatePageSize"
        @update:page="
          page => {
            table.setPageIndex(page - 1)
          }
        "
      >
        <template #prev> Previous </template>
        <template #next> Next </template>
      </HLPagination>
    </HLSpace>
    <HLSpace v-if="table && tableHeightSelectedOption === 'all'" :size="4" align="center" justify="center">
      <HLButton size="2xs" variant="secondary" color="gray" @click="handleLoadMore">Load More </HLButton>
    </HLSpace>
  </template>
</HLDataTableWrapper>
ts
import {
  HLDataTable,
  HLDataTableWrapper,
  HLAlert,
  HLSpace,
  HLPagination,
  HLButton,
  HLText,
  HLEmpty,
  HLDropdown,
  HLCheckbox,
  HLProgress,
  HLRadio,
  HLToggle,
  HLTooltip,
} from '@platform-ui/highrise'
import { Download01Icon, Share01Icon, Trash01Icon, DotsVerticalIcon } from '@gohighlevel/ghl-icons/24/outline'
ts
const searchTableData = ref([
  {
    id: 1,
    firstName: 'John',
    lastName: 'Doe',
    age: 25,
    rating: 4,
    status: 'Active',
    progress: 75,
    toggle: true,
    DOB: '2024-01-01',
  },
])

const sortedColumn = ref(null)
const sortDir = ref(null)
const SearchTableColumns = [
  {
    id: 'checkbox',
    header: () => {
      return h(HLCheckbox, {
        checked: selectedRows.value?.length === data.value?.length,
        indeterminate: selectedRows.value?.length > 0 && selectedRows.value?.length < data.value?.length,
        id: 'header-checkbox',
        size: 'xs',
        onUpdateChecked: e => toggleAllRowsSelection(e),
        style: {
          justifyContent: 'center',
        },
      })
    },
    cellFormatter: ({ row }) => {
      return h(HLCheckbox, {
        checked: selectedRows.value.includes(row.original.id),
        id: `select-${row.original.id}`,
        size: 'xs',
        style: {
          justifyContent: 'center',
        },
        onUpdateChecked: () => toggleRowSelection(row.original.id, !selectedRows.value.includes(row.original.id)),
      })
    },
    size: 32,
    meta: {
      align: 'center',
    },
  },
  {
    id: 'id',
    header: 'Id',
    accessorKey: 'id',
    size: 50,
    meta: {
      align: 'center',
    },
  },
  {
    id: 'toggle',
    accessorKey: 'toggle',
    header: 'Toggle',
    size: 70,
    cellFormatter: ({ row }) =>
      h(HLSpace, { align: 'center', wrapItem: false, justify: 'center' }, () =>
        h(HLToggle, {
          id: 'toggle',
          'onUpdate:value': value => {
            row.original.toggle = value
          },
          value: row.original.toggle,
        })
      ),
    meta: {
      align: 'center',
    },
  },
  {
    id: 'firstName',
    header: {
      text: 'First Name',
      icon: UserCircleIcon,
      filterComponent: h(NameFilter, {
        onSetFilter: setFilter,
        onClearFilter: clearFilter,
        onSetSort: setSort,
        sortedColumn,
        sortDir,
        id: 'first-name-filter',
        onResetSort: resetSort,
      }),
    },
    sortingFn: 'alphanumeric',
    accessorKey: 'firstName',
    size: 150,
    cellFormatter: ({ row }) => {
      if (typeof row.original.firstName === 'string') {
        return row.original.firstName
      } else {
        return h(row.original.firstName)
      }
    },
    meta: {
      align: 'left',
    },
  },
  {
    id: 'age',
    header: {
      text: 'Age',
      icon: BarChartCircle01Icon,
      filterComponent: h(AgeFilter, {
        onSetFilter: setFilter,
        onClearFilter: clearFilter,
        onSetSort: setSort,
        sortedColumn,
        sortDir,
        id: 'age-filter',
        onResetSort: resetSort,
      }),
    },
    accessorKey: 'age',
    sortingFn: 'alphanumeric',
    meta: {
      align: 'right',
      headerAlign: 'right',
    },
  },
  {
    id: 'progress',
    header: { text: 'Progress', icon: UserCircleIcon },
    accessorKey: 'progress',
    sortingFn: 'alphanumeric',
    size: 250,
    cellFormatter: ({ row }) => {
      return h(HLProgress, {
        id: 'progress',
        percentage: row.original.progress,
        type: 'line',
        dashboardSize: 'sm',
        valuePlacement: 'outside',
      })
    },
    meta: {
      headerAlign: 'left',
    },
  },
  {
    id: 'radio',
    header: 'Radio',
    size: 70,
    cellFormatter: ({ row }) => {
      return h(HLRadio, {
        id: 'radio',
        value: row.original.id,
        size: 'xs',
        onChange: () => {
          selectedRadio.value = row.original.id
        },
        checked: selectedRadio.value === row.original.id,
      })
    },
    meta: {
      align: 'center',
    },
  },
  {
    id: 'rating',
    accessorKey: 'rating',
    header: { text: 'Rating', icon: Star01Icon },
    cellFormatter: ({ row }) => {
      const rating = row.original.rating
      const stars = []
      for (let i = 0; i < 5; i++) {
        const fill = i < rating ? 'var(--yellow-500)' : 'var(--gray-400)'
        stars.push(h(HLIcon, { size: '16', color: fill }, Star01Icon))
      }
      return h(
        HLTooltip,
        { trigger: 'hover', variant: 'dark', placement: 'top-start' },
        {
          trigger: h('div', { class: 'flex items-center gap-2 cursor-pointer' }, stars),
          default: h(HLSpace, { align: 'center', wrapItem: false, size: 4 }, [
            h(HLIcon, { size: '20', color: 'white' }, InfoCircleIcon),
            h(HLText, { size: 'sm', weight: 'semibold' }, row.original.rating),
          ]),
        }
      )
    },
    meta: {
      headerAlign: 'left',
    },
  },
  {
    id: 'DOB',
    accessorKey: 'DOB',
    meta: {
      align: 'right',
      headerAlign: 'right',
    },
    header: {
      text: 'DOB',
      icon: CalendarIcon,
      filterComponent: h(DateFilter, {
        onSetFilter: setFilter,
        onClearFilter: clearFilter,
        onSetSort: setSort,
        sortedColumn,
        sortDir,
        id: 'date-filter',
        onResetSort: resetSort,
      }),
    },
  },
]
const freezedColumns = ref(['checkbox'])
const columnOptions = ref([
  {
    value: 'id',
    label: 'Id',
    checked: true,
  },

  {
    value: 'toggle',
    label: 'Toggle',
    checked: true,
    icon: Wallet01Icon,
  },
  {
    value: 'firstName',
    label: 'First Name',
    checked: true,
    icon: Wallet01Icon,
  },
  {
    value: 'age',
    label: 'Age',
    checked: true,
    icon: Wallet01Icon,
  },
  {
    value: 'progress',
    label: 'Progress',
    checked: true,
    icon: BarChartCircle01Icon,
  },
  {
    value: 'radio',
    label: 'Radio',
    checked: true,
  },
  {
    value: 'rating',
    label: 'Rating',
    checked: true,
    icon: Star01Icon,
  },
  {
    value: 'DOB',
    label: 'DOB',
    checked: true,
    icon: CalendarDateIcon,
  },
])
const paginationOptions = ref([
  {
    key: 50,
    label: 50,
  },
  {
    key: 100,
    label: 100,
  },
  {
    key: 200,
    label: 200,
  },
  {
    key: 300,
    label: 300,
  },
])

const preSetLayouts = ref([
  {
    value: 'custom',
    label: 'Custom',
  },
  {
    value: 'all',
    label: 'All',
  },
])
const selectedRadio = ref(null)
function handleSaveAsLayout(value) {
  preSetLayouts.value.push({
    value: value,
    label: value,
  })
  showAlert.value = true
  alertTitle.value = `${value} Layout saved successfully`
}
function handleSaveLayout() {
  showAlert.value = true
  alertTitle.value = `${tableLayout.value.label || 'Default'} Layout saved successfully`
}
let backUpLayoutColumnData = {
  columnOptions: [],
  freezedColumn: {},
}
let backUpLayoutTableHeight = {
  tableHeightSelectedOption: '',
  pageSize: '',
  userDrivenValue: '',
}

function handleCancelLayout(value) {
  if (previousLayout === 'columns') {
    columnOptions.value = [...backUpLayoutColumnData.columnOptions]

    // Handle visibility for each column
    columnOptions.value.forEach(option => {
      const tableColumn = tableInstance.value.table?.getColumn(option.value)
      if (tableColumn?.getIsVisible() !== option.checked) {
        tableColumn?.toggleVisibility(option.checked)
      }
    })

    // Handle column order
    const columnIdsInOrder = columnOptions.value.map(option => option.value)
    tableInstance.value.table?.setColumnOrder(columnIdsInOrder)
    // Handle frozen columns
    if (backUpLayoutColumnData?.freezedColumn?.value && backUpLayoutColumnData?.freezedColumn?.value !== 'None') {
      const columnIds = ['checkbox', ...columnOptions.value.map(option => option.value)]
      freezedColumn.value = { ...backUpLayoutColumnData.freezedColumn }
      const columnIndex = columnIds.indexOf(backUpLayoutColumnData.freezedColumn.value)
      tableInstance.value.table?.setColumnPinning({
        left: columnIds.slice(0, columnIndex + 1),
        right: [],
      })
    } else {
      freezedColumn.value = {
        value: 'None',
        label: 'None',
      }
      tableInstance.value.table?.setColumnPinning({
        left: ['checkbox'],
        right: [],
      })
    }
  }
  if (previousLayout === 'tableHeight') {
    tableHeightSelectedOption.value = backUpLayoutTableHeight.tableHeightSelectedOption
    pageSize.value = backUpLayoutTableHeight.pageSize
    userDrivenValue.value = backUpLayoutTableHeight.userDrivenValue
    if (backUpLayoutTableHeight.tableHeightSelectedOption === 'userDriven') {
      tableInstance.value.table?.setPageSize(backUpLayoutTableHeight.userDrivenValue)
    } else {
      tableInstance.value.table?.setPageSize(pageSize.value)
    }
  }
}
function handleGlobalFilterChange(value) {
  table.value.setGlobalFilter(String(value))
}

const handleColumnClick = columnId => {
  if (columnId === 'firstName' || columnId === 'DOB' || columnId === 'age') {
    if (table.value.getState()?.sorting[0]?.id === columnId) {
      const desc = table.value.getState()?.sorting[0]?.desc
      if (desc) {
        sortedColumn.value = ''
        sortDir.value = null
        table.value.setSorting([])
      } else {
        sortedColumn.value = columnId
        sortDir.value = 'desc'
        table.value.setSorting([{ id: columnId, desc: true }])
      }
    } else {
      sortedColumn.value = columnId
      sortDir.value = 'asc'
      table.value.setSorting([{ id: columnId, desc: false }])
    }
  }
}
function setSort(id, desc) {
  sortedColumn.value = id
  sortDir.value = desc ? 'desc' : 'asc'
  table.value.setSorting([{ id, desc }])
}
function resetSort() {
  sortedColumn.value = null
  sortDir.value = null
  table.value.setSorting([])
}
function setFilter(id, filterFn, value) {
  const column = table.value.getColumn(id)
  column.columnDef.filterFn = filterFn
  column.setFilterValue(value)
}
function clearFilter(id) {
  const column = table.value.getColumn(id)
  column.setFilterValue(undefined)
}

const handleColumnOrder = columnIds => {
  columnIds.shift()
  columnOptions.value = columnIds.map(id => columnOptions.value.find(option => option.value === id))
}
const handleReorder = (...args) => {
  columnOptions.value = args[0]
  const columnIds = columnOptions.value.filter(option => option.checked !== false).map(option => option.value)
  tableInstance.value.table?.setColumnOrder(columnIds)
}
const handleChecked = (...args) => {
  const [field, checked] = args
  const column = columnOptions.value.find(option => option.value === field)
  column.checked = checked
  const tableColumn = tableInstance.value.table?.getColumn(field)
  tableColumn?.toggleVisibility(checked)
}
const freezedColumn = ref()
const handleFreeze = item => {
  freezedColumn.value = item
  const columnIds = ['checkbox', ...columnOptions.value.map(option => option.value)]
  const columnIndex = columnIds.indexOf(item.value)
  tableInstance.value.table?.setColumnPinning({
    left: columnIds.slice(0, columnIndex + 1),
    right: [],
  })
}
const handleUnfreeze = () => {
  columnOptions.value.forEach(option => {
    option.frozen = false
  })
  tableInstance.value.table?.setColumnPinning({
    left: ['checkbox'],
    right: [],
  })
}
const pageSize = ref(20)
const tableHeightSelectedOption = ref('default')
const userDrivenValue = ref(null)
const updateCustomPageSize = userDrivenNumber => {
  userDrivenValue.value = userDrivenNumber
  tableInstance.value.table?.setPageSize(userDrivenNumber)
  // add in pagination options with ascending order
  paginationOptions.value = paginationOptions.value.filter(option => option.key !== userDrivenNumber)
  paginationOptions.value.push({
    key: userDrivenNumber,
    label: userDrivenNumber,
  })
  paginationOptions.value.sort((a, b) => a.key - b.key)
}
const setTableHeightSelectedOption = value => {
  tableHeightSelectedOption.value = value
  if (tableHeightSelectedOption.value === 'default') {
    tableInstance.value.table?.setPageSize(pageSize.value)
  }
}
const updatePageSize = value => {
  tableHeightSelectedOption.value = 'default'
  pageSize.value = Number(value)
  tableInstance.value.table?.setPageSize(pageSize.value)
}
const handleLoadMore = () => {
  tableInstance.value.table?.setPageSize(table.value.getState().pagination.pageSize + 50)
}

const handleDeselectAll = () => {
  selectedRows.value = []
}
const handleSelectAll = () => {
  selectedRows.value = data.value.map(row => row.id)
}

const currentLayout = ref('')
let previousLayout = ''
const updateCurrentLayout = value => {
  if (value) previousLayout = value
  currentLayout.value = value
  if (value === 'columns') {
    backUpLayoutColumnData = {
      columnOptions: columnOptions.value.map(option => ({
        ...option,
        icon: option.icon, // Preserve the icon reference directly
      })),
      freezedColumn: freezedColumn.value
        ? {
            ...freezedColumn.value,
            icon: freezedColumn.value.icon, // Preserve the icon reference if it exists
          }
        : null,
    }
  } else if (value === 'tableHeight') {
    backUpLayoutTableHeight = {
      tableHeightSelectedOption: tableHeightSelectedOption.value,
      pageSize: pageSize.value,
      userDrivenValue: userDrivenValue.value,
    }
  }
}
const noData = ref(false)
const handleNoData = value => {
  noData.value = value
}

const tableLayout = ref({
  value: 'default',
  label: 'Default',
})
const handleSwitchLayout = value => {
  tableLayout.value = value
  handleReorder(columnOptions.value.reverse())
}
const columnOrder = computed(() => ['checkbox', ...columnOptions.value.map(option => option.value)])
const handleDeleteLayout = value => {
  if (value.value === tableLayout.value.value) {
    tableLayout.value = {
      value: 'default',
      label: 'Default',
    }
  }
  preSetLayouts.value = preSetLayouts.value.filter(option => option.value !== value.value)
  showAlert.value = true
  alertTitle.value = `${value.value} Layout deleted successfully`
}
const alertTitle = ref('')
const showAlert = ref(false)

Imports

ts
import { HLDataTable, HLDataTableWrapper } from '@platform-ui/highrise'

Props

DataTable

NameTypeDefaultDescription
id *string | undefinedundefinedUnique identifier for the table
columns *DataTableColumn[][]Columns (headers)
data *any[][]Data (rows)
maxHeightstring'800px'Maximum height of the table
rowHeightstring'40px'Height of the row
headerRowHeightstring'40px'Height of the header row
stripedbooleanfalseApplies alternating background colors to rows
horizontalBordersbooleanfalseShow horizontal borders
verticalBordersbooleanfalseShow vertical borders
rowHoverbooleantrueApplies hover effect to table rows
columnHoverbooleantrueApplies hover effect to table columns
highlightSelectedRowbooleanfalseHighlight selected row
highlightedRowsstring[][]Predefined highlighted rows
rowReorderingbooleanfalseEnable row reordering
columnReorderingbooleantrueEnable column reordering
freezedColumns{ left: string[], right: string[] }{ left: [], right: [] }Pins columns to the left or right side of the table
columnResizingbooleantrueEnable column resizing
selectedRowsstring[][]Array of selected row IDs
currentLayout'rows' | 'columns' | 'tableHeight' | undefinedundefinedCurrent layout mode
columnOrderstring[] | undefinedundefinedColumn order
rowClickablebooleanfalseEnable row clickable
rowUniqueIdstring'id'Unique identifier for the row
expandedRowRenderer(row: DataTableRow) => VNodeChildnullRenderer for expanded rows
loadingboolean | { status: boolean; skeletonRows: number }falseShow loading state, default skeletonRows is 10
pageSizenumber20Number of rows per page
tableBodyMinHeightstring | undefinedundefinedMinimum height of the table body

DataTableWrapper

NameTypeDefaultDescription
id *string | undefinedundefinedUnique identifier for the table
showHeaderbooleanfalseShow header
showLayoutOptionsbooleanfalseShow layout options
showRowLayoutOptionbooleanfalseShow row layout option
showColumnLayoutOptionbooleanfalseShow column layout option
showTableHeightLayoutOptionbooleanfalseShow table height layout option
showGlobalSearchbooleanfalseShow global search
columnOptionsColumnOption[][]DataTableColumn options
freezedColumnColumnOption | undefinedundefinedFreezed column
layoutOptionsLayoutOption[][]Layout options
selectedTableLayoutLayoutOption | undefinedundefinedSelected table layout
noOfRowsSelectednumber | undefinedundefinedNumber of rows selected
pageSizenumber | undefinedundefinedNumber of rows per page
tableHeightSelectedOptionstring'default'Table height selected option
userDrivenValuenumber | nullnullUser driven value
maxWidthstring'800px'Maximum width of the table
responsiveColumnWidthbooleanfalseEnable responsive column width
globalFilterstring | undefinedundefinedGlobal filter value
fillParentHeightboolean | undefinedundefinedOccupy the full height of the parent container

Types

ts
export interface DataTableColumn {
  id: string
  header:
    | string
    | {
        text: string
        icon?: VNodeChild
        filterComponent?: VNodeChild
      }
    | VNodeChild
  accessorKey?: string
  size?: number
  minSize?: number // min width of the column in pixels
  maxSize?: number // max width of the column in pixels
  filterFn?: FilterFns
  sortingFn?: SortingFn
  cellFormatter?: (row: DataTableRow<any>) => VNodeChild
  meta?: {
    align?: 'start' | 'end' | 'center' | 'space-around' | 'space-between' | 'space-evenly'
    headerAlign?: 'start' | 'end' | 'center' | 'space-around' | 'space-between' | 'space-evenly'
  }
}
ts
export interface ColumnOption {
  value: string
  label: string
  checked?: boolean
  frozen?: boolean
}
ts
export interface LayoutOption {
  value: string
  label: string
}

Emits

DataTable

NameParametersDescription
@no-data(isEmpty: boolean)Emitted when table has no data
@table-row-clicked(row: DataTableRow, rowInfo: RowInfo)Emitted when row is clicked
@update:column-checked(columnId: string, checked: boolean)Emitted when column visibility changes
@update:column-clicked(columnId: string)Emitted when column is clicked
@update:column-order(order: string[])Emitted when column order changes

DataTableWrapper

NameParametersDescription
@update:cancel(layout: string)Emitted when table layout is cancelled
@update:column-checked(columnId: string, checked: boolean)Emitted when column visibility changes
@update:column-freeze(columnId: string)Emitted when column is frozen
@update:column-reorder(columnIds: string[])Emitted when columns are reordered
@update:column-unfreeze(columnId: string)Emitted when columns are unfrozen
@update:current-layout(layout: string)Emitted when current layout changes
@update:delete-table-layout(layout: string)Emitted when table layout is deleted
@update:global-filter(filter: string)Emitted when global filter changes
@update:page-size(pageSize: number)Emitted when page size changes
@update:row-deselect-all(rowIds: string[])Emitted when all rows are deselected
@update:switch-table-layout(layout: string)Emitted when table layout is switched
@update:table-height-selected-option(option: string)Emitted when table height selected option changes
@update:table-layout-as(layout: string)Emitted when table layout is saved as
@update:table-layout(layout: string)Emitted when table layout changes

Slots

DataTable

NameDescription
no-dataWhen table has no data

DataTableWrapper

NameDescription
headerHeader
footerFooter
header-content-leftHeader content left (left side of the header)
header-content-rightHeader content right (right side of the header)
defaultThe default content slot for table content