Membuat Fitur Datatable dengan Techstack Laravel + Next.js
Membuat Aplikasi Fullstack Dengan Laravel dan Next.js — Pt.3
Halo gaes, dalam aplikasi web tentu sudah tidak asing lagi dengan yang namanya datatable.
Yup, datatable berfungsi untuk menampilkan data-data — biasanya dari database — ke dalam bentuk table di frontend nya.
Pada postingan kali ini kita akan membahas bagaimana cara membuat fitur datatable ini dengan menggunakan techstack Laravel + Next.js dengan pagination yang diproses di server-side.
Postingan ini merupakan lanjutan dari postingan sebelumnya yang membahas tentang cara insert data pada techstack Laravel + Next.js.
Baik, kita langsung saja ke pembuatan fitur nya ya.
Backend
Pertama kita akan membuat api untuk meng-handle request dan mengembalikan response yang biasa digunakan pada datatable.
Model
Edit file model App/Models/Product.php
, lalu tambahkan kode protected $appends
danfunction imageUrl
seperti di bawah ini:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
use HasFactory;
protected $fillable = ['name', 'description', 'image', 'price', 'stock',];
protected $appends = ['image_url'];
/**
* Get the image URL.
*
* @return \Illuminate\Database\Eloquent\Casts\Attribute
*/
protected function imageUrl(): Attribute
{
return Attribute::make(
get: fn () => $this->image ? asset(str_replace('public/', 'storage/', $this->image)) : $this->image,
);
}
}
Kode tersebut berfungsi untuk menambahkan atribut image_url
pada model Product dan mereplace teks public/
dengan storage/
agar image bisa sudah diupload sebelumnya ke folder storage diakses melalui url.
Resource
Selanjutnya kita akan membuat resource
agar response nya bisa sesuai dengan standar format output API dan lebih mudah untuk dimanage.
Pastikan kita berada di folder laranext-be
, lalu jalankan perintah:
php artisan make:resource ProductResource
Perintah tersebut akan membuat file app/Http/Resources/ProductResource.php
Dan pastikan isinya seperti berikut:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class ProductResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
*/
public function toArray($request)
{
return parent::toArray($request);
}
}
Resource adalah format untuk output single data, sedangkan untuk output yang bentuknya list kita perlu membuat resource collection.
Jalankan perintah:
php artisan make:resource ProductCollection --collection
Perintah tersebut akan membuat file app/Http/Resources/ProductCollection.php
Dan pastikan juga isinya seperti berikut:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\ResourceCollection;
class ProductCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* @param \Illuminate\Http\Request $request
* @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
*/
public function toArray($request)
{
return parent::toArray($request);
}
}
Controller
Setelah resource nya siap, kita akan membuat logic untuk menerima request dan mengembalikan response untuk kebutuhan datatable nya.
Kita akan membuat dan mengedit function-function pada classProductController
seperti berikut ini:
use App\Http\Resources\ProductResource;
// ... other codes
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index(Request $request)
{
$product = app(Product::class);
$product = $this->resolveGlobalFilter($product, $request);
$product = $this->resolveSort($product, $request);
$products = $product->paginate($this->resolvePageSize($request));
return ProductResource::collection($products);
}
function resolveGlobalFilter($model, Request $request)
{
if (!is_null($request->globalFilter) && trim($request->globalFilter) != '') {
$model = $model->where('name', 'like', '%' . $request->globalFilter . '%')->orWhere('description', 'like', '%' . $request->globalFilter . '%');
}
return $model;
}
function resolveSort($model, Request $request)
{
if (!is_null($request->sortColumn)) {
$sortDirection = $request->sortDirection == 'asc' ? 'asc' : 'desc';
$model = $model->orderBy($request->sortColumn, $sortDirection);
}
return $model;
}
function resolvePageSize(Request $request)
{
if (is_null($request->pageSize) or $request->pageSize > 100) {
return 10; // set default to 10 and limit pageSize to 100 to avoid overload
} else {
return $request->pageSize;
}
}
Function utama untuk kebutuhan datatable adalah function index
. Function tersebut memproses request dari client dan mengembalikan response berupa collection dari ProductResource
yang sudah di paginasi per halaman. Jumlah item per halamanya diset default 10 dan maksimal 100 oleh function resolvePageSize
agar server tidak overload.
Selain per halaman, sudah dihandle juga filter untuk pencarian pada column name
dan description
oleh function resolveGlobalFilter
Tidak lupa sort by column tertentu baik ascending maupun descending sudah dihandle oleh function resolveSort
Frontend
Setelah backend ready, kita lanjut ke kodingan untuk frontend nya agar user bisa berinteraksi dengan datatable nya
Library Datatable
Library datatable yang akan kita gunakan untuk case kali ini adalah react-data-table-component karena library tersebut cukup mudah untuk digunakan dengan fitur yang cukup untuk kebutuhan datatable pada umumnya.
Install library yang dibutuhkan dengan perintah:
npm install react-data-table-component styled-components react-debounce-input
react-data-table-component
adalah library utama yang diperlukan untuk membuat datatable.
styled-components
adalah library untuk style tampilan table-nya yang dibutuhkan oleh react-data-table-component
react-debounce-input
adalah library untuk kebutuhan searching realtime pada sebuah input html agar proses searching dibatasi (debounced) sehingga tidak memberatkan server.
Navigation
Lalu tambahkan kode berikut pada file src/components/Layouts/Navigation.js
setelah baris <NavLink href="dashboard" …
seperti berikut:
<NavLink
href="/products"
active={router.pathname === '/products'}>
Products
</NavLink>
Page
Buat file src/pages/products/index.js
lalu isi dengan kode berikut:
import { useAuth } from "@/hooks/auth"
import axios from "@/lib/axios"
import Head from "next/head"
import Link from "next/link"
import { useMemo, useState, useEffect } from "react"
import DataTable from "react-data-table-component"
import { DebounceInput } from "react-debounce-input"
function Products() {
const { user } = useAuth({ middleware: 'guest' })
const [data, setData] = useState([])
const [globalFilter, setGlobalFilter] = useState('')
const [loading, setLoading] = useState(false)
const [page, setPage] = useState(1)
const [perPage, setPerPage] = useState(10)
const [sortColumn, setSortColumn] = useState()
const [sortDirection, setSortDirection] = useState()
const [totalRows, setTotalRows] = useState(0)
const columns = useMemo(() => [
{
name: 'ID',
selector: row => row.id,
sortable: true,
},
{
name: 'Name',
selector: row => row.name,
sortable: true,
},
{
name: 'Description',
selector: row => row.description,
sortable: true,
},
{
name: 'Image',
sortable: false,
cell: (row, index, column, id) => {
return row.image_url ? <img src={row.image_url} alt={row.name} /> : '';
},
},
{
name: 'Price',
selector: row => row.price,
sortable: true,
format: (row, index) => {
return Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', maximumFractionDigits: 0 }).format(row.price);
},
right: true,
},
{
name: 'Stock',
selector: row => row.stock,
sortable: true,
format: (row, index) => {
return Intl.NumberFormat('id-ID').format(row.stock);
},
right: true,
},
])
const fetchData = async (page, perPage, sortColumn, sortDirection, globalFilter) => {
setLoading(true)
const response = await axios.get(
`/api/products`, { params: { page: page, pageSize: perPage, sortColumn: sortColumn, sortDirection: sortDirection, globalFilter: globalFilter } }
)
setData(response.data.data)
setTotalRows(response.data.meta.total)
setLoading(false)
}
const handlePageChange = page => {
setPage(page)
}
const handlePerRowsChange = (newPerPage, page) => {
setPerPage(newPerPage)
setPage(page)
}
const handleGlobalFilterChange = (value) => {
setGlobalFilter(value)
}
const handleSortChange = (column, sortDirection) => {
setSortColumn(column.name)
setSortDirection(sortDirection)
}
useEffect(() => {
fetchData(page, perPage, sortColumn, sortDirection, globalFilter)
}, [page, perPage, sortColumn, sortDirection, globalFilter])
const customStyles = {
headCells: {
style: {
fontWeight: 'bold',
},
}
};
return (
<>
<Head>
<title>Laranext - Products</title>
</Head>
<div className="relative flex items-top justify-center min-h-screen bg-gray-100 dark:bg-gray-900 sm:items-center sm:pt-0">
<div className="hidden fixed top-0 right-0 px-6 py-4 sm:block">
{user ? (
<Link
href="/dashboard"
className="ml-4 text-sm text-gray-700 underline">
Dashboard
</Link>
) : (
<>
<Link
href="/login"
className="text-sm text-gray-700 underline">
Login
</Link>
<Link
href="/register"
className="ml-4 text-sm text-gray-700 underline">
Register
</Link>
</>
)}
</div>
<div className="max-w-6xl mx-auto sm:px-6 lg:px-8 overflow-x-auto ">
<div className="py-2">
<Link
className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm w-full sm:w-auto px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
href="/products/create">
Create New Product
</Link>
</div>
<DebounceInput
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 my-4"
placeholder="Search..."
minLength={1}
debounceTimeout={500}
onChange={event => (handleGlobalFilterChange(event.target.value))}
/>
<div className="rounded-lg p-10 bg-white">
<DataTable
title="Products"
columns={columns}
data={data}
// progressPending={loading}
pagination
paginationServer
paginationTotalRows={totalRows}
sortServer
onSort={handleSortChange}
onChangeRowsPerPage={handlePerRowsChange}
onChangePage={handlePageChange}
customStyles={customStyles}
/>
</div>
</div>
</div>
</>
)
}
export default Products
Komponen datatable utamanya adalah DataTable
Dan function utama yang memanggil API backend adalah fetchData
Di dalam komponen DataTable
menyediakan props yang bisa dipanggil ketika terjadi perubahan pada parameter seperti page, sort, dll
Di dalam komponen page, kita menyiapkan state yang bisa menyimpan parameter-parameter tersebut dengan hook useState
. Dan ketika terjadi perubahan pada state, otomatis akan dipanggil api fetchData
dengan hook useEffect
Result
Jalankan dan pastikan backend dan front-end nya running seperti pada tutorial sebelumnya.
Lalu buka alamat ini pada browser http://localhost:3000/products/
Data yang ada pada database akan ditampilkan dalam bentuk datatable seperti pada contoh di gambar.
Jika belum ada data, coba klik tombol Create New Product
yang akan meredirect ke fitur form create yang sudah kita buat sebelumnya. Lalu isi beberapa data sample.
Coba juga fitur sort nya dengan mengklik salah satu header table nya:
Lalu coba juga fitur filter nya dengan mengetik teks yang dicari pada search box:
Penutup
Demikian cara menampilkan data dalam bentuk datatable dengan techstack Laravel + Next.js.
Cukup simple dan mudah kan? 👍
Silakan komentar jika ada pertanyaan ataupun hal lain yang ingin disampaikan.
Jangan lupa untuk clap dan follow agar tidak ketinggalan info jika ada postingan baru ya☺️
Akhir kata, semoga sharingan kecil ini bisa bermanfaat bagi pembaca semua, khusunya bagi penulis sendiri.
Keep coding dan sampai jumpa di postingan berikutnya…
Wassalam…