Skip to content
On this page

Datatable

One of the most valued components of Vanilla Components, Datatables to display your bulk resources, perform actions filter results, search & many more features, everything is perfectly designed with Tailwind CSS.

The data table component includes a lot of features and we plan to include more with time without making it too complicated to keep it as much vanilla as possible 😄 . feel free to explore most of them but some highlights:

  • Data Fetched via Fetch just provide an endpoint
  • Bulk Selection with persistence on Local Storage
  • Bulk Actions
  • Actions with Hooks, totally configurable
  • Filters with more than 5 different types of components to use & Persisted on Local Storage
  • Copy Friendly Filters Link to share with your pals
  • Handcrafts with TailwindCSS + Dark Mode
  • Settings to Hide/Show Pages
  • Multi-Column sorting
  • Fully Slotable
  • Dynamic Slots for Rows, Filters & Actions
  • Loading Skeletons
  • Search
  • Local Storage Persistence for Selected, Filters & Columns visibility
  • Pooling & Pooling Only After Actions
  • Translatable
  • Laravel Adapter
  • Hybridly + InertiaJS Adapter

But enough talk, let's see it in action!

Preview & Playground 🖼️

Here you may find a preview of the component, with error & possible variants.

🏄 Click to expand the code
vue
<script setup lang="ts">
import { Datatable } from '@flavorly/vanilla-components'
import TrashIcon from '~icons/heroicons/trash'

const onGenericEvent = (e) => {
  console.log('Datatable Event dispatched', e)
}

// Options
const options = {
  allSelectable: true,
  isSearchHidden: false,
  selectable: true,
  searchable: true,
  refreshable: true,
  manageSettings: true,
  showTotalItems: true,
  compact: false,
  striped: false,
}

// Columns
const columns = [
  {
    name: 'id',
    label: 'ID',
    sortable: true,
    native: true,
    hidden: false,
    defaultSortAs: 'desc',
    raw: false,
  },
  {
    name: 'gateway',
    label: 'Gateway',
    sortable: true,
    native: true,
    hidden: false,
    defaultSortAs: undefined,
    raw: false,
  },
  {
    name: 'amount',
    label: 'Amount',
    sortable: true,
    native: true,
    hidden: false,
    defaultSortAs: undefined,
    raw: false,
  },
  {
    name: 'status',
    label: 'Status',
    sortable: true,
    native: true,
    hidden: false,
    defaultSortAs: undefined,
    raw: false,
  },
]

// Actions
const actions = [
  {
    name: 'delete-items',
    label: 'Deleted Items',
    permissions: {
      execute: true,
      view: true,
    },
    before: {
      confirm: {
        enable: true,
        title: 'Delete Payments?',
        subtitle: 'Something',
        text: 'Are you sure you want to :name all the :itemsSelected selected payments? Please confirm.',
        icon: undefined,
        confirmButton: 'Yes, go on',
        cancelButton: 'No, take me back.',
        safe: true,
        classes: {
          title: '',
          text: '',
          icon: '',
        },
      },
      callback: (action) => {
        console.log('Im being executed before on the action', action)
      },
    },
    after: {
      clearSelected: true,
      resetFilters: false,
      pooling: {
        enable: false,
        interval: 5,
        during: 120,
        stopWhenDataChanges: false,
      },
      callback: (action) => {
        console.log('Im being executed after')
      },
    },
  },
]

const filters = [
  {
    name: 'id',
    label: 'Filter by ID',
    component: 'VanillaInput',
    placeholder: 'Ex: 1,2,3',
    value: 5,
    defaultValue: 5,
    options: [],
  },
  {
    name: 'amount',
    label: 'Filter by Amount',
    component: 'VanillaInput',
    placeholder: 'Amount',
    value: undefined,
    defaultValue: undefined,
    options: [],
  },
  {
    name: 'gateway',
    label: 'Gateway',
    component: 'VanillaSelect',
    placeholder: 'Payment Gateway',
    value: undefined,
    defaultValue: undefined,
    options: [
      { value: 'Paypal', text: 'Paypal' },
      { value: 'Bitcoin', text: 'Bitcoin' },
      { value: 'Ethereum', text: 'Ethereum' },
    ],
  },
]

// Translations
const translations = {
  title: 'Payments',
  subtitle: 'Check your latest payments here',
  resource: 'Payment',
  resources: 'Payments',

  actionsButton: 'Actions',
  actionsSelectedRows: 'With :rows selected',

  actionConfirmTitle: 'Confirm your action',
  actionConfirmText: 'Are you sure you want to :name on the :itemsSelected item(s) selected? Please confirm',
  actionConfirmButton: 'Yes, I\'v Confirmed',
  actionCancelButton: 'Nah, Cancel',

  search: 'Search',
  searchPlaceholder: 'Search your latest Payments',

  selectRows: 'You currently have :rows payments selected ',
  selectedUndo: 'Deselect',
  selectAllOr: ' or ',
  selectAllMatching: 'Select :rows matching',
  selectAllMatchingUndo: 'Undo select all :rows',

  filters: 'Filters',
  filtersWithEmptyData: 'Oops, seems like there is no records after filtering',
  filtersReset: 'Reset Filters',

  recordsEmpty: 'Seems like its quiet here! No Records were found',
  settingsPerPage: ':count Items per page',

  showingFrom: 'Showing :from to :to of :total results',
  nextPage: 'Next',
  previousPage: 'Previous',
}

// Per Page Options
const perPageItemsOptions = [
  { value: 5, text: '5 Items per page' },
  { value: 10, text: '10 Items per page' },
  { value: 50, text: '50 Items per page' },
  { value: 100, text: '100 Items per page' },
  { value: 300, text: '300 Items per page' },
]

const poolingOptions = {
  enable: false,
  interval: 5,
  during: 60,
  stopWhenDataChanges: true,
}

// const fetchEndpoint = new URL('/datatables', typeof window === 'undefined' || typeof document === 'undefined' ? undefined : document.baseURI).href

const fetchEndpoint = '/datatables'

// A function to get the current url plus a endpoint
const config = {
  name: 'ExampleDatatable',
  primaryKey: 'id',
  fetchEndpoint,
  columns,
  actions,
  filters,
  options,
  translations,
  perPageOptions: perPageItemsOptions,
  pooling: poolingOptions,
}
</script>

<template>
  <PreviewWrapper>
    <div>
      <Datatable
        :config="config"
        @fetched-success="onGenericEvent"
        @fetch-error="onGenericEvent"
        @opened-settings="onGenericEvent"
        @opened-filters="onGenericEvent"
      >
        <template #rowId="{ result, resultRaw }">
          <span><u>{{ result }}</u></span>
        </template>

        <template #actionDeleteItems="{ action }">
          <TrashIcon class="h-4 w-4" /><span>{{ action.label }}</span>
        </template>
      </Datatable>
    </div>
  </PreviewWrapper>
</template>

Props 📥

Props available for this component extending the default variant & global props.

PropDescriptionAccepted ValuesDefault
configArray of configuration to the table[Object]{}
fetchDataPromise / Function to fetch data[Function, undefined]undefined
onActionExecutedCallbackHook/Function after Action executed[Function, undefined]undefined
onExceptionCallbackHook/Function after Fetch Exception[Function, undefined]undefined

Configuration 🕹️

Here you will find a sample of JSON configuration that you can pass on prop config. For a more in-depth example please check the demo available on the snippet.

🏄 Click to check the demo configuration
json
{
  "name":"ExampleDatatable",
  "primaryKey":"id",
  "columns":[
    {
      "name":"id",
      "label":"ID",
      "sortable":true,
      "native":true,
      "hidden":false,
      "defaultSortAs":"desc",
      "raw":false
    },
    {
      "name":"gateway",
      "label":"Gateway",
      "sortable":true,
      "native":true,
      "hidden":false,
      "raw":false
    },
    {
      "name":"amount",
      "label":"Amount",
      "sortable":true,
      "native":true,
      "hidden":false,
      "raw":false
    },
    {
      "name":"status",
      "label":"Status",
      "sortable":true,
      "native":true,
      "hidden":false,
      "raw":false
    }
  ],
  "actions":[
    {
      "name":"delete-items",
      "label":"Deleted Items",
      "permissions":{
        "execute":true,
        "view":true
      },
      "before":{
        "confirm":{
          "enable":true,
          "title":"Delete Payments?",
          "subtitle":"Something",
          "text":"Are you sure you want to :name all the :itemsSelected selected payments? Please confirm.",
          "confirmButton":"Yes, go on",
          "cancelButton":"No, take me back.",
          "safe":true,
          "classes":{
            "title":"",
            "text":"",
            "icon":""
          }
        }
      },
      "after":{
        "clearSelected":true,
        "resetFilters":false,
        "pooling":{
          "enable":false,
          "interval":5,
          "during":120,
          "stopWhenDataChanges":false
        }
      }
    }
  ],
  "filters":[
    {
      "name":"id",
      "label":"Filter by ID",
      "component":"VanillaInput",
      "placeholder":"Ex: 1,2,3",
      "value":5,
      "defaultValue":5,
      "options":[

      ],
      "rules":[

      ]
    },
    {
      "name":"amount",
      "label":"Filter by Amount",
      "component":"VanillaInput",
      "placeholder":"Amount",
      "options":[

      ]
    },
    {
      "name":"gateway",
      "label":"Gateway",
      "component":"VanillaSelect",
      "placeholder":"Payment Gateway",
      "options":[
        {
          "value":"Paypal",
          "text":"Paypal"
        },
        {
          "value":"Bitcoin",
          "text":"Bitcoin"
        },
        {
          "value":"Ethereum",
          "text":"Ethereum"
        }
      ]
    }
  ],
  "options":{
    "selectable":true,
    "searchable":true,
    "refreshable":true,
    "manageSettings":true,
    "showTotalItems":true,
    "compact":false,
    "striped":false
  },
  "translations":{
    "title":"Payments",
    "subtitle":"Check your latest payments here",
    "resource":"Payment",
    "resources":"Payments",
    "actionsButton":"Actions",
    "actionsSelectedRows":"With :rows selected",
    "actionConfirmTitle":"Confirm your action",
    "actionConfirmText":"Are you sure you want to :name on the :itemsSelected item(s) selected? Please confirm",
    "actionConfirmButton":"Yes, I'v Confirmed",
    "actionCancelButton":"Nah, Cancel",
    "search":"Search",
    "searchPlaceholder":"Search your latest Payments",
    "selectRows":"You currently have :rows payments selected ",
    "selectedUndo":"Deselect",
    "selectAllOr":" or ",
    "selectAllMatching":"Select :rows matching",
    "selectAllMatchingUndo":"Undo select all :rows",
    "filters":"Filters",
    "filtersWithEmptyData":"Oops, seems like there is no records after filtering",
    "filtersReset":"Reset Filters",
    "recordsEmpty":"Seems like its quiet here! No Records were found",
    "settingsPerPage":":count Items per page",
    "showingFrom":"Showing :from to :to of :total results",
    "nextPage":"Next",
    "previousPage":"Previous"
  },
  "perPageOptions":[
    {
      "value":5,
      "text":"5 Items per page"
    },
    {
      "value":10,
      "text":"10 Items per page"
    },
    {
      "value":50,
      "text":"50 Items per page"
    },
    {
      "value":100,
      "text":"100 Items per page"
    },
    {
      "value":300,
      "text":"300 Items per page"
    }
  ],
  "pooling":{
    "enable":false,
    "interval":5,
    "during":60,
    "stopWhenDataChanges":true
  }
}

API Response & Requests 🏝️

Server Side Response

While we don't dictate how you should handle your data response, you are still free to override the default fetchData method. You are still required to follow some patterns when it comes to pagination.

While making this library my main use case was to use it along with Laravel ® + InertiaJS ®. Below, you find demo data of what your API should reply for the default fetchData, you can also find more details in the source code of the MirageJS mock server on this repository:

🏄 Click to check the demo response
json
{
  "data":[
    {
      "status":"Completed",
      "amount":"177€",
      "gateway":"Bitcoin",
      "id":"0"
    },
    {
      "status":"Pending",
      "amount":"36€",
      "gateway":"Credit Card",
      "id":"1"
    },
    {
      "status":"Refunded",
      "amount":"47€",
      "gateway":"Bitcoin",
      "id":"2"
    },
    {
      "status":"Pending",
      "amount":"47€",
      "gateway":"Bitcoin",
      "id":"3"
    },
    {
      "status":"Completed",
      "amount":"67€",
      "gateway":"Bitcoin",
      "id":"4"
    }
  ],
  "links":{
    "next":"/datatables/?page=2",
    "previous":null,
    "pages":[
      {
        "url":"/datatables/?page=1",
        "label":1,
        "active":true
      },
      {
        "url":"/datatables/?page=2",
        "label":2,
        "active":false
      },
      {
        "url":"/datatables/?page=3",
        "label":3,
        "active":false
      },
      {
        "url":"/datatables/?page=4",
        "label":4,
        "active":false
      },
      {
        "url":"/datatables/?page=5",
        "label":5,
        "active":false
      },
    ]
  },
  "meta":{
    "current_page":1,
    "from":0,
    "to":5,
    "total":25
  },
}

The response must include :

KeyDescription
dataArray of of items, representing your resources
linksnext, previous, and array of pages and their links
metaTotal number of records, current, etc

Client Side Request

All requests made by the table are using Fetch, if you want to use your own adapter you can override the default fetchData method. Below, you will find what parameters are sent to the server-side so you can build any server-side adapters.

🏄 Click to check the demo request
json
{
  "search":"An Amazing Search query",
  "perPage":5,
  "selected":[
    "1",
    "2",
    "3",
  ],
  "selectedAll":false,
  "filters":{
    "id":"10"
  },
  "sorting":[
    {
      "column":"gateway",
      "direction":"desc",
      "sortedTimes":1
    }
  ],
  "action":"delete-items"
}

Request Params explained :

KeyDescription
searchSearch Query when the user searches
perPageHow many items to show for the page / take
selectedArray of the primary keys selected
selectedAllIf all items matching the criteria are selected
filtersArray with Key, Value of the applied filters
sortingArray with sorting's applied & their direction
actionAction selected to perform bulk actions

Events 🚇

Here you may find the events emitted by this component.

EventDescriptionValue
fetchedSuccessAPI call finishesArray<Results>
fetchErrorAPI call failsException
actionExecutedAction was executedAction
sortingUpdatedSorting was updatedColumn
navigateToPageNavigates/switch pagesString<Page>
filtersSavedFilters was applied & savedArray<Filter>
mountedDatatable was initializedBoolean
unmountedDatatable was destroyedBoolean
searchSearch was performedString
openedFiltersFilters Dialog OpenedBoolean
openedSettingsSettings Dialog OpenedBoolean

Slots 🧬

Current slots available for this component are the following:

Besides, the regular static slots, data table also provides dynamic slots for rows columns & actions

Work in progress

Please keep in mind some slots override can break functionality is this is a work in progress.

Slot Dynamic row<ColumnName>

Following the camel case standard, you can slot each cell of your table by using the column name in camel case prefixed with row. Ex: rowId, rowName, rowDate, rowCreatedAt, etc.

AttributeDescriptionType
columnColumn definitionObject
resultActual item value for this column[String, Any]
resultRawWhole row raw item from the APIObject

Slot Dynamic action<actionName>

Following the camel case standard, you can slot each action of your table by using the column name in camel case prefixed with action. Ex: rowDelete, rowUpdate, rowDoSomethingCrazy, etc.

AttributeDescriptionType
actionAction ObjectObject
selectActionFunction to Select the actionFunction

Slot headerActions

Overrides the datatable actions.

AttributeDescriptionType
actionsArray with all the actionsArray
hasActionsIf any actions are availableBoolean
hasAnyItemsSelectedIf any items are selectedBoolean
onActionSelectedFunction when a action is selectedFunction

Slot headerFilters

Overrides the datatable filters button.

AttributeDescriptionType
filtersArray with all the filtersArray
hasFiltersIf any filters are availableBoolean
isShowingFiltersIf its showing filtersBoolean
filtersActiveCountNumber of filters enabledNumber

Slot headerSettings

Overrides the datatable settings button/dropdown.

AttributeDescriptionType
refreshableIf the datatable is refreshableBoolean
isFetchingIf table is fetchingBoolean
isShowingSettingsIf its showing settingsBoolean
userSettingsSettings from the user / savedObject
refreshFunction to refresh the tableFunction

Slot headerSearch

Overrides the datatable search bar

AttributeDescriptionType
searchableIf the datatable is searchableBoolean
hasAnyItemsSelectedIf any items are selectedBoolean
queryQuery String[String, Undefined]
placeholderPlaceholder for search barString
onSearchFunction to execute on searchFunction

Slot headerFiltersActive

When filters are enabled and some filter is applied, a little bar shows Below the search bar This slot gives you the ability to override this piece.

AttributeDescriptionType
hasFiltersIf any filter is appliedBoolean
filtersActiveCountHow much filters are appliedNumber
filtersArray of all filters available & appliedArray
resetFilterFunction to execute to reset a specific filterFunction

Slot selection

When items are selected, the search bar will hide and a little bar will show up how many items are selected and actions to select / deselect all, this slot overrides that little bar.

AttributeDescriptionType
isAllSelectedIf "all" is selectedBoolean
countSelectedHow much items are selectedNumber
countTotalTotal Number of items in pageNumber
deselectAllFunction to deselect all itemsFunction
selectMatchingFunction to select all items availableFunction
deselectMatchingFunction to de-select all all items availableFunction

Slot emptyWithFilters

When filters or search are applied and no results were found, this slot lets you customize the empty state that will be displayed.

Sub-slots are also available ( with the same props ) for a fine-tuned UI customization:

  • emptyWithFiltersIcon : Overrides the Icon
  • emptyWithFiltersTitle : Overrides the Title
  • emptyWithFiltersButton : Overrides the Button
AttributeDescriptionType
filtersArray of the filter appliedArray
filtersActiveCountCount of the filters appliedNumber
resetFiltersAndSearchFunction to clear all filters & searchFunction

Slot emptyWithoutRecords

When we are unable to get items for some reason or the items/data is/are actually empty this slot will be used to show an empty state

Sub-slots are also available ( with the same props ) for a fine-tuned UI customization:

  • emptyWithoutRecordsIcon : Overrides the Icon
  • emptyWithoutRecordsTitle : Overrides the Title
  • emptyWithoutRecordsButton : Overrides the Button

Slot header

Table Head/Header, this slot let you override the default table head with your own

AttributeDescriptionType
datatableAll configuration of the datableObject
isFetchingIf table is fetchingBoolean
isAllItemsInPageSelectedIf All items on page are selectedBoolean
hasAnyItemsSelectedForCurrentPageIf any item on the page is selectedBoolean
columnsArray of columnsArray
selectAllItemsInPageFunction select all items in pageFunction

Slot skeleton

When table is loading or busy, a skeleton will be shown with the animation of loading state this slot let you override the current skeleton.

AttributeDescriptionType
isFetchingIf table is fetchingBoolean
showBeInLoadingStateIf loading should be displayedBoolean
columnsCountNumber of columnsNumber
rowsCountNumber of rows being displayed ( fake )Number

Slot default

The main slow for each row / record / td being displayed.

AttributeDescriptionType
resultAPI / Data result item rawObject
columnsArray of columns availableArray
selectableIf table is selectableBoolean
selectedIf the row is selectedBoolean

Slot pagination

Slot to override the pagination on the card footer

AttributeDescriptionType
isFetchingIf table is fetchingBoolean
pagesArray of pages availableArray
previousPagePrevious pageString
nextPageNext PageString
currentPageCurrent PageNumber
showingFromShow from X recordsNumber
showingToShow Until Y RecordsNumber
totalOf a total ofNumber

Slot actionsDialog

When an action requires a confirmation, the dialog to show for the user to confirm.

AttributeDescriptionType
isShowingActionConfirmationIf its showing the dialogBoolean
currentActionCurrent action selectedObject
selectedItemsCountNumber of rows selectedNumber
onActionConfirmedFunction on Act. ConfirmedFunction

Sub-slots are also available for a fine-tuned customization with the following props & slots

  • actionsIcon : Overrides the Action Icon
  • actionBody : Overrides the Action Body/Text

Props passed to the above slots:

AttributeDescriptionType
actionCurrent action selectedObject
titleTitle of the dialogString
textBody/Text of the dialogString
confirmTextConfirm Button LabelString
cancelTextCancel Button LabelString

Slot settingsDialog

Dialog that shows the user settings to configure or customize de table

AttributeDescriptionType
isShowingSettingsIf its showing the dialogBoolean
userSettingsCurrent user settingsObject
perPageOptionsArray of Per Page OptionsArray
columnsArray of Columns AvailableArray

Slot filtersDialog

Dialog that shows the filters to apply

AttributeDescriptionType
isShowingFiltersIf its showing the dialogBoolean
userSettingsCurrent user settingsObject
filtersArray of Filters AvailableArray

Known issues 🐛

  • When using method GET to fetch items, we are unable to send filters in a proper way, for the time being please use post
  • Overriding certain slots can still cause issues, please kindly double-check has the library lacks of tests.

Released under the MIT License.