@saulpaulus17/vue-datatables-flex 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 saul paulus (Ixspx)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,209 @@
1
+ # @saulpaulus17/vue-datatables-flex
2
+
3
+ A high-performance, responsive DataTable component for Vue 3 and Nuxt 3/4.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@saulpaulus17/vue-datatables-flex.svg?style=flat-square)](https://www.npmjs.com/package/@saulpaulus17/vue-datatables-flex)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square)](https://opensource.org/licenses/MIT)
7
+ [![Vue Version](https://img.shields.io/badge/Vue-3.x-42b883?style=flat-square&logo=vue.js)](https://vuejs.org/)
8
+ [![Nuxt Version](https://img.shields.io/badge/Nuxt-3.x%20|%204.x-00dc82?style=flat-square&logo=nuxt.js)](https://nuxt.com/)
9
+
10
+ ---
11
+
12
+ ## Features
13
+
14
+ - Optimized performance using DataTables
15
+ - Clean UI with Bootstrap 5 styling
16
+ - Fully responsive layout
17
+ - Nuxt 3 & 4 module support
18
+ - TypeScript support
19
+ - SSR-friendly rendering
20
+ - Indonesian locale ready
21
+
22
+ ---
23
+
24
+ ## 🚀 Installation
25
+
26
+ Install the package:
27
+
28
+ ```bash
29
+ npm install @saulpaulus17/vue-datatables-flex
30
+ ```
31
+
32
+ ### Required Peer Dependencies
33
+
34
+ Ensure your project has the following essentials installed:
35
+
36
+ ```bash
37
+ npm install vue bootstrap datatables.net-bs5
38
+ ```
39
+
40
+ ---
41
+
42
+ ## 📦 Usage in Vue 3
43
+
44
+ ### Global Registration
45
+
46
+ Recommended for large applications using the component across multiple pages.
47
+
48
+ ```ts
49
+ // main.ts
50
+ import { createApp } from "vue";
51
+ import { VueDatatablesFlex } from "@saulpaulus17/vue-datatables-flex";
52
+ import App from "./App.vue";
53
+
54
+ // Import Required CSS
55
+ import "bootstrap/dist/css/bootstrap.min.css";
56
+ import "datatables.net-bs5/css/dataTables.bootstrap5.min.css";
57
+
58
+ const app = createApp(App);
59
+ app.use(VueDatatablesFlex);
60
+ app.mount("#app");
61
+ ```
62
+
63
+ ### Basic Usage
64
+
65
+ ```vue
66
+ <template>
67
+ <MainDataTable
68
+ :data="userRows"
69
+ :columns="userColumns"
70
+ scroll-y="500px"
71
+ @ready="onReady"
72
+ />
73
+ </template>
74
+
75
+ <script setup lang="ts">
76
+ import { ref } from "vue";
77
+ import type { Column } from "@saulpaulus17/vue-datatables-flex";
78
+
79
+ const userColumns: Column[] = [
80
+ { data: "id", title: "ID" },
81
+ { data: "name", title: "Full Name" },
82
+ { data: "email", title: "Email Address" },
83
+ ];
84
+
85
+ const userRows = ref([
86
+ { id: 1, name: "Ahmad", email: "ahmad@example.com" },
87
+ { id: 2, name: "Budi", email: "budi@example.com" },
88
+ ]);
89
+
90
+ const onReady = (dt: any) => console.log("Table instance:", dt);
91
+ </script>
92
+ ```
93
+
94
+ ---
95
+
96
+ ## 🟢 Usage in Nuxt 3 & 4
97
+
98
+ The simplest way is to use the provided Nuxt module. It handles everything including **Universal Rendering (SSR)**.
99
+
100
+ ### 1. Add to Nuxt Config
101
+
102
+ ```ts
103
+ // nuxt.config.ts
104
+ export default defineNuxtConfig({
105
+ modules: ["@saulpaulus17/vue-datatables-flex/nuxt"],
106
+
107
+ // Optional: Module Configuration
108
+ vueDatatablesFlex: {
109
+ componentName: "MainDataTable", // Use a custom component name
110
+ addCss: true, // Automatically includes DataTables Bootstrap 5 CSS (default: true)
111
+ },
112
+
113
+ // You still need to include Bootstrap 5 CSS
114
+ css: ["bootstrap/dist/css/bootstrap.min.css"],
115
+ });
116
+ ```
117
+
118
+ ### 2. Implementation
119
+
120
+ ```vue
121
+ <!-- pages/users.vue -->
122
+ <script setup lang="ts">
123
+ const columns = [{ data: "name", title: "Name" }];
124
+ const { data: users } = await useFetch("/api/users");
125
+ </script>
126
+
127
+ <template>
128
+ <!-- No import needed! Component is auto-registered -->
129
+ <MainDataTable :data="users" :columns="columns" />
130
+ </template>
131
+ ```
132
+
133
+ > [!TIP]
134
+ > **Universal Rendering Support:**
135
+ > Version 0.1.4+ is fully SSR-aware. While the interactive DataTable initializes on the client, the component renders a semantic HTML table on the server. This ensures your data is **visible to search engines** and avoids Cumulative Layout Shift (CLS).
136
+
137
+ ---
138
+
139
+ ## 🛠 API Reference
140
+
141
+ ### Props
142
+
143
+ | Property | Type | Default | Description |
144
+ | :----------- | :----------------- | :------- | :--------------------------------------------------- |
145
+ | `data` | `unknown[]` | `[]` | The data array to display (Alias: `dataTableProps`). |
146
+ | `columns` | `Column[]` | `[]` | Column definitions (Alias: `columnsProps`). |
147
+ | `options` | `DataTableOptions` | `{}` | Advanced DataTables configuration overrides. |
148
+ | `loading` | `boolean` | `false` | Shows a beautiful animated loading overlay. |
149
+ | `responsive` | `boolean` | `false` | Enables DataTables Responsive extension. |
150
+ | `scrollY` | `string \| number` | `'65vh'` | Sets the vertical scroll height. |
151
+ | `tableClass` | `string` | `''` | Custom CSS class for the `<table>` element. |
152
+
153
+ ### Events
154
+
155
+ | Event | Payload | Description |
156
+ | :-------- | :---------------- | :-------------------------------------------------------- |
157
+ | `@ready` | `(instance: Api)` | Fired when initialization is complete. |
158
+ | `@draw` | `(settings: any)` | Fired whenever the table is redrawn (sort, filter, page). |
159
+ | `@select` | `(items, type)` | Fired when rows are selected (requires `select: true`). |
160
+ | `@error` | `(err: Error)` | Fired if initialization fails. |
161
+
162
+ ### Exposed Methods (Template Ref)
163
+
164
+ Access these by adding a `ref` to your component:
165
+
166
+ ```ts
167
+ const table = ref<InstanceType<typeof MainDataTable>>();
168
+
169
+ // Methods
170
+ table.value?.getInstance(); // Get raw DataTables API instance
171
+ table.value?.reload(); // Reload AJAX data (if using server-side)
172
+ table.value?.adjustColumns(); // recalculate column widths
173
+ ```
174
+
175
+ ---
176
+
177
+ ## 🎨 Customizing Columns
178
+
179
+ ```ts
180
+ const columns: Column[] = [
181
+ {
182
+ data: "status",
183
+ title: "Status",
184
+ className: "text-center",
185
+ render: (data) => {
186
+ const color = data === "Active" ? "success" : "secondary";
187
+ return `<span class="badge bg-${color}">${data}</span>`;
188
+ },
189
+ },
190
+ ];
191
+ ```
192
+
193
+ ---
194
+
195
+ ## 🙋 Troubleshooting
196
+
197
+ **"JQuery is not defined"**
198
+ Ensure you are importing the Bootstrap integration correctly in your entry point: `import 'datatables.net-bs5';`.
199
+
200
+ **CSS Alignment Issues**
201
+ Ensure you've imported BOTH the Bootstrap 5 core CSS and the DataTables Bootstrap 5 styling in the correct order.
202
+
203
+ ---
204
+
205
+ ## 📄 License
206
+
207
+ This package is open-sourced software licensed under the [MIT License](LICENSE).
208
+
209
+ Built with ❤️ for the Vue ecosystem.
Binary file
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ const kit = require("@nuxt/kit");
3
+ var _documentCurrentScript = typeof document !== "undefined" ? document.currentScript : null;
4
+ const nuxt = kit.defineNuxtModule({
5
+ meta: {
6
+ name: "@saulpaulus17/vue-datatables-flex",
7
+ configKey: "vueDatatablesFlex",
8
+ compatibility: {
9
+ nuxt: ">=3.0.0"
10
+ }
11
+ },
12
+ defaults: {
13
+ componentName: "MainDataTable",
14
+ addCss: true
15
+ },
16
+ setup(options, nuxt2) {
17
+ const resolver = kit.createResolver(typeof document === "undefined" ? require("url").pathToFileURL(__filename).href : _documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === "SCRIPT" && _documentCurrentScript.src || new URL("nuxt.cjs.js", document.baseURI).href);
18
+ kit.addComponent({
19
+ name: options.componentName || "MainDataTable",
20
+ // Point langsung ke file .vue di folder runtime
21
+ filePath: resolver.resolve("./runtime/components/MainDataTable.vue")
22
+ });
23
+ if (options.addCss) {
24
+ nuxt2.options.css.push("datatables.net-bs5/css/dataTables.bootstrap5.min.css");
25
+ }
26
+ nuxt2.options.build.transpile.push("@saulpaulus17/vue-datatables-flex");
27
+ }
28
+ });
29
+ module.exports = nuxt;
30
+ //# sourceMappingURL=nuxt.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nuxt.cjs.js","sources":["../src/nuxt.ts"],"sourcesContent":["import { defineNuxtModule, addComponent, createResolver } from '@nuxt/kit'\n\nexport interface ModuleOptions {\n /** Nama komponen global yang didaftarkan (default: \"MainDataTable\") */\n componentName?: string;\n /** Auto-import CSS Bootstrap DataTables */\n addCss?: boolean;\n}\n\nexport default defineNuxtModule<ModuleOptions>({\n meta: {\n name: '@saulpaulus17/vue-datatables-flex',\n configKey: 'vueDatatablesFlex',\n compatibility: {\n nuxt: '>=3.0.0'\n }\n },\n defaults: {\n componentName: 'MainDataTable',\n addCss: true\n },\n setup(options: ModuleOptions, nuxt: any) {\n const resolver = createResolver(import.meta.url)\n\n // Daftarkan komponen secara global (Default: MainDataTable)\n addComponent({\n name: options.componentName || 'MainDataTable',\n // Point langsung ke file .vue di folder runtime\n filePath: resolver.resolve('./runtime/components/MainDataTable.vue'),\n })\n\n // Tambah CSS jika diminta\n if (options.addCss) {\n nuxt.options.css.push('datatables.net-bs5/css/dataTables.bootstrap5.min.css')\n }\n\n // Pastikan library di-transpile oleh Nuxt\n nuxt.options.build.transpile.push('@saulpaulus17/vue-datatables-flex')\n }\n})\n"],"names":["defineNuxtModule","nuxt","createResolver","addComponent"],"mappings":";;;AASA,MAAA,OAAeA,qBAAgC;AAAA,EAC7C,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,WAAW;AAAA,IACX,eAAe;AAAA,MACb,MAAM;AAAA,IAAA;AAAA,EACR;AAAA,EAEF,UAAU;AAAA,IACR,eAAe;AAAA,IACf,QAAQ;AAAA,EAAA;AAAA,EAEV,MAAM,SAAwBC,OAAW;AACvC,UAAM,WAAWC,IAAAA,mQAA8B;AAG/CC,qBAAa;AAAA,MACX,MAAM,QAAQ,iBAAiB;AAAA;AAAA,MAE/B,UAAU,SAAS,QAAQ,wCAAwC;AAAA,IAAA,CACpE;AAGD,QAAI,QAAQ,QAAQ;AAClB,MAAAF,MAAK,QAAQ,IAAI,KAAK,sDAAsD;AAAA,IAC9E;AAGA,IAAAA,MAAK,QAAQ,MAAM,UAAU,KAAK,mCAAmC;AAAA,EACvE;AACF,CAAC;;"}
@@ -0,0 +1,30 @@
1
+ import { defineNuxtModule, createResolver, addComponent } from "@nuxt/kit";
2
+ const nuxt = defineNuxtModule({
3
+ meta: {
4
+ name: "@saulpaulus17/vue-datatables-flex",
5
+ configKey: "vueDatatablesFlex",
6
+ compatibility: {
7
+ nuxt: ">=3.0.0"
8
+ }
9
+ },
10
+ defaults: {
11
+ componentName: "MainDataTable",
12
+ addCss: true
13
+ },
14
+ setup(options, nuxt2) {
15
+ const resolver = createResolver(import.meta.url);
16
+ addComponent({
17
+ name: options.componentName || "MainDataTable",
18
+ // Point langsung ke file .vue di folder runtime
19
+ filePath: resolver.resolve("./runtime/components/MainDataTable.vue")
20
+ });
21
+ if (options.addCss) {
22
+ nuxt2.options.css.push("datatables.net-bs5/css/dataTables.bootstrap5.min.css");
23
+ }
24
+ nuxt2.options.build.transpile.push("@saulpaulus17/vue-datatables-flex");
25
+ }
26
+ });
27
+ export {
28
+ nuxt as default
29
+ };
30
+ //# sourceMappingURL=nuxt.es.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nuxt.es.js","sources":["../src/nuxt.ts"],"sourcesContent":["import { defineNuxtModule, addComponent, createResolver } from '@nuxt/kit'\n\nexport interface ModuleOptions {\n /** Nama komponen global yang didaftarkan (default: \"MainDataTable\") */\n componentName?: string;\n /** Auto-import CSS Bootstrap DataTables */\n addCss?: boolean;\n}\n\nexport default defineNuxtModule<ModuleOptions>({\n meta: {\n name: '@saulpaulus17/vue-datatables-flex',\n configKey: 'vueDatatablesFlex',\n compatibility: {\n nuxt: '>=3.0.0'\n }\n },\n defaults: {\n componentName: 'MainDataTable',\n addCss: true\n },\n setup(options: ModuleOptions, nuxt: any) {\n const resolver = createResolver(import.meta.url)\n\n // Daftarkan komponen secara global (Default: MainDataTable)\n addComponent({\n name: options.componentName || 'MainDataTable',\n // Point langsung ke file .vue di folder runtime\n filePath: resolver.resolve('./runtime/components/MainDataTable.vue'),\n })\n\n // Tambah CSS jika diminta\n if (options.addCss) {\n nuxt.options.css.push('datatables.net-bs5/css/dataTables.bootstrap5.min.css')\n }\n\n // Pastikan library di-transpile oleh Nuxt\n nuxt.options.build.transpile.push('@saulpaulus17/vue-datatables-flex')\n }\n})\n"],"names":["nuxt"],"mappings":";AASA,MAAA,OAAe,iBAAgC;AAAA,EAC7C,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,WAAW;AAAA,IACX,eAAe;AAAA,MACb,MAAM;AAAA,IAAA;AAAA,EACR;AAAA,EAEF,UAAU;AAAA,IACR,eAAe;AAAA,IACf,QAAQ;AAAA,EAAA;AAAA,EAEV,MAAM,SAAwBA,OAAW;AACvC,UAAM,WAAW,eAAe,YAAY,GAAG;AAG/C,iBAAa;AAAA,MACX,MAAM,QAAQ,iBAAiB;AAAA;AAAA,MAE/B,UAAU,SAAS,QAAQ,wCAAwC;AAAA,IAAA,CACpE;AAGD,QAAI,QAAQ,QAAQ;AAClB,MAAAA,MAAK,QAAQ,IAAI,KAAK,sDAAsD;AAAA,IAC9E;AAGA,IAAAA,MAAK,QAAQ,MAAM,UAAU,KAAK,mCAAmC;AAAA,EACvE;AACF,CAAC;"}
@@ -0,0 +1,341 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ <script setup lang="ts">
3
+ import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
4
+ import DataTableSource from "datatables.net-vue3";
5
+ import DataTablesLib from "datatables.net-bs5";
6
+ import "datatables.net-responsive-bs5";
7
+ import "datatables.net-select-bs5";
8
+
9
+ import type { Api } from "datatables.net";
10
+ import type { Column, DataTableOptions } from "../types/index";
11
+ import { defaultOptions } from "../types/index";
12
+
13
+ // Datatables.net component: casting to any avoids leaky types (TS4058 / JQueryDataTables)
14
+ const DataTableComponent = DataTableSource as any;
15
+
16
+ const isMounted = ref(false);
17
+
18
+ // Konfigurasi DataTables: Register core dan extension
19
+ if (typeof window !== "undefined") {
20
+ DataTableSource.use(DataTablesLib);
21
+ }
22
+
23
+ // Datatables.net menggunakan API yang sangat dinamis (any is unavoidable here)
24
+ // Semua interaksi publik (props/emits) sudah ditype dengan benar di DataTableMain.vue.d.ts
25
+
26
+ // ===================================================
27
+ // Props
28
+ // ===================================================
29
+ interface Props {
30
+ /** Data array untuk tabel (Alias: dataTableProps) */
31
+ data?: unknown[];
32
+ /** Data array untuk tabel (Prop name dari user snippet) */
33
+ dataTableProps?: unknown[];
34
+ /** Definisi kolom (Alias: columnsProps) */
35
+ columns?: Column[];
36
+ /** Definisi kolom (Prop name dari user snippet) */
37
+ columnsProps?: Column[];
38
+ /** Override DataTable options */
39
+ options?: DataTableOptions;
40
+ /** Tinggi scroll vertikal (mis: "65vh", "400px") */
41
+ scrollY?: string | number | false;
42
+ /** Aktifkan horizontal scroll */
43
+ scrollX?: boolean;
44
+ /** Mode responsive (disable scrollX jika aktif) */
45
+ responsive?: boolean;
46
+ /** Class CSS tambahan untuk wrapper div */
47
+ wrapperClass?: string;
48
+ /** Class CSS tambahan untuk elemen table */
49
+ tableClass?: string;
50
+ /** Loading state — tampilkan overlay loading */
51
+ loading?: boolean;
52
+ /** Teks loading */
53
+ loadingText?: string;
54
+ }
55
+
56
+ const props = withDefaults(defineProps<Props>(), {
57
+ data: () => [],
58
+ dataTableProps: () => [],
59
+ columns: () => [],
60
+ columnsProps: () => [],
61
+ options: () => ({}),
62
+ scrollY: "65vh",
63
+ scrollX: true,
64
+ responsive: false,
65
+ wrapperClass: "",
66
+ tableClass: "",
67
+ loading: false,
68
+ loadingText: "Memuat data...",
69
+ });
70
+
71
+ // Compute internal data and columns from aliases
72
+ const internalData = computed(() =>
73
+ props.data && props.data.length > 0 ? props.data : props.dataTableProps,
74
+ );
75
+ const internalColumns = computed(() =>
76
+ props.columns && props.columns.length > 0 ? props.columns : props.columnsProps,
77
+ );
78
+
79
+ // ===================================================
80
+ // Emits
81
+ // ===================================================
82
+ const emit = defineEmits<{
83
+ /** DataTable selesai diinisialisasi */
84
+ ready: [instance: unknown];
85
+ /** Tabel selesai di-draw */
86
+ draw: [settings: unknown];
87
+ /** Baris di-select */
88
+ select: [items: unknown[], type: string];
89
+ /** Baris di-deselect */
90
+ deselect: [items: unknown[], type: string];
91
+ /** Error saat inisialisasi */
92
+ error: [err: Error];
93
+ }>();
94
+
95
+ // ===================================================
96
+ // Refs (datatables API requires any)
97
+ // ===================================================
98
+ const containerRef = ref<HTMLElement | null>(null);
99
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
100
+ const datatableRef = ref<{ dt: any } | null>(null);
101
+
102
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
103
+ let dtInstance: Api<any> | null = null;
104
+ let resizeObserver: ResizeObserver | null = null;
105
+
106
+ // ===================================================
107
+ // Computed Options — merge defaultOptions + props
108
+ // ===================================================
109
+ const tableOptions = computed<DataTableOptions>(() => ({
110
+ ...defaultOptions,
111
+ scrollY: props.scrollY,
112
+ scrollX: props.responsive ? false : props.scrollX,
113
+ responsive: props.responsive,
114
+ autoWidth: false,
115
+ deferRender: true,
116
+ ...props.options,
117
+ }));
118
+
119
+ // ===================================================
120
+ // Init
121
+ // ===================================================
122
+ onMounted(async () => {
123
+ isMounted.value = true;
124
+ await nextTick();
125
+
126
+ if (!datatableRef.value) return;
127
+
128
+ try {
129
+ dtInstance = datatableRef.value.dt;
130
+ if (!dtInstance) return;
131
+
132
+ // Adjust columns setelah mount
133
+ dtInstance.columns.adjust();
134
+
135
+ // Event: ready
136
+ emit("ready", dtInstance);
137
+
138
+ // Event: draw
139
+ dtInstance.on("draw", (e: Event, settings: unknown) => {
140
+ emit("draw", settings);
141
+ });
142
+
143
+ // Event: select / deselect (jika pakai select extension)
144
+ dtInstance.on("select", (_e: Event, _dt: unknown, type: string, indexes: number[]) => {
145
+ if (!dtInstance) return;
146
+ const rows = dtInstance.rows(indexes).data().toArray();
147
+ emit("select", rows, type);
148
+ });
149
+
150
+ dtInstance.on("deselect", (_e: Event, _dt: unknown, type: string, indexes: number[]) => {
151
+ if (!dtInstance) return;
152
+ const rows = dtInstance.rows(indexes).data().toArray();
153
+ emit("deselect", rows, type);
154
+ });
155
+
156
+ // ResizeObserver untuk handle container resize
157
+ if (containerRef.value) {
158
+ resizeObserver = new ResizeObserver(() => {
159
+ if (!dtInstance) return;
160
+ dtInstance.columns.adjust();
161
+ setTimeout(() => dtInstance?.columns.adjust(), 150);
162
+ setTimeout(() => dtInstance?.columns.adjust(), 400);
163
+ });
164
+ resizeObserver.observe(containerRef.value);
165
+ }
166
+
167
+ window.addEventListener("resize", handleResize);
168
+ } catch (err) {
169
+ emit("error", err instanceof Error ? err : new Error(String(err)));
170
+ }
171
+ });
172
+
173
+ // ===================================================
174
+ // Resize Handler
175
+ // ===================================================
176
+ const handleResize = () => {
177
+ dtInstance?.columns.adjust();
178
+ };
179
+
180
+ // ===================================================
181
+ // Watch: Data changes
182
+ // ===================================================
183
+ watch(
184
+ () => internalData.value,
185
+ async (newData) => {
186
+ if (!dtInstance) return;
187
+ await nextTick();
188
+
189
+ dtInstance.clear();
190
+
191
+ if (Array.isArray(newData) && newData.length > 0) {
192
+ dtInstance.rows.add(newData);
193
+ }
194
+
195
+ dtInstance.draw(false);
196
+ dtInstance.columns.adjust();
197
+
198
+ if (props.responsive) {
199
+ dtInstance.responsive?.recalc();
200
+ }
201
+ },
202
+ { deep: false },
203
+ );
204
+
205
+ // ===================================================
206
+ // Expose public API
207
+ // ===================================================
208
+ defineExpose({
209
+ /** Dapatkan DataTables instance untuk akses API langsung */
210
+ getInstance: (): Api<any> | null => dtInstance,
211
+ /** Reload data secara manual */
212
+ reload: () => {
213
+ if (!dtInstance) return;
214
+ dtInstance.ajax?.reload(undefined, false);
215
+ },
216
+ /** Clear & redraw tabel */
217
+ redraw: () => {
218
+ if (!dtInstance) return;
219
+ dtInstance.draw(false);
220
+ },
221
+ /** Adjust column widths */
222
+ adjustColumns: () => {
223
+ dtInstance?.columns.adjust();
224
+ },
225
+ });
226
+
227
+ // ===================================================
228
+ // Cleanup
229
+ // ===================================================
230
+ onBeforeUnmount(() => {
231
+ window.removeEventListener("resize", handleResize);
232
+
233
+ if (dtInstance) {
234
+ dtInstance.off("draw");
235
+ dtInstance.off("select");
236
+ dtInstance.off("deselect");
237
+ }
238
+
239
+ resizeObserver?.disconnect();
240
+ resizeObserver = null;
241
+
242
+ dtInstance?.destroy(true);
243
+ dtInstance = null;
244
+ });
245
+ </script>
246
+
247
+ <template>
248
+ <div ref="containerRef" :class="['dt-wrapper', wrapperClass]" style="position: relative">
249
+ <!-- Loading Overlay -->
250
+ <Transition name="dt-fade">
251
+ <div v-if="loading" class="dt-loading-overlay">
252
+ <div class="dt-loading-spinner" />
253
+ <span class="dt-loading-text">{{ loadingText }}</span>
254
+ </div>
255
+ </Transition>
256
+
257
+ <DataTableComponent
258
+ v-if="isMounted"
259
+ ref="datatableRef"
260
+ :class="['table', 'table-hover', 'table-bordered', 'table-sm', 'w-100', tableClass]"
261
+ :columns="internalColumns as any"
262
+ :data="internalData"
263
+ :options="tableOptions as unknown as any"
264
+ >
265
+ <slot />
266
+ </DataTableComponent>
267
+
268
+ <!-- SSR Fallback: Render a simple table to avoid layout shift -->
269
+ <table
270
+ v-else
271
+ :class="['table', 'table-hover', 'table-bordered', 'table-sm', 'w-100', 'dataTable', tableClass]"
272
+ >
273
+ <thead>
274
+ <tr>
275
+ <th v-for="col in internalColumns" :key="col.data || col.title">
276
+ {{ col.title }}
277
+ </th>
278
+ </tr>
279
+ </thead>
280
+ <tbody>
281
+ <slot />
282
+ </tbody>
283
+ </table>
284
+ </div>
285
+ </template>
286
+
287
+ <style scoped>
288
+ .dt-wrapper {
289
+ width: 100%;
290
+ overflow: hidden;
291
+ }
292
+
293
+ /* ---- Loading Overlay ---- */
294
+ .dt-loading-overlay {
295
+ position: absolute;
296
+ inset: 0;
297
+ z-index: 10;
298
+ display: flex;
299
+ flex-direction: column;
300
+ align-items: center;
301
+ justify-content: center;
302
+ gap: var(--space-3);
303
+ background: rgba(255, 255, 255, 0.7);
304
+ backdrop-filter: blur(4px);
305
+ border-radius: var(--radius-lg);
306
+ transition: var(--transition);
307
+ }
308
+
309
+ .dt-loading-spinner {
310
+ width: 40px;
311
+ height: 40px;
312
+ border: 3px solid var(--bg-mute);
313
+ border-top-color: var(--primary);
314
+ border-radius: 50%;
315
+ animation: dt-spin 0.8s cubic-bezier(0.4, 0, 0.2, 1) infinite;
316
+ }
317
+
318
+ .dt-loading-text {
319
+ font-size: var(--fs-sm);
320
+ color: var(--text-soft);
321
+ font-weight: var(--fw-medium);
322
+ letter-spacing: 0.025em;
323
+ }
324
+
325
+ @keyframes dt-spin {
326
+ to {
327
+ transform: rotate(360deg);
328
+ }
329
+ }
330
+
331
+ /* ---- Fade Transition ---- */
332
+ .dt-fade-enter-active,
333
+ .dt-fade-leave-active {
334
+ transition: opacity 0.3s ease;
335
+ }
336
+
337
+ .dt-fade-enter-from,
338
+ .dt-fade-leave-to {
339
+ opacity: 0;
340
+ }
341
+ </style>