Last active
May 10, 2024 08:28
-
-
Save coolsam726/f156daa5b36a7a8217526eb82bcaa798 to your computer and use it in GitHub Desktop.
A fully functional Vue component to work with savannabits/primevue-datatables package (Works with tailwindcss and Vue.js 3.x). NB: The usage example is based on savannabits/acacia, a backend generator I developed to make your life easier by generating code for the backend CRUDs. You can flesh out the unnecessary parts to remain with the bare-bon…
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export const useDebounce = () => { | |
let timeout:any = null; | |
return function (fn: Function, delayMs: number = 500) { | |
clearTimeout(timeout); | |
timeout = setTimeout(() => { | |
fn(); | |
}, delayMs); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<template> | |
<DataTable | |
class="p-datatable-sm" | |
:value="records" | |
:lazy="true" | |
:auto-layout="true" | |
:paginator="true" | |
:rows="10" | |
v-model:filters="filters" | |
ref="dt" | |
:loading="loading" | |
:total-records="totalRecords" | |
:globalFilterFields="searchableColumns" | |
@page="onPage" | |
@sort="onSort" | |
@filter="onFilter" | |
filter-display="row" | |
responsive-layout="stack" | |
breakpoint="960px" | |
:state-key="stateKey" | |
state-storage="session" | |
currentPageReportTemplate="Showing {first} to {last} of {totalRecords} records" | |
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown" :rowsPerPageOptions="[5,10,15,25,50]" | |
> | |
<template #loading> | |
Loading... | |
</template> | |
<template #header> | |
<div class="flex flex-wrap gap-2 justify-between items-center"> | |
<div> | |
<div class="flex items-center gap-2"> | |
<Button @click="loadLazyData" class="p-button-text p-button-plain" icon="pi pi-refresh"/> | |
<slot name="header"> | |
<h5 class="font-semibold">{{title}}</h5> | |
</slot> | |
</div> | |
</div> | |
<div class="p-input-icon-left max-w-[360px]"> | |
<i v-if="!filters['global'].value" class="pi pi-search" /> | |
<i v-else v-if="filters['global'].value" class="pi pi-times" @click="filters['global'].value = null; onFilter()"></i> | |
<InputText v-model="filters['global'].value" @input="debounce(onFilter,500)" placeholder="Keyword Search" /> | |
</div> | |
</div> | |
</template> | |
<template #empty> | |
<p class="text-center">No records found.</p> | |
</template> | |
<slot></slot> | |
</DataTable> | |
</template> | |
<script lang="ts"> | |
import {defineComponent, onMounted, Ref, ref, toRef, watch} from "vue"; | |
import {FilterMatchMode} from "primevue/api"; | |
import {useDebounce} from "@/composables/debounce"; | |
import axios from "axios"; | |
import InputText from "primevue/inputtext"; | |
import DataTable from "primevue/datatable"; | |
import Button from "primevue/button"; | |
export default defineComponent({ | |
name: "PrimeDatatables", | |
components: { | |
DataTable, | |
InputText, | |
Button, | |
}, | |
props: { | |
apiUrl: String, | |
title: String, | |
refresh: String, | |
defaultSortField: String, | |
defaultSortDesc: { | |
type: Boolean, | |
default: true, | |
}, | |
searchableColumns: { | |
type: Array, | |
default: [], | |
}, | |
columnFilters: { | |
required: true, | |
type: Object, | |
default: {} | |
}, | |
stateKey: String, | |
}, | |
setup(props) { | |
onMounted(async () => { | |
// loading.value = true; | |
// console.log(filters.value); | |
lazyParams.value = JSON.parse(sessionStorage.getItem(stateKey.value as string) as string); | |
if (!lazyParams.value) { | |
lazyParams.value = { | |
first: 0, | |
filters: filters.value, | |
rows: 10, | |
} | |
} | |
lazyParams.value.page = Math.fround(parseInt(lazyParams.value.first)/parseInt(lazyParams.value.rows || 10)) | |
console.log(lazyParams.value); | |
await loadLazyData(); | |
}) | |
const refresh = toRef(props, "refresh"); | |
watch(refresh,(val) => { | |
loadLazyData(); | |
}); | |
const dt = ref(); | |
const debounce = useDebounce(); | |
const loading = ref(false); | |
const totalRecords = ref(0); | |
const records = ref(); | |
const filtersProp = toRef(props,"columnFilters"); | |
const filters = ref({}); | |
filters.value = { | |
...filtersProp.value, | |
global: {value: '', matchMode: FilterMatchMode.CONTAINS} | |
} | |
const searchableColumns = toRef(props, "searchableColumns") as Ref<Array<string>> | |
const lazyParams: Ref<any> = ref({}); | |
const apiUrl = toRef(props, "apiUrl") as Ref<string>; | |
const stateKey = toRef(props,'stateKey'); | |
const loadLazyData = async () => { | |
loading.value = true; | |
lazyParams.value.filters = filters.value; | |
if (!lazyParams.value.sortField) { | |
lazyParams.value.sortField = toRef(props, "defaultSortField").value; | |
} | |
if (![-1,1].includes(lazyParams.value.sortOrder)) { | |
lazyParams.value.sortOrder = toRef(props, "defaultSortDesc").value ? -1 : 1; | |
} | |
try { | |
const res = await axios.get(apiUrl.value,{ | |
params: { | |
dt_params: JSON.stringify(lazyParams.value), | |
searchable_columns: JSON.stringify(searchableColumns.value), | |
}, | |
}); | |
records.value = res.data.data ?? []; | |
totalRecords.value = res.data.total; | |
loading.value = false; | |
} catch (e) { | |
records.value = []; | |
totalRecords.value = 0; | |
loading.value = false; | |
} | |
}; | |
const onPage = (event) => { | |
lazyParams.value = event; | |
// lazyParams.value.filters = filters.value; | |
loadLazyData(); | |
}; | |
const onSort = (event) => { | |
lazyParams.value = event; | |
loadLazyData(); | |
}; | |
const onFilter = () => { | |
// lazyParams.value.filters = filters.value; | |
//Reset pagination first | |
// lazyParams.value.originalEvent = {first: 0, page: 0} | |
// onPage(lazyParams.value); | |
loadLazyData(); | |
} | |
return { | |
dt, | |
loading, | |
totalRecords, | |
records, | |
filters, | |
lazyParams, | |
loadLazyData, | |
onPage, | |
onSort, | |
onFilter, | |
debounce | |
} | |
} | |
}); | |
</script> | |
<style scoped> | |
</style> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<template> | |
<Head> | |
<title>Roles</title> | |
</Head> | |
<Backend> | |
<template #header> | |
<h4 class="font-black text-2xl px-1 md:px-4">Roles</h4> | |
</template> | |
<div class="mx-auto flex container items-center justify-center mt-4"> | |
<div class="rounded w-full p-2 bg-white"> | |
<div class="flex flex-wrap items-center justify-end gap-2"> | |
<Button | |
v-if="$page.props.can?.create" | |
@click="createModal = true" | |
aria-label="New Comment" | |
label="New Role" | |
icon="pi pi-plus" | |
/> | |
</div> | |
<PrimeDatatables | |
v-if="$page.props.can?.viewAny" | |
:apiUrl="apiUrl" | |
:columnFilters="{}" | |
:searchableColumns="searchableCols" | |
:stateKey="stateKey" | |
defaultSortField="id" | |
contextMenu | |
v-model:contextMenuSelection="selectedRow" | |
@row-contextmenu="showContextMenu" | |
:rowHover="true" | |
:refresh="refreshTime" | |
> | |
<Column field="id" header="Id" :sortable="true" /> | |
<Column field="name" header="Name" :sortable="true" /> | |
<Column | |
field="guard_name" | |
header="Guard Name" | |
:sortable="true" | |
/> | |
<Column | |
field="created_at" | |
header="Created At" | |
:sortable="true" | |
> | |
<template #body="{ data }"> | |
<span>{{ | |
data.created_at | |
? dayjs(data.created_at).format( | |
"MMM DD, YYYY hh:mm a" | |
) | |
: "-" | |
}}</span> | |
</template> | |
</Column> | |
<Column | |
field="updated_at" | |
header="Updated At" | |
:sortable="true" | |
> | |
<template #body="{ data }"> | |
<span>{{ | |
data.updated_at | |
? dayjs(data.updated_at).format( | |
"MMM DD, YYYY hh:mm a" | |
) | |
: "-" | |
}}</span> | |
</template> | |
</Column> | |
<Column> | |
<template #body="props"> | |
<Button | |
class="" | |
@click="toggleOptions($event, props.data)" | |
icon-pos="right" | |
:class="'p-button-text'" | |
:icon="'pi pi-ellipsis-v'" | |
:label="'Options'" | |
/> | |
</template> | |
</Column> | |
</PrimeDatatables> | |
<Message v-else severity="error" | |
>You are not authorized to access this content</Message | |
> | |
</div> | |
</div> | |
<ContextMenu ref="contextMenu" :model="options" /> | |
<Dialog | |
position="top" | |
:maximizable="true" | |
v-model:visible="createModal" | |
:modal="true" | |
:breakpoints="{ | |
'1600px': '50vw', | |
'960px': '75vw', | |
'540px': '100vw', | |
}" | |
:style="{ width: '35vw' }" | |
> | |
<template #header> | |
<h4 class="font-black text-xl">New Role</h4> | |
</template> | |
<CreateForm @created="onCreated" v-if="createModal" /> | |
<template #footer> | |
<Button | |
label="Open in a Page" | |
icon="pi pi-window" | |
@click="$inertia.visit(route('acacia.auth.roles.create'))" | |
class="p-button-text" | |
/> | |
<Button | |
label="Close" | |
icon="pi pi-times" | |
@click="createModal = false" | |
class="p-button-text" | |
/> | |
</template> | |
</Dialog> | |
<Dialog | |
position="top" | |
:maximizable="true" | |
v-model:visible="showModal" | |
:modal="true" | |
:breakpoints="{ | |
'1600px': '50vw', | |
'960px': '75vw', | |
'540px': '100vw', | |
}" | |
:style="{ width: '35vw' }" | |
> | |
<template #header> | |
<h4 class="font-black text-xl">Role Details</h4> | |
</template> | |
<ShowForm :model="currentModel" v-if="showModal && currentModel" /> | |
<template #footer> | |
<Button | |
label="Open in a Page" | |
icon="pi pi-window" | |
@click=" | |
$inertia.visit( | |
route('acacia.auth.roles.show', currentModel) | |
) | |
" | |
class="p-button-text" | |
/> | |
<Button | |
label="Close" | |
icon="pi pi-times" | |
@click="(showModal = false), (currentModel = null)" | |
class="p-button-text" | |
/> | |
</template> | |
</Dialog> | |
<Dialog | |
position="top" | |
:maximizable="true" | |
v-model:visible="editModal" | |
:modal="true" | |
:breakpoints="{ | |
'1600px': '50vw', | |
'960px': '75vw', | |
'540px': '100vw', | |
}" | |
:style="{ width: '50vw' }" | |
> | |
<template #header> | |
<h4 class="font-black text-xl">Edit Single Role</h4> | |
</template> | |
<EditForm | |
:model="currentModel" | |
@updated="onUpdated" | |
v-if="editModal && currentModel" | |
/> | |
<template #footer> | |
<Button | |
label="Open in a Page" | |
icon="pi pi-window" | |
@click=" | |
$inertia.visit( | |
route('acacia.auth.roles.edit', currentModel) | |
) | |
" | |
class="p-button-text" | |
/> | |
<Button | |
label="Close" | |
icon="pi pi-times" | |
@click="(editModal = false), (currentModel = null)" | |
class="p-button-text" | |
/> | |
</template> | |
</Dialog> | |
</Backend> | |
</template> | |
<script lang="ts"> | |
import { defineComponent } from "vue"; | |
export default defineComponent({ | |
name: "RolesIndex", | |
}); | |
</script> | |
<script lang="ts" setup> | |
import Head from "@inertiajs/inertia-vue3"; | |
import Backend from "@Acacia/Core/Js/Layouts/Backend.vue"; | |
import PrimeDatatables from "@Acacia/Core/Js/Components/PrimeDatatables.vue"; | |
import Column from "primevue/column"; | |
import Button from "primevue/button"; | |
import ContextMenu from "primevue/contextmenu"; | |
import Badge from "primevue/badge"; | |
import dayjs from "dayjs"; | |
import route from "ziggy-js"; | |
import { nextTick, Ref, ref } from "vue"; | |
import { useConfirm } from "primevue/useconfirm"; | |
import { useToast } from "primevue/usetoast"; | |
import { Inertia } from "@inertiajs/inertia"; | |
import axios from "axios"; | |
import Dialog from "primevue/dialog"; | |
import CreateForm from "./Partials/CreateForm.vue"; | |
import EditForm from "./Partials/EditForm.vue"; | |
import ShowForm from "./Partials/ShowForm.vue"; | |
import Message from "primevue/message"; | |
const apiUrl = route("api.v1.roles.dt"); | |
const stateKey = "roles-dt"; | |
const searchableCols = ref([ | |
"id", | |
"name", | |
"guard_name", | |
"created_at", | |
"updated_at", | |
]); | |
const selectedRow = ref(null) as Ref<any>; | |
const contextMenu = ref(); | |
const options = ref([]) as Ref<any>; | |
const confirm = useConfirm(); | |
const toast = useToast(); | |
const refreshTime = ref(null) as Ref<string | null>; | |
const createModal = ref(false); | |
const editModal = ref(false); | |
const showModal = ref(false); | |
const currentModel = ref(null) as Ref<any>; | |
const makeOptionsMenu = (row) => [ | |
{ | |
label: "Details", | |
icon: "pi pi-eye", | |
command: async () => { | |
currentModel.value = null; | |
await fetchModel(row); | |
showModal.value = true; | |
}, | |
visible: () => row?.can?.view, | |
}, | |
{ | |
separator: true, | |
}, | |
{ | |
label: "Edit", | |
icon: "pi pi-pencil", | |
command: async () => { | |
currentModel.value = null; | |
await fetchModel(row); | |
editModal.value = true; | |
}, | |
visible: () => row?.can?.update, | |
}, | |
{ | |
label: "Delete", | |
icon: "pi pi-trash", | |
command: () => { | |
confirm.require({ | |
message: "Are you sure you want to delete this record?", | |
header: "Confirm Deletion", | |
accept: () => deleteModel(row), | |
}); | |
}, | |
visible: () => row?.can?.delete, | |
}, | |
]; | |
const fetchModel = async (row) => { | |
axios | |
.get(route("api.v1.roles.show", row)) | |
.then((res) => { | |
currentModel.value = res.data?.payload; | |
}) | |
.catch((err) => { | |
console.error(err); | |
currentModel.value = null; | |
}); | |
}; | |
const refresh = () => { | |
refreshTime.value = new Date().toUTCString(); | |
}; | |
const toggleOptions = async (e, row) => { | |
options.value = makeOptionsMenu(row); | |
await nextTick(); | |
contextMenu.value.show(e); | |
}; | |
const showContextMenu = async (e) => { | |
options.value = makeOptionsMenu(e.data); | |
await nextTick(); | |
contextMenu.value.show(e.originalEvent); | |
}; | |
const deleteModel = async function (row) { | |
try { | |
const res = await axios.delete( | |
route("api.v1.roles.destroy", row as any) | |
); | |
toast.add({ | |
severity: "success", | |
detail: res.data.message, | |
life: 3000, | |
}); | |
refresh(); | |
} catch (e: any) { | |
console.log(e); | |
const msg = | |
e?.response?.data?.message || | |
e?.data?.message || | |
e?.message || | |
e || | |
"Server error"; | |
toast.add({ | |
severity: "error", | |
detail: msg, | |
summary: "Server Error", | |
life: 10000, | |
}); | |
} | |
}; | |
const onCreated = (e) => { | |
// console.log(e.payload); | |
createModal.value = false; | |
refresh(); | |
}; | |
const onUpdated = (e) => { | |
// console.log(e.payload); | |
editModal.value = false; | |
refresh(); | |
}; | |
</script> | |
<style scoped></style> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment