Skip to content
Accessibility: Full
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

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>
ts
const columnsResponsive = [
  {
    id: 'firstName',
    header: 'First Name',
    sortingFn: 'alphanumeric',
    accessorKey: 'firstName',
    size: 30, // this will occupy 30% of the table width
    meta: {
      align: 'left',
      headerAlign: 'left',
    },
  },
  {
    id: 'age',
    header: 'Age',
    accessorKey: 'age',
    sortingFn: 'alphanumeric',
    size: 10, // this will occupy 10% of the table width
    meta: {
      align: 'right',
      headerAlign: 'right',
    },
  },
  {
    id: 'rating',
    accessorKey: 'rating',
    header: 'Rating',
    size: 30, // this will occupy 30% of the table width
    meta: {
      align: 'center',
      headerAlign: 'start',
    },
  },
  {
    id: 'DOB',
    accessorKey: 'DOB',
    size: 30, // this will occupy 30% of the table width
    meta: {
      align: 'right',
    },
    header: 'DOB',
  },
]

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.

The search placeholder can be customized using the search-placeholder prop on the HLDataTableWrapper.

Search table data
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"
  search-placeholder="Search table data"
  @update:global-filter="updateGlobalFilter"
>
  <HLDataTable ref="tableInstance" id="table-with-search" :columns="columns" :data="data" max-height="auto" :page-size="6"> </HLDataTable>
</HLDataTableWrapper>
ts
import { ref } from 'vue'

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. This can be controlled by the column-hover prop.

ID

First Name

Age

Progress

Rating

DOB

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>
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',
  },
]

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',
  },
])

Column Resize

Drag the column header to adjust the column width. This can be controlled by the column-resizing prop.

ID

First Name

Age

Progress

Rating

DOB

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>
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',
  },
]

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',
  },
])

Multi Row Header

More than one row in the header can be used to display custom content.

Info

DOB

Progress

Rating

Name

Sub text

20%

Age

html
<script setup lang="ts">
  import { DataTableColumn, HLContentWrap, HLDataTable, HLDataTableWrapper, HLIcon, HLProgress, HLSpace } from '@platform-ui/highrise'
  import AgeFilter from './AgeFilter.vue'
  import DateFilter from './DateFilter.vue'
  import { MaterialIdentityPlatformIcon } from '@gohighlevel/ghl-icons/24/material/rounded'
  import { CalendarIcon, Star01Icon } from '@gohighlevel/ghl-icons/24/outline'
  import MultiRowName from './MultiRowName.vue'
  import { h, ref } from 'vue'
  import commondata from './table-data'

  const data = ref<any[]>(commondata)
  const tableInstance = ref<any>(null)
  const sortedColumn = ref<string>('')
  const sortDir = ref<string>('')

  function setSortSortFilter(id, desc) {
    sortedColumn.value = id
    sortDir.value = desc ? 'desc' : 'asc'
    tableInstance.value.table.setSorting([{ id, desc }])
  }
  function resetSortSortFilter() {
    sortedColumn.value = null
    sortDir.value = null
    tableInstance.value.table.setSorting([])
  }
  function setFilterSortFilter(id, filterFn, value) {
    const column = tableInstance.value.table.getColumn(id)
    column.columnDef.filterFn = filterFn
    column.setFilterValue(value)
  }
  function clearFilterSortFilter(id) {
    const column = tableInstance.value.table.getColumn(id)
    column.setFilterValue(undefined)
  }

  const multiRowColumns: DataTableColumn[] = [
    {
      id: 'id',
      header: ' ',
      columns: [
        {
          id: 'id',
          header: ' ',
          accessorKey: 'id',
          size: 50,
          cellFormatter: ({ row }) => {
            return row.original.id
          },
          meta: {
            headerAlign: 'start',
          },
        },
      ],
    },
    {
      id: 'info',
      header: {
        text: 'Info',
        icon: MaterialIdentityPlatformIcon,
      },
      columns: [
        {
          id: 'firstName',
          header: () =>
            h(MultiRowName, {
              onSetFilter: setFilterSortFilter,
              onClearFilter: clearFilterSortFilter,
              onSetSort: setSortSortFilter,
              sortedColumn,
              sortDir,
              id: 'name-filter',
              onResetSort: resetSortSortFilter,
            }),
          sortingFn: 'alphanumeric',
          accessorKey: 'firstName',
          size: 200,
          meta: {
            align: 'start',
            headerAlign: 'start',
          },
        },
        {
          id: 'age',
          header: {
            text: 'Age',
            filterComponent: h(AgeFilter, {
              onSetFilter: setFilterSortFilter,
              onClearFilter: clearFilterSortFilter,
              onSetSort: setSortSortFilter,
              sortedColumn,
              sortDir,
              id: 'age-filter',
              onResetSort: resetSortSortFilter,
            }),
          },
          accessorKey: 'age',
          filterFn: 'greaterThan',
          sortingFn: 'alphanumeric',

          meta: {
            align: 'end',
            headerAlign: 'end',
          },
        },
      ],
      meta: {
        headerAlign: 'start',
      },
    },
    {
      id: 'DOB',
      header: {
        text: 'DOB',
        icon: CalendarIcon,
        filterComponent: h(DateFilter, {
          onSetFilter: setFilterSortFilter,
          onClearFilter: clearFilterSortFilter,
          onSetSort: setSortSortFilter,
          sortedColumn,
          sortDir,
          id: 'date-filter',
          onResetSort: resetSortSortFilter,
        }),
      },
      columns: [
        {
          id: 'DOB',
          accessorKey: 'DOB',
          meta: {
            align: 'end',
            headerAlign: 'end',
          },
          size: 200,
          header: ' ',
        },
      ],
    },
    {
      id: 'progress',
      header: 'Progress',
      columns: [
        {
          id: 'progress',
          header: ' ',
          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',
      header: 'Rating',
      columns: [
        {
          id: 'rating',
          accessorKey: 'rating',
          size: 200,
          header: ' ',
          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 })
          },
        },
      ],
    },
  ]
</script>

<template>
  <HLDataTableWrapper id="multi-row-table-wrapper">
    <HLDataTable id="multi-row-table" ref="tableInstance" :columns="multiRowColumns" :data="data" headerRowHeight="auto" :page-size="10" />
  </HLDataTableWrapper>
</template>
html
<script setup lang="ts">
  import { HLTag, HLText } from '@platform-ui/highrise'
  import NameFilter from './NameFilter.vue'
  import { MaterialArrowUpwardFillIcon } from '@gohighlevel/ghl-icons/24/material/rounded'
  import { Ref } from 'vue'

  interface Props {
    onSetFilter: (id: string, filterFn: any, value: any) => void
    onClearFilter: (id: string) => void
    onSetSort: (id: string, desc: boolean) => void
    onResetSort: () => void
    sortedColumn: Ref<string>
    sortDir: Ref<string>
    id?: string
  }

  defineProps<Props>()
</script>

<template>
  <div class="flex flex-col text-start">
    <span class="flex items-center justify-between">
      <HLText size="lg" weight="medium">Name</HLText>
      <NameFilter
        :id="id || 'age-filter'"
        :onSetFilter="onSetFilter"
        :onClearFilter="onClearFilter"
        :onSetSort="onSetSort"
        :sortedColumn="sortedColumn"
        :sortDir="sortDir"
        :onResetSort="onResetSort"
      />
    </span>
    <span class="flex items-center justify-start gap-2">
      <HLText size="lg" weight="semibold">Sub text</HLText>
      <HLTag color="success" :bordered="true">
        <template #icon>
          <component :is="MaterialArrowUpwardFillIcon" />
        </template>
        20%
      </HLTag>
    </span>
  </div>
</template>

Infinite Scroll

Load more data on scroll while listening to the @scroll event.

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. This can be controlled by the column-reordering prop.

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

Make columns sticky to the the table. This can be controlled by the freezed-columns prop.

ID

First Name

Age

Progress

Rating

DOB

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>
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',
  },
]

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',
  },
])

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>
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([])
}
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
<template>
  <HLDataTableWrapper id="column-filter-table-wrapper">
    <HLDataTable ref="tableInstance" id="column-filter-table" :columns="columns" :data="data" />
  </HLDataTableWrapper>
</template>
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)
}
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. This will be displayed when the table data prop is an empty array.

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. This will be displayed when the table data prop is an empty array.

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. This can be controlled by 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 (Custom slot)

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="(row)=>h('div',{class: 'bg-gray-200 p-2'}, row.data)"
    >
    </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()()
}

Nested Row Expand

This is useful when you have a nested structure of data.

[Note]

Don't pass the expandedRowRenderer prop to the table to get the nested data as per the columns.

Last Name

ID

First Name

Age

Progress

Rating

DOB

html
<HLContentWrap>
  <HLDataTableWrapper>
    <HLDataTable
      ref="tableInstance"
      :columns="columns"
      :data="date"
      :horizontal-borders="true"
      :vertical-borders="true"
      :row-hover="false"
      :column-hover="false"
      max-height="900px"
    >
    </HLDataTable>
  </HLDataTableWrapper>
</HLContentWrap>
ts
const columns = [
  {
    id: 'lastName',
    header: 'Last Name',
    accessorKey: 'lastName',
    size: 250,
    meta: {
      align: 'left',
      headerAlign: 'left',
    },
    cellFormatter: ({ row }) => {
      return h('div', { class: 'flex items-center' }, [
        h('div', { style: { paddingLeft: `${row.depth * 32}px` } }, null),
        row.originalSubRows &&
          h(
            'div',
            { style: { paddingRight: `12px` } },
            h(
              HLIcon,
              {
                id: 'expand-button',
                size: '20',
                class: 'cursor-pointer',
                onClick: () => {
                  row.getToggleExpandedHandler()()
                },
              },
              row.getIsExpanded() ? ChevronUpIcon : ChevronDownIcon
            )
          ),
        h(
          HLText,
          {
            size: 'lg',
            weight: 'medium',
          },
          row.original.lastName
        ),
      ])
    },
  },
  {
    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: 'Jerel',
    lastName: 'Rath',
    age: 38,
    rating: 5,
    status: 'vilicus',
    progress: 19,
    toggle: false,
    DOB: '04/11/1995',
    expandData: [
      {
        id: 7,
        firstName: 'Darius',
        lastName: 'Abshire',
        age: 18,
        rating: 1,
        status: 'neque',
        progress: 37,
        toggle: false,
        DOB: '27/11/1970',
        expandData: [
          {
            id: 22,
            firstName: 'Jayden',
            lastName: 'Padberg',
            age: 55,
            rating: 3,
            status: 'cumque',
            progress: 99,
            toggle: false,
            DOB: '08/04/1974',
          },
          {
            id: 23,
            firstName: 'Chyna',
            lastName: 'Bruen',
            age: 25,
            rating: 5,
            status: 'laboriosam',
            progress: 46,
            toggle: false,
            DOB: '15/11/1997',
          },
          {
            id: 24,
            firstName: 'Shanon',
            lastName: 'Sauer',
            age: 24,
            rating: 3,
            status: 'dolor',
            progress: 80,
            toggle: true,
            DOB: '04/05/1947',
          },
        ],
      },
      {
        id: 8,
        firstName: 'Damon',
        lastName: 'Frami',
        age: 25,
        rating: 2,
        status: 'corona',
        progress: 37,
        toggle: false,
        DOB: '14/10/1976',
      },
      {
        id: 9,
        firstName: 'Margret',
        lastName: 'Maggio',
        age: 24,
        rating: 3,
        status: 'voluptatem',
        progress: 60,
        toggle: true,
        DOB: '12/08/1969',
      },
    ],
  },
  {
    id: 2,
    firstName: 'Avery',
    lastName: 'Pagac',
    age: 67,
    rating: 1,
    status: 'candidus',
    progress: 0,
    toggle: false,
    DOB: '16/08/1996',
  },
  {
    id: 3,
    firstName: 'Jessica',
    lastName: 'Bailey',
    age: 24,
    rating: 3,
    status: 'dens',
    progress: 74,
    toggle: true,
    DOB: '02/03/1980',
  },
  {
    id: 4,
    firstName: 'Emmalee',
    lastName: 'Anderson',
    age: 18,
    rating: 2,
    status: 'cognatus',
    progress: 92,
    toggle: true,
    DOB: '09/02/1994',
  },
  {
    id: 5,
    firstName: 'Christian',
    lastName: 'Ritchie',
    age: 53,
    rating: 5,
    status: 'strenuus',
    progress: 100,
    toggle: true,
    DOB: '08/06/1963',
  },
])

Row Reorder

Reorder rows by dragging and dropping them. This can be controlled by the row-reordering prop.

ID

First Name

Age

Progress

Rating

DOB

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>
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',
  },
]

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',
  },
])

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(
        'div',
        { class: 'flex items-center justify-center h-full' },
        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. This can be controlled by the striped prop.

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

Adding state to the data will add a border to the cell.

ID

First Name

Age

Progress

Rating

DOB

html
<HLDataTableWrapper id="table-cell-borders-table-wrapper" max-width="1000px">
  <HLDataTable
    ref="tableInstance"
    id="table-cell-borders-table"
    :columns="columns"
    :data="cellScenariosData"
    :horizontal-borders="false"
    :vertical-borders="false"
    :page-size="6"
    max-height="900px"
  >
  </HLDataTable>
</HLDataTableWrapper>
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',
    },
  },
]

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

html
<HLDataTableWrapper id="table-cell-borders-table-wrapper" max-width="1000px">
  <HLDataTable
    ref="tableInstance"
    id="table-cell-borders-table"
    :columns="columns"
    :data="data"
    :horizontal-borders="false"
    :vertical-borders="false"
    :page-size="6"
    max-height="900px"
  >
  </HLDataTable>
</HLDataTableWrapper>
ts
const columns = [
  {
    id: 'lastName',
    header: 'Last Name',
    accessorKey: 'lastName',
    size: 150,
    meta: {
      align: 'left',
      sticky: true,
    },
  },
  {
    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',
    },
  },
]
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)

Table CRUD

A table component that allows you to perform CRUD operations on the data.

Search

First Name

Last Name

Age

Rating

Date

html
<script setup lang="ts">
  import { ArrowDownIcon, ArrowUpIcon, FilterLinesIcon } from '@gohighlevel/ghl-icons/24/outline'
  import {
    HLButton,
    HLDataTable,
    HLDataTableWrapper,
    HLDatePicker,
    HLDivider,
    HLDropdown,
    HLEmpty,
    HLIcon,
    HLPopover,
    HLTable,
    HLTag,
    HLText,
  } from '@platform-ui/highrise'
  import { computed, h, nextTick, ref, useAttrs, watch } from 'vue'
  import TableCrudLeft from './TableCrudLeft.vue'
  import TableCrudRight from './TableCrudRight.vue'

  const globalFilter = ref('')
  const tableInstance = ref<{ table: HLTable<any> }>()
  const data = ref([
    {
      id: 1,
      firstName: 'Nikhil',
      lastName: 'Mara',
      age: 53,
      rating: 2,
      DOB: 1762021800000,
      progress: 32,
    },
    {
      id: 2,
      firstName: 'Nathanial',
      lastName: 'Johnson',
      age: 71,
      rating: 5,
      DOB: 1757183400000,
      progress: 48,
    },
  ])
  // Global filter change handler
  const handleGlobalFilterChange = (value: string) => {
    globalFilter.value = value
    tableInstance.value?.table?.setGlobalFilter(value)
  }

  const args: any = useAttrs()

  const defaultColumnOptions = ref<any[]>([
    {
      value: 'name',
      label: 'Name',
      expand: false,
      options: [
        { value: 'firstName', label: 'First Name', checked: true, frozen: true },
        { value: 'lastName', label: 'Last Name', checked: true },
      ],
    },
    {
      value: 'others',
      label: 'Others',
      expand: false,
      options: [
        { value: 'age', label: 'Age', checked: true },
        { value: 'rating', label: 'Rating', checked: true },
        { value: 'DOB', label: 'DOB', checked: true },
      ],
    },
  ])

  // Column re ordering
  const columnOptions = ref(defaultColumnOptions.value.flatMap(option => option.options || [option]) as any[])

  const handleColumnOrder = (columnIds: string[]) => {
    columnOptions.value = columnIds.map((id: string) => columnOptions.value.find(option => option.value === id))
  }

  const handleFilterChange = (columnId: string, filterValues: string[] | number[]) => {
    const column: any = tableInstance.value?.table.getColumn(columnId)
    column.columnDef.filterFn = 'containsInArray' as any
    if (filterValues.length === 0) {
      column.setFilterValue(undefined)
      return
    }
    column.setFilterValue(filterValues)
  }

  const handleSort = (sortObj: any) => {
    tableInstance.value?.table?.setSorting(sortObj)
  }

  const handleColumnOrdering = (columns: any[]) => {
    const remainingColumns = columnOptions.value.filter(option => option.checked == false)
    columnOptions.value = [...columns, ...remainingColumns]
    const columnIds = columnOptions.value.filter(option => option.checked !== false).map(option => option.value)
    tableInstance.value?.table?.setColumnOrder(columnIds)
  }

  const handleColumnChecked = (field: string, checked: boolean) => {
    const tableColumn = tableInstance.value?.table?.getColumn(field)
    tableColumn?.toggleVisibility(checked)
    const column = columnOptions.value.find(option => option.value === field)
    column.checked = checked
  }

  const tableCrudLeftRef = ref<any>(null)

  const callCrud = (columnId: string, value: string, option: any) => {
    if (value === 'filter') {
      tableCrudLeftRef.value?.handleAddFilter(columnId, option)
    } else if (value === 'desc') {
      tableCrudLeftRef.value?.handleSort(columnId, option, 'desc')
    } else {
      tableCrudLeftRef.value?.handleSort(columnId, option, 'asc')
    }
  }

  const getFilterComponent = (columnId: string, label: string) => {
    return () =>
      h(
        HLDropdown,
        {
          id: `${columnId}-dropdown`,
          options: [
            { key: 'asc', label: 'Sort A → Z', selected: false },
            { key: 'desc', label: 'Sort Z → A', selected: false },
            { key: 'filter', label: 'Filter This Column', selected: false },
          ],

          onSelect: value => {
            callCrud(columnId, value, { label: label })
          },
        },
        h(HLIcon, { size: 14, color: 'var(--gray-600)' }, h(FilterLinesIcon))
      )
  }

  const columns = [
    {
      id: 'firstName',
      header: {
        text: 'First Name',
        filterComponent: getFilterComponent('firstName', 'First Name'),
      },
      sortingFn: 'alphanumeric',
      accessorKey: 'firstName',
      size: 100,
      meta: {
        align: 'left',
        headerAlign: 'left',
      },
    },
    {
      id: 'lastName',
      header: {
        text: 'Last Name',
        filterComponent: getFilterComponent('lastName', 'Last Name'),
      },
      accessorKey: 'lastName',
      sortingFn: 'alphanumeric',
      size: 100,
      meta: {
        align: 'left',
        headerAlign: 'left',
      },
    },
    {
      id: 'age',
      header: {
        text: 'Age',
        filterComponent: getFilterComponent('age', 'Age'),
      },
      accessorKey: 'age',
      sortingFn: 'alphanumeric',
      size: 100,
      meta: {
        align: 'right',
        headerAlign: 'right',
      },
    },
    {
      id: 'rating',
      accessorKey: 'rating',
      header: {
        text: 'Rating',
        filterComponent: getFilterComponent('rating', 'Rating'),
      },
      size: 100,
      meta: {
        align: 'center',
        headerAlign: 'start',
      },
    },
    {
      id: 'DOB',
      accessorKey: 'DOB',
      size: 100,
      meta: {
        align: 'right',
      },
      header: {
        text: 'Date',
        filterComponent: () =>
          h(
            HLDropdown,
            {
              id: 'DOB-dropdown',
              options: [
                { key: 'asc', label: 'Sort Newest First' },
                { key: 'desc', label: 'Sort Oldest First' },
                { key: 'filter', label: 'Filter This Column' },
              ],
              showSearch: false,
              showArrow: false,
              onSelect: value => {
                callCrud('DOB', value, { label: 'Date' })
              },
            },
            h(HLIcon, { size: 14, color: 'var(--gray-600)' }, h(FilterLinesIcon))
          ),
      },
      cellFormatter: ({ row }: { row: any }) => {
        return h('div', { style: { textAlign: 'right' } }, new Date(row.original.DOB).toLocaleDateString())
      },
    },
  ]

  const today = new Date().setHours(0, 0, 0, 0)
  const times = [
    { label: 'Yesterday', selected: false, value: today - 24 * 60 * 60 * 1000 },
    {
      label: 'Past Week',
      selected: false,
      value: today - 7 * 24 * 60 * 60 * 1000,
    },
    {
      label: 'Past Month',
      selected: false,
      value: today - 30 * 24 * 60 * 60 * 1000,
    },
    { label: 'Tomorrow', selected: false, value: today + 24 * 60 * 60 * 1000 },
  ]
  const selectedDate = ref<any>(null)
  const selectedDateLabel = computed(() => {
    if (times.find(time => time.value === selectedDate.value)) {
      return times.find(time => time.value === selectedDate.value)?.label
    }
    return selectedDate.value ? new Date(selectedDate.value).toLocaleDateString() : 'Select'
  })
  const handleDateConfirm = () => {
    handleFilterChange('DOB', [selectedDate.value])
    showFilterDropdown.value = false
    setTimeout(() => {
      showFilterDropdown.value = undefined
    }, 100)
  }

  const handleDateCancel = () => {
    handleFilterChange('DOB', [])
    selectedDate.value = null
    showFilterDropdown.value = false
    setTimeout(() => {
      showFilterDropdown.value = undefined
    }, 100)
  }
  const showWatcher = ref(false)
  const showFilterDropdown = ref<boolean | undefined>(undefined)
  watch(showWatcher, newValue => {
    if (newValue) {
      nextTick(() => {
        showFilterDropdown.value = newValue
      })
    } else {
      showFilterDropdown.value = undefined
    }
  })
</script>

<template>
  <div>
    <HLDataTableWrapper
      id="data-table-wrapper-default"
      max-width="1000px"
      :show-global-search="true"
      :show-header="true"
      :global-filter="globalFilter"
      :search-placeholder="args.searchPlaceholder"
      :fill-parent-height="args.fillParentHeight"
      responsive-column-width
      @update:global-filter="handleGlobalFilterChange"
    >
      <template #header-content-left>
        <TableCrudLeft
          ref="tableCrudLeftRef"
          :custom-filter-dropdown-for="['DOB']"
          :data="data"
          :columnOptions="columnOptions"
          @set-sort="handleSort"
          @filter-change="handleFilterChange"
        >
          <template #filter-dropdown="{ onClose, show }">
            <HLPopover
              :show="showFilterDropdown"
              :data-dummy="(showWatcher = show)"
              trigger="click"
              :show-arrow="false"
              @clickoutside="showFilterDropdown = undefined"
            >
              <template #trigger>
                <HLTag size="lg" round closable @close="onClose">
                  <span>
                    Date
                    <HLTag id="filter-tag-group" size="xs" :bordered="false">
                      <HLText size="md" weight="medium"> {{ selectedDateLabel }} </HLText>
                    </HLTag>
                  </span>
                </HLTag>
              </template>
              <div class="p-2 flex flex-col gap-2">
                <div class="flex gap-3">
                  <div class="flex flex-col gap-1 pt-2">
                    <HLButton
                      v-for="time in times"
                      :id="time.label"
                      :key="time.label"
                      size="2xs"
                      variant="secondary"
                      :color="time.value === selectedDate ? 'primary' : 'gray'"
                      @click="selectedDate = time.value"
                    >
                      {{ time.label }}
                    </HLButton>
                  </div>
                  <HLDatePicker id="date-picker-with-side-content" v-model:value="selectedDate" :panel="true" type="date"> </HLDatePicker>
                </div>
                <HLDivider :margin-bottom="false" :margin-top="false" />
                <div class="flex justify-end gap-1">
                  <HLButton id="date-picker-cancel" size="2xs" variant="secondary" @click="handleDateCancel"> Cancel </HLButton>
                  <HLButton id="date-picker-confirm" size="2xs" variant="primary" color="blue" @click="handleDateConfirm">
                    Confirm
                  </HLButton>
                </div>
              </div>
            </HLPopover>
          </template>
          <template #sort-dropdown="{ onClose, isAscending, onToggleSort, sortOptions }">
            <HLTag size="lg" round closable @close="onClose">
              <template #icon>
                <span style="--n-text-color: var(--primary-600)" @click="onToggleSort">
                  <ArrowUpIcon v-if="isAscending" />
                  <ArrowDownIcon v-else />
                </span>
              </template>
              <HLDropdown
                id="add-filter-dropdown"
                :width="240"
                :options="sortOptions"
                :show-arrow="false"
                :multiple="false"
                show-search-highlight
                show-selected-mark
                @select="
                  (key, option) =>
                    callCrud(key, isAscending ? 'asc' : 'desc', option)
                "
              >
                <HLTag id="filter-tag-group" size="xs" :bordered="false">
                  <HLText size="md" weight="medium"> Date {{ isAscending ? 'Newest' : 'Oldest' }} </HLText>
                </HLTag>
              </HLDropdown>
            </HLTag>
          </template>
        </TableCrudLeft>
      </template>
      <template #header-content-right>
        <TableCrudRight
          :columnOptions="columnOptions"
          :defaultColumnOptions="defaultColumnOptions"
          @column-ordering="handleColumnOrdering"
          @column-checked="handleColumnChecked"
        />
      </template>
      <HLDataTable
        id="data-table-default"
        ref="tableInstance"
        v-bind="args"
        :data="data"
        :columns="columns"
        @update:column-order="handleColumnOrder"
      >
        <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>
  </div>
</template>
html
<script setup lang="ts">
  import { MaterialArrowRightIcon } from '@gohighlevel/ghl-icons/24/material/rounded'
  import { Columns03Icon, SearchLgIcon } from '@gohighlevel/ghl-icons/24/outline'
  import { HLButton, HLCheckbox, HLIcon, HLInput, HLPopover, HLTableDragGroup, HLText } from '@platform-ui/highrise'
  import { computed, h, PropType, ref } from 'vue'

  const emit = defineEmits(['column-ordering', 'column-checked'])

  const props = defineProps({
    defaultColumnOptions: {
      type: Array as PropType<any[]>,
      required: true,
    },
    columnOptions: {
      type: Array as PropType<any[]>,
      required: true,
    },
  })

  const styles = {
    '--n-padding': '8px',
    border: '1px solid var(--gray-300)',
    minWidth: '300px',
  }

  const searchColumns = ref('')
  const searchColumnsInput = ref()
  const columnPopoverRef = ref()
  const isPopoverOpen = ref(false)
  const selectOptions = computed(() => {
    return props.columnOptions.filter(column => column.checked)
  })

  const filteredDefaultColumnOptions = computed(() => {
    const filterOptions = props.defaultColumnOptions.map(option => {
      if (searchColumns.value) {
        if (option.options) {
          option.options.forEach((option: any) => {
            option.show = option.label.toLowerCase().includes(searchColumns.value.toLowerCase())
          })
          option.show = option.options.some((option: any) => option.show)
        } else {
          option.show = option.label.toLowerCase().includes(searchColumns.value.toLowerCase())
        }
      } else {
        option.show = true
        if (option.options) {
          option.options.forEach((option: any) => {
            option.show = true
          })
        }
      }
      return option
    })
    return filterOptions
  })

  const handleReorder = (args: any) => {
    emit('column-ordering', args)
  }
  const handleChecked = (field: string, checked: boolean) => {
    emit('column-checked', field, checked)

    setTimeout(() => {
      columnPopoverRef.value?.syncPosition()
    }, 50)
  }

  const handleSelectAll = () => {
    props.columnOptions.forEach(column => {
      handleChecked(column.value, true)
    })
  }

  const handlePopoverUpdate = (show: boolean) => {
    isPopoverOpen.value = show
    if (show) {
      setTimeout(() => {
        searchColumnsInput.value?.focus()
      }, 100)
    }
  }

  const allOptionsChecked = (options: any[]) => {
    return options.every(option => option.checked)
  }

  const someOptionsChecked = (options: any[]) => {
    return options.some(option => option.checked)
  }

  const toggleAllOptions = (options: any[], checked: boolean) => {
    options.forEach(option => {
      if (option.frozen) return
      handleChecked(option.value, checked)
    })
  }
  function highlightSplit(st1: string, st2: string) {
    const index = st1.toLowerCase().indexOf(st2.toLowerCase())
    if (index === -1) return [st1] // substring not found
    const before = st1.slice(0, index)
    const match = st1.slice(index, index + st2.length)
    const after = st1.slice(index + st2.length)
    return [before, match, after]
  }

  const highlightSearched = (label: string) => {
    const searchValue = searchColumns.value
    if (!searchValue) {
      return h('span', {}, label)
    }
    const parts = highlightSplit(label, searchValue).map(part => {
      return h(
        'span',
        {
          style: {
            fontWeight: part.toLowerCase() === searchValue.toLowerCase() ? 'var(--hr-font-weight-bold)' : 'var(--hr-font-weight-regular)',
          },
        },
        part
      )
    })
    return h('span', {}, parts)
  }
</script>

<template>
  <HLPopover
    ref="columnPopoverRef"
    trigger="click"
    placement="bottom"
    :style="styles"
    :show-arrow="false"
    @update:show="handlePopoverUpdate"
  >
    <template #trigger>
      <HLButton id="column-eye-icon" variant="tertiary" size="xs">
        <template #iconLeft>
          <Columns03Icon />
        </template>
        {{ selectOptions.length }}/{{ columnOptions.length }} Columns
      </HLButton>
    </template>
    <div>
      <HLInput id="search-columns" ref="searchColumnsInput" v-model="searchColumns" size="2xs" :prefix-icon="SearchLgIcon as any" />
      <span class="flex items-center gap-2 justify-between p-2">
        <HLText size="sm"> {{ selectOptions.length }} Fields Selected </HLText>
        <HLButton id="select-all-button" variant="text" size="2xs" @click="handleSelectAll">
          Select All ({{ columnOptions.length }})
        </HLButton>
      </span>
      <HLTableDragGroup
        id="column-drag"
        :disabled="searchColumns.length > 0"
        :options="columnOptions.filter(column => column.checked)"
        :search="searchColumns"
        max-height="400px"
        @reorder="handleReorder"
        @update:column-checked="handleChecked"
      />
      <div class="remaining-columns">
        <div v-for="column in filteredDefaultColumnOptions" :key="column.value">
          <div v-if="column.options && column.show">
            <div class="remaining-column--group">
              <span class="cursor-pointer inline-flex" @click="column.expand = !column.expand">
                <HLIcon :size="18" :class="column.expand ? 'rotate-90' : ''"><MaterialArrowRightIcon /></HLIcon>
              </span>
              <HLCheckbox
                id="remaining-column-checkbox"
                :checked="allOptionsChecked(column.options)"
                :indeterminate="
                  !allOptionsChecked(column.options) &&
                  someOptionsChecked(column.options)
                "
                size="xs"
                @update:checked="toggleAllOptions(column.options, $event)"
              >
                <HLText size="sm"> {{ column.label }} </HLText>
              </HLCheckbox>
            </div>
            <div v-for="option in column.options" v-show="column.expand" :key="option.value" class="remaining-column--child-item">
              <HLCheckbox
                v-show="option.show"
                id="remaining-column-checkbox"
                :checked="option.checked"
                :disabled="option.frozen"
                size="xs"
                @update:checked="handleChecked(option.value, $event)"
              >
                <component :is="highlightSearched(option.label)" />
              </HLCheckbox>
            </div>
          </div>
          <div v-else-if="column.show" class="remaining-column--item">
            <HLCheckbox
              id="remaining-column-checkbox"
              :checked="column.checked"
              size="xs"
              @update:checked="handleChecked(column.value, $event)"
            >
              <component :is="highlightSearched(column.label)" />
            </HLCheckbox>
          </div>
        </div>
      </div>
    </div>
  </HLPopover>
</template>
<style lang="scss" scoped>
  .remaining-columns {
    display: flex;
    flex-direction: column;
    gap: 2px;
    .remaining-column--item {
      padding: 4px 8px;
    }
    .remaining-column--child-item {
      padding: 4px 12px 4px 30px;
    }
    .remaining-column--group {
      background-color: var(--primary-50);
      display: flex;
      align-items: center;
      gap: 4px;
      padding: 4px 6px;
    }
  }
</style>
html
<script setup lang="ts">
  import { ArrowDownIcon, ArrowUpIcon, ChevronSelectorVerticalIcon, PlusIcon } from '@gohighlevel/ghl-icons/24/outline'
  import { HLDivider, HLDropdown, HLIcon, HLPopover, HLTag, HLText } from '@platform-ui/highrise'
  import { computed, h, PropType, ref } from 'vue'
  import TableCRUDDropdown from './TableCRUDDropdown.vue'

  const props = defineProps({
    data: {
      type: Array as PropType<any[]>,
      required: true,
    },
    columnOptions: {
      type: Array as PropType<any[]>,
      required: true,
    },
    customFilterDropdownFor: {
      type: Array as PropType<string[]>,
      default: () => [],
    },
  })

  const selectedFilters = ref<any[]>([])
  const selectedSort = ref<{ key: string; label: string } | null>(null)

  const isAscending = ref(true)
  const toggleSort = () => {
    isAscending.value = !isAscending.value
    if (selectedSort.value) {
      emit('set-sort', [{ id: selectedSort.value.key, desc: !isAscending.value }])
    }
  }

  // Sort
  const handleSort = (key: string, option: any, dir: string) => {
    isAscending.value = dir ? dir === 'asc' : isAscending.value
    selectedSort.value = { key: key, label: option.label }
    emit('set-sort', [{ id: key, desc: !isAscending.value }])
  }

  const popoverShow = ref<boolean | undefined>(undefined)

  // show filter open
  const openFilter = (index: number) => {
    if (index > 1 || (index > 0 && selectedSort.value)) {
      popoverShow.value = true
    }
    setTimeout(() => {
      selectedFilters.value[index].show = true
    }, 100)
    setTimeout(() => {
      selectedFilters.value[index].show = undefined
    }, 1000)
  }

  // Filters
  const handleAddFilter = (key: string, option: any) => {
    const index = selectedFilters.value.findIndex(filter => filter.columnId === key)
    if (index !== -1) {
      openFilter(index)
      return
    }
    selectedFilters.value.splice(0, 0, {
      columnId: key,
      label: option.label,
      data: {
        value: option.label,
        options: Array.from(new Set(props.data.map(data => data[key]))).map(option => ({ key: option, label: option })),
      },
    })
    openFilter(0)
  }
  const emit = defineEmits(['filter-change', 'set-sort'])
  const handleFilterChange = (columnId: string, filterValues: string[]) => {
    emit('filter-change', columnId, filterValues)
  }
  const handleTagClose = (columnId: string) => {
    emit('filter-change', columnId, [])
    selectedFilters.value = selectedFilters.value.filter(filter => filter.columnId !== columnId)
  }

  const getOptions = (isSort: boolean = false) => {
    const allOptions = props.columnOptions.map(column => ({
      key: column.value,
      label: column.label,
      selected: isSort
        ? selectedSort.value
          ? selectedSort.value.key === column.value
          : false
        : selectedFilters.value.find(filter => filter.columnId === column.value),
      checked: column.checked,
    }))
    const allCheckedOptions = allOptions.filter(option => option.checked)
    const allUnCheckedOptions = allOptions.filter(option => !option.checked)
    if (allUnCheckedOptions.length > 0) {
      return [...allCheckedOptions, { key: 'unChecked', label: 'Hidden Fields', type: 'header' }, ...allUnCheckedOptions]
    }
    return allOptions
  }

  const options = computed(() => {
    return getOptions()
  })

  const sortOptions = computed(() => [
    {
      label: 'Sort By:',
      type: 'render',
      render: () =>
        h(
          'div',
          {
            class: 'flex items-center justify-between cursor-auto',
            style: { width: '222px', padding: '4px 8px' },
            onClick: e => {
              e.preventDefault()
              e.stopPropagation()
            },
          },
          [
            h(HLText, { size: 'md' }, ['Sort By:']),
            h(HLText, { style: { display: 'inline-flex', gap: '4px' }, size: 'md' }, [
              isAscending.value ? 'Ascending' : 'Descending',
              h(
                'span',
                {
                  class: 'cursor-pointer flex items-center gap-0.5',
                  style: { color: 'var(--primary-600)' },
                  onClick: toggleSort,
                },
                [
                  isAscending.value ? '(A → Z)' : '(Z → A)',
                  h(HLIcon, { size: '14' }, { default: isAscending.value ? ArrowUpIcon : ArrowDownIcon }),
                ]
              ),
            ]),
          ]
        ),
      value: 'sort',
    },
    { type: 'divider', value: 'divider' },
    ...getOptions(true),
  ])

  const handleCloseSort = () => {
    selectedSort.value = null
    emit('set-sort', [])
  }
  const sliceNumber = computed(() => {
    return selectedSort.value ? 1 : 2
  })

  defineExpose({
    handleAddFilter,
    handleSort,
    toggleSort,
  })
</script>
<template>
  <div v-if="selectedSort" class="flex items-center gap-2">
    <slot
      v-if="customFilterDropdownFor.includes(selectedSort.key)"
      name="sort-dropdown"
      :is-ascending="isAscending"
      :sort-options="sortOptions"
      @close="handleCloseSort"
      @toggle-sort="toggleSort"
    >
    </slot>
    <HLTag v-else size="lg" round closable @close="handleCloseSort">
      <template #icon>
        <span style="--n-text-color: var(--primary-600)" @click="toggleSort">
          <ArrowUpIcon v-if="isAscending" />
          <ArrowDownIcon v-else />
        </span>
      </template>
      <HLDropdown
        id="add-filter-dropdown"
        :width="240"
        :options="sortOptions"
        :show-arrow="false"
        :multiple="false"
        show-search-highlight
        show-selected-mark
        @select="handleSort"
      >
        <HLTag id="filter-tag-group" size="xs" :bordered="false">
          <HLText size="md" weight="medium"> {{ selectedSort.label }} {{ isAscending ? 'A -> Z' : 'Z -> A' }} </HLText>
        </HLTag>
      </HLDropdown>
    </HLTag>
  </div>
  <HLDivider v-if="selectedSort" vertical style="height: 14px"></HLDivider>
  <div v-if="selectedFilters.length" class="flex items-center gap-2">
    <div v-for="filter in selectedFilters.slice(0, sliceNumber)" :key="filter.columnId" class="">
      <slot
        v-if="customFilterDropdownFor.includes(filter.columnId)"
        name="filter-dropdown"
        :column-id="filter.columnId"
        :options="filter.data.options"
        :label="filter.label"
        :show="filter.show"
        @select="handleFilterChange(filter.columnId, $event)"
        @close="handleTagClose(filter.columnId)"
      >
      </slot>
      <TableCRUDDropdown
        v-else
        :options="filter.data.options"
        :label="filter.label"
        :show="filter.show"
        @select="handleFilterChange(filter.columnId, $event)"
        @close="handleTagClose(filter.columnId)"
      />
    </div>
    <div v-if="selectedFilters.length > sliceNumber">
      <HLPopover trigger="click" style="max-width: 433px" :show="popoverShow" @clickoutside="popoverShow = undefined">
        <template #trigger>
          <HLTag size="lg" round> +{{ selectedFilters.length - sliceNumber }} </HLTag>
        </template>
        <div style="display: flex; gap: 4px; padding: 8px; flex-wrap: wrap">
          <div v-for="filter in selectedFilters.slice(sliceNumber)" :key="filter.columnId">
            <slot
              v-if="customFilterDropdownFor.includes(filter.columnId)"
              name="filter-dropdown"
              :column-id="filter.columnId"
              :options="filter.data.options"
              :label="filter.label"
              :show="filter.show"
              @select="handleFilterChange(filter.columnId, $event)"
              @close="handleTagClose(filter.columnId)"
            >
            </slot>
            <TableCRUDDropdown
              v-else
              :options="filter.data.options"
              :label="filter.label"
              :show="filter.show"
              @select="handleFilterChange(filter.columnId, $event)"
              @close="handleTagClose(filter.columnId)"
            />
          </div>
        </div>
      </HLPopover>
    </div>
  </div>
  <HLDivider v-if="selectedFilters.length" vertical style="height: 14px"></HLDivider>
  <div class="flex items-center gap-2">
    <HLDropdown
      id="add-filter-dropdown"
      :options="options"
      :show-arrow="false"
      show-search-highlight
      show-selected-mark
      multiple
      @select="handleAddFilter"
    >
      <HLTag round size="lg">
        <template #icon>
          <PlusIcon />
        </template>
        Add Filter
      </HLTag>
    </HLDropdown>
    <HLDropdown
      id="add-filter-dropdown"
      :width="240"
      :options="sortOptions"
      :show-arrow="false"
      :multiple="false"
      show-search-highlight
      show-selected-mark
      @select="handleSort"
    >
      <HLTag round size="lg">
        <template #icon>
          <ChevronSelectorVerticalIcon />
        </template>
        Sort
      </HLTag>
    </HLDropdown>
  </div>
</template>
html
<script setup lang="ts">
  import { HLDropdown, HLTag, HLText } from '@platform-ui/highrise'
  import { computed, PropType, ref, watch } from 'vue'
  const props = defineProps({
    options: {
      type: Array as PropType<any[]>,
      required: true,
    },
    label: {
      type: String,
      default: '',
    },
    show: {
      type: Boolean,
      default: undefined,
    },
  })

  const updatedShow = ref(props.show)
  watch(
    () => props.show,
    newVal => {
      if (newVal) updatedShow.value = newVal
    },
    { immediate: true }
  )

  const updateUpdatedShow = (value: boolean) => {
    updatedShow.value = value
  }

  const emit = defineEmits(['select', 'close'])
  const allOptions = computed(() => {
    if (isAllSelected.value) {
      return [{ label: 'All', key: 'all', selected: isAllSelected.value }, ...props.options.map(option => ({ ...option, selected: false }))]
    } else {
      return [
        { label: 'All', key: 'all', selected: isAllSelected.value },
        ...props.options.map((option: any) => ({
          ...option,
          selected: selectedArray.value.includes(option.label) ? true : false,
        })),
      ]
    }
  })
  const isAllSelected = ref(true)
  let selectedArray = ref<string[]>([])
  const handleSelect = (key: string, option: any) => {
    if (key === 'all') {
      selectedArray.value = []
      isAllSelected.value = true
    } else {
      if (option.selected) {
        selectedArray.value.push(option.label)
      } else {
        selectedArray.value = selectedArray.value.filter(item => item !== option.label)
      }
      isAllSelected.value = false
    }
    if (selectedArray.value.length === 0) {
      isAllSelected.value = true
    }
    emit('select', selectedArray.value)
  }
  const handleClose = () => {
    emit('close')
  }
</script>
<template>
  <HLDropdown
    id="table-crud-dropdown"
    max-height="300px"
    :options="allOptions"
    :show-arrow="false"
    multiple
    show-selected-mark
    :close-on-select="false"
    :show-search-highlight="true"
    :show="updatedShow"
    @select="handleSelect"
    @update:show="updateUpdatedShow"
  >
    <span :key="selectedArray.length">
      <HLTag size="lg" round closable @close="handleClose">
        {{ label }}
        <HLTag id="filter-tag-group" :key="selectedArray.length" size="xs" :bordered="false">
          <HLText size="md" weight="medium">
            {{ isAllSelected ? 'All' : selectedArray[0] }}
            <span v-if="selectedArray.length > 1" :key="selectedArray.length"> , +{{ selectedArray.length - 1 }} </span>
          </HLText>
        </HLTag>
      </HLTag>
    </span>
  </HLDropdown>
</template>

Imports

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

Accessibility

  • Provide a caption or set aria-label / aria-labelledby on HLDataTable to summarize the dataset.
  • Maintain aria-sort on sortable headers and expose selected rows via aria-selected or aria-describedby.
  • When paginating or virtualizing, link the pager with aria-controls and announce counts through helper text referenced by aria-describedby.
  • Pass attributes to be attached to the <table> element to tableAttributes for HLDataTable and tableWrapperAttributes for HLDataTableWrapper.

Props

DataTable

NameTypeDefaultDescription
id *string-Unique identifier for the table
columns *DataDataTableColumn[]-Columns (headers)
data *any[]-Data (rows)
maxHeightstring'800px'Maximum height of the table
rowHeightstring'36px'Height of the row
headerRowHeightstring'36px'Height of the header row
stripedbooleanfalseApplies alternating background colors to rows
horizontalBordersbooleantrueShow horizontal borders
verticalBordersbooleantrueShow 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'undefinedCurrent layout mode
columnOrderstring[]undefinedColumn order
rowClickablebooleanfalseEnable row clickable
rowUniqueIdstring'id'Unique identifier for the row
expandedRowRenderer(data: any) => VNodeChildnullRenderer for expanded rows
loadingboolean | { status: boolean; skeletonRows: number }falseShow loading state, default skeletonRows is 10
pageSizenumber20Number of rows per page
tableBodyMinHeightstringundefinedMinimum height of the table body
restrictRowReorderingUptonumberundefinedExclude rows reordering from 0 to this index
tableAttributesHTMLAttributes{}Additional HTML attributes for the table element

DataTableWrapper

NameTypeDefaultDescription
id *string-Unique 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
searchPlaceholderstring'Search'Placeholder text for the global search input field
columnOptionsColumnOption[][]Column options
freezedColumnColumnOptionundefinedFreezed column
layoutOptionsLayoutOption[][]Layout options
selectedTableLayoutLayoutOptionundefinedSelected table layout
noOfRowsSelectednumberundefinedNumber of rows selected
pageSizenumberundefinedNumber 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, if this is set true then size of each column will rendered in %
globalFilterstringundefinedGlobal filter value
fillParentHeightbooleanundefinedOccupy the full height of the parent container
tableWrapperAttributesHTMLAttributes{}Additional HTML attributes for the table wrapper element

A11y

Add aria-label and aria-describedby labels to tableAttributes or tableWrapperAttributes to maintain accessibility and WCAG compliance. Refer to guidelines here

Types

ts
export interface DataTableColumn {
  id: string
  header:
    | string
    | {
        text: string
        icon?: VNodeChild
        filterComponent?: VNodeChild
      }
    | VNodeChild
  accessorKey?: string
  size?: number // if responsiveColumnWidth is `true` then size will treated as `%` else in `pixels`
  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
@update:row-reordered(draggedRowIndex: number, targetIndex: number)Emitted when row is reordered

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