Tutorial Inertia Roles & Permissions - #11 - Membuat Module Post Dengan Inertia React

Artikel ini merupakan series dari Tutorial Laravel Inertia Roles & Permissions, disini kita akan membuat sebuah module post dengan inertia react.

Rafi Taufiqurrahman
Dipublish 25/07/2024

Pendahuluan

Pada artikel sebelumnya kita telah berhasil membuat module dashboard, dan pada artikel kali ini kita akan lajutkan untuk melakukan pembuatan module posts.

Membuat Controller Posts

Silahkan teman - teman buka terminal-nya, kemudian jalankan perintah berikut ini :

Terminal
php artisan make:controller PostController -r

Jika perintah diatas berhasil dijalankan, maka kita akan mendapatkan sebuah file yang terletak di app/Http/Controllers dengan nama PostController.php.

Silahkan buka file tersebut, kemudian ubah semua kodenya sebagai berikut.

PostController.php
<?php

namespace App\Http\Controllers;

use App\Models\Post;
use Illuminate\Http\Request;
use Illuminate\Routing\Controllers\HasMiddleware;
use Illuminate\Routing\Controllers\Middleware;

class PostController extends Controller implements HasMiddleware
{
    public static function middleware()
    {
        return [
            new Middleware('permission:posts-access', only: ['index']),
            new Middleware('permission:posts-create', only: ['store']),
            new Middleware('permission:posts-update', only: ['update']),
            new Middleware('permission:posts-delete', only: ['destroy']),
        ];
    }

    /**
     * Display a listing of the resource.
     */
    public function index(Request $request)
    {
        // get all posts
        $posts = Post::with('user')
            ->whereUserId($request->user()->id)
            ->when($request->search, fn($query) => $query->where('title', 'like', '%'.$request->search.'%'))
            ->latest()
            ->paginate(6)->withQueryString();

        // render view
        return inertia('Posts/Index', ['posts' => $posts]);
    }

    /**
     * Store a newly created resource in storage.
     */
    public function store(Request $request)
    {
        // validate request
        $request->validate([
            'title' => 'required|min:3|max:255',
            'content' => 'required|min:3|max:255',
        ]);

        // create new post
        Post::create([
            'user_id' => $request->user()->id,
            'title' => $request->title,
            'content' => $request->content,
        ]);

        // render view
        return back();
    }

    /**
     * Update the specified resource in storage.
     */
    public function update(Request $request, Post $post)
    {
        // validate request
        $request->validate([
            'title' => 'required|min:3|max:255',
            'content' => 'required|min:3|max:255',
        ]);

        // update post
        $post->update([
            'title' => $request->title,
            'content' => $request->content,
        ]);

        // render view
        return back();
    }

    /**
     * Remove the specified resource from storage.
     */
    public function destroy(Post $post)
    {
        // delete post
        $post->delete();

        // render view
        return back();
    }
}

Didalam class PostController kita memiliki 7 method yang kita generate diawal dengan perintah.

Terminal
php artisan make:controller PostController -r

-r disini maksudnya adalah resources , jadi kita membuat PostController dengan full resource sehingga kita mendapatkan 7 method sekaligus dengan satu perintah artisan, tapi disini kita hanya akan menggunakan 4 method yaitu :

  • index
  • store
  • update
  • destroy

Dari kode diatas, pertama kita lakukan import model post.

PostController.php
use App\Models\Post;

Kemudian kita juga melakukan implementasi sebuah interface middleware terbaru yang telah disediakan oleh laravel dengan cara menambahkan kata kunci implements HasMiddleware pada controller kita, kemudian kita import agar tidak error.

PostController.php
use Illuminate\Routing\Controllers\HasMiddleware;
use Illuminate\Routing\Controllers\Middleware;

Selanjutnya kita bisa menggunakan sebuah method dari interface HasMiddleware seperti kode dibawah ini.

PostController.php
public static function middleware()
{
    return [
        new Middleware('permission:posts-access', only: ['index']),
        new Middleware('permission:posts-create', only: ['store']),
        new Middleware('permission:posts-update', only: ['update']),
        new Middleware('permission:posts-delete', only: ['destroy']),
    ];
}

Dari kode diatas kita menerapkan beberapa middleware yang berfungsi untuk melakukan pembatasan akses method - method yang telah kita generate sesuai dengan hak akses yang dimiliki oleh user.

Method Index

Method ini kita gunakan untuk menampilkan seluruh data posts yang ada didalam database.

PostController.php
public function index(Request $request)
{
    // get all posts
    $posts = Post::with('user')
        ->whereUserId($request->user()->id)
        ->when($request->search, fn($query) => $query->where('title', 'like', '%'.$request->search.'%'))
        ->latest()
        ->paginate(6)->withQueryString();

    // render view
    return inertia('Posts/Index', ['posts' => $posts]);
}

Pertama - tama kita membuat sebuah variabel posts yang kita gunakan untuk menampilkan sebuah data dari tabel posts. Data yang akan kita tampilkan, tentu saja hanya data posts yang dimiliki oleh pengguna yang sedang login menggunakan method whereUserId. selain menampilkan data, kita juga menambahkan sebuah pencarian data posts ketika method when dieksekusi. Method tersebut akan dieksekusi ketika kita mengirimkan sebuah request dengan nama search dan ketika kita mengirimkan request tersebut, maka kita akan melakukan filter data posts menggunakan method where , selain itu kita juga melakukan pembatasan data yang ditampilkan perhalamanya hanya sebanyak 6 data menggunaakn method paginate dan yang terakhir kita urutkan datanya sesuai data yang paling baru menggunakan method latest.

PostController.php
// get all posts
$posts = Post::with('user')
    ->whereUserId($request->user()->id)
    ->when($reques t->search, fn($query) => $query->where('title', 'like', '%'.$request->search.'%'))
    ->latest()
    ->paginate(6);

Kemudian data diatas, kita kirimkan ke sebuah view dengan nama Posts/Index.jsx, dalam bentuk props.

PostController.php
// render view
return inertia('Posts/Index', ['posts' => $posts]);

Method Store

Method ini kita gunakan untuk melakukan proses insert data baru kedalam tabel posts.

PostController.php
public function store(Request $request)
{
    // validate request
    $request->validate([
        'title' => 'required|min:3|max:255',
        'content' => 'required|min:3|max:255',
    ]);

    // create new post
    Post::create([
        'user_id' => $request->user()->id,
        'title' => $request->title,
        'content' => $request->content,
    ]);

    // render view
    return back();
}

Pada kode diatas, pertama - tama kita melakukan validasi data terlebih dahulu sebelum melakukan insert data kedalam database menggunakan method validate.

PostController.php
// validate request
$request->validate([
    'title' => 'required|min:3|max:255',
    'content' => 'required|min:3|max:255',
]);

Selanjutnya jika request yang kita kirimkan sudah sesuai dengan kriteria validasi yang kita definisikan, maka kita akan melakukan insert data baru kedalam tabel posts menggunakan method create.

PostController.php
// create new post
Post::create([
    'user_id' => $request->user()->id,
    'title' => $request->title,
    'content' => $request->content,
]);

Setelah berhasil menambahkan data baru, kita akan diarahkan ke sebuah route posts.index.

PostController.php
// render view
return back();

Method Update

Method ini kita gunakan untuk melakukan proses update data yang ada didalam tabel posts kita.

PostController.php
public function update(Request $request, Post $post)
{
    // validate request
    $request->validate([
        'title' => 'required|min:3|max:255',
        'content' => 'required|min:3|max:255',
    ]);

    // update post
    $post->update([
        'title' => $request->title,
        'content' => $request->content,
    ]);

    // render view
    return back();
}

Pada kode diatas, method update kita modifikasi menggunakan sebuah route model binding yang menandakan bahwa variabel $post merupakan dirinya sendiri.

PostController.php
public function update(Request $request, Post $post)

Berikutnya kita definisikan sebuah validasi menggunakan method validate.

PostController.php
// validate request
$request->validate([
    'title' => 'required|min:3|max:255',
    'content' => 'required|min:3|max:255',
]);

Kemudian jika request yang kita kirimkan sudah sesuai dengan kriteria validasi yang kita definisikan, maka kita akan melakukan insert data baru kedalam tabel posts menggunakan method update.

PostController.php
// update post
$post->update([
    'title' => $request->title,
    'content' => $request->content,
]);

Setelah berhasil melakukan update data, kita akan diarahkan ke sebuah route posts.index.

PostController.php
// render view
return back();

Method Destroy

Method ini kita gunakan untuk melakukan proses penghapusan data posts yang kita miliki.

PostController.php
public function destroy(Post $post)
{
    // delete post
    $post->delete();

    // render view
    return back();
}

Pada kode diatas, method destroy kita modifikasi menggunakan sebuah route model binding yang menandakan bahwa variabel $post merupakan dirinya sendiri.

PostController.php
public function destroy(Post $post)

Selanjutnya kita lakukan penghapusan data menggunakan method delete.

PostController.php
// delete post
$post->delete();

Terakhir setelah data berhasil dihapus, kita akan diarahkan ke sebuah route posts.index.

PostController.php
// render view
return back();

Membuat Route Posts

Setelah berhasil membuat sebuah controller Post, sekarang kita akan lanjutkan untuk pembuatan route-nya, silahkan teman - teman buka file routes/web.php, kemudian ubah kode-nya menjadi seperti berikut ini.

web.php
<?php

use App\Http\Controllers\DashboardController;
use App\Http\Controllers\PostController;
use App\Http\Controllers\ProfileController;
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;

Route::get('/', function () {
    return Inertia::render('Welcome', [
        'canLogin' => Route::has('login'),
        'canRegister' => Route::has('register'),
        'laravelVersion' => Application::VERSION,
        'phpVersion' => PHP_VERSION,
    ]);
});

Route::middleware('auth')->group(function () {
    // dashboard route
    Route::get('/dashboard', DashboardController::class)->name('dashboard');
    // posts route
    Route::resource('/posts', PostController::class)->only('index', 'store', 'update', 'destroy');
    // profile route
    Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
    Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
    Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
});

require __DIR__.'/auth.php

Dari perubahan kode diatas, kita menambahkan sebuah route baru dengan nama posts, untuk memastikan route yang kita buat telah berfungsi, teman - teman bisa jalankan.

Terminal
php artisan r:l --name=posts

Setelah perintah artisan diatas dijalankan, maka kita akan mendapatkan output, kurang lebih seperti berikut ini.

 GET|HEAD        posts ..................................................................... posts.index › PostController@index
  POST            posts ..................................................................... posts.store › PostController@store
  PUT|PATCH       posts/{post} ............................................................ posts.update › PostController@update
  DELETE          posts/{post} .......................................................... posts.destroy › PostController@destroy

Membuat Navigasi Posts

Silahkan teman - teman buka file yang bernama AuthenticatedLayout.jsx yang terletak di folder resources/js/layouts, kemudian ubah kodenya menjadi seperti berikut ini :

import { useState } from 'react';
import ApplicationLogo from '@/Components/ApplicationLogo';
import Dropdown from '@/Components/Dropdown';
import NavLink from '@/Components/NavLink';
import ResponsiveNavLink from '@/Components/ResponsiveNavLink';
import { Link } from '@inertiajs/react';
import hasAnyPermission from '@/Utils/Permissions';

export default function Authenticated({ user, header, children }) {
    const [showingNavigationDropdown, setShowingNavigationDropdown] = useState(false);

    return (
        <div className="min-h-screen bg-gray-100">
            <nav className="bg-white border-b border-gray-100">
                <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
                    <div className="flex justify-between h-16">
                        <div className="flex">
                            <div className="shrink-0 flex items-center">
                                <Link href="/">
                                    <ApplicationLogo className="block h-9 w-auto fill-current text-gray-800" />
                                </Link>
                            </div>

                            <div className="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex">
                                <NavLink href={route('dashboard')} active={route().current('dashboard')}>
                                    Dashboard
                                </NavLink>
                                {hasAnyPermission(['posts-access']) &&
                                    <NavLink href={route('posts.index')} active={route().current('posts*')}>
                                        Posts
                                    </NavLink>
                                }
                            </div>
                        </div>

                        <div className="hidden sm:flex sm:items-center sm:ms-6">
                            <div className="ms-3 relative">
                                <Dropdown>
                                    <Dropdown.Trigger>
                                        <span className="inline-flex rounded-md">
                                            <button
                                                type="button"
                                                className="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 bg-white hover:text-gray-700 focus:outline-none transition ease-in-out duration-150"
                                            >
                                                {user.name}

                                                <svg
                                                    className="ms-2 -me-0.5 h-4 w-4"
                                                    xmlns="http://www.w3.org/2000/svg"
                                                    viewBox="0 0 20 20"
                                                    fill="currentColor"
                                                >
                                                    <path
                                                        fillRule="evenodd"
                                                        d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
                                                        clipRule="evenodd"
                                                    />
                                                </svg>
                                            </button>
                                        </span>
                                    </Dropdown.Trigger>

                                    <Dropdown.Content>
                                        <Dropdown.Link href={route('profile.edit')}>Profile</Dropdown.Link>
                                        <Dropdown.Link href={route('logout')} method="post" as="button">
                                            Log Out
                                        </Dropdown.Link>
                                    </Dropdown.Content>
                                </Dropdown>
                            </div>
                        </div>

                        <div className="-me-2 flex items-center sm:hidden">
                            <button
                                onClick={() => setShowingNavigationDropdown((previousState) => !previousState)}
                                className="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 focus:text-gray-500 transition duration-150 ease-in-out"
                            >
                                <svg className="h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
                                    <path
                                        className={!showingNavigationDropdown ? 'inline-flex' : 'hidden'}
                                        strokeLinecap="round"
                                        strokeLinejoin="round"
                                        strokeWidth="2"
                                        d="M4 6h16M4 12h16M4 18h16"
                                    />
                                    <path
                                        className={showingNavigationDropdown ? 'inline-flex' : 'hidden'}
                                        strokeLinecap="round"
                                        strokeLinejoin="round"
                                        strokeWidth="2"
                                        d="M6 18L18 6M6 6l12 12"
                                    />
                                </svg>
                            </button>
                        </div>
                    </div>
                </div>

                <div className={(showingNavigationDropdown ? 'block' : 'hidden') + ' sm:hidden'}>
                    <div className="pt-2 pb-3 space-y-1">
                        <ResponsiveNavLink href={route('dashboard')} active={route().current('dashboard')}>
                            Dashboard
                        </ResponsiveNavLink>
                        {hasAnyPermission(['posts-access']) &&
                            <ResponsiveNavLink href={route('posts.index')} active={route().current('posts*')}>
                                Posts
                            </ResponsiveNavLink>
                        }
                    </div>

                    <div className="pt-4 pb-1 border-t border-gray-200">
                        <div className="px-4">
                            <div className="font-medium text-base text-gray-800">{user.name}</div>
                            <div className="font-medium text-sm text-gray-500">{user.email}</div>
                        </div>

                        <div className="mt-3 space-y-1">
                            <ResponsiveNavLink href={route('profile.edit')}>Profile</ResponsiveNavLink>
                            <ResponsiveNavLink method="post" href={route('logout')} as="button">
                                Log Out
                            </ResponsiveNavLink>
                        </div>
                    </div>
                </div>
            </nav>

            {header && (
                <header className="bg-white shadow">
                    <div className="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">{header}</div>
                </header>
            )}

            <main>{children}</main>
        </div>
    );
}

Pada kode diatas, kita memanggil 2 buah component dengan nama NavLink dan ResponsiveNavLink dengan props href yang kita set ke route yang bernama posts.index dan props active kita set ke semua route posts* dan tidak lupa kita tambahan pengecekan sebuah hak akses yang dimiliki oleh user menggunakan utils hasAnyPermissions yang telah kita buat sebelumnya.

{hasAnyPermission(['posts-access']) &&
    <NavLink href={route('posts.index')} active={route().current('posts*')}>
        Posts
    </NavLink>
}

{hasAnyPermission(['posts-access']) &&
    <ResponsiveNavLink href={route('posts.index')} active={route().current('posts*')}>
        Posts
    </ResponsiveNavLink>
}

Membuat View Posts

Silahkan teman - teman buat folder baru dengan nama Posts kemudian didalam folder tersebut buat file baru dengan nama Index.jsx yang akan kita letakan di resources/js/Pages, kemudian ubah kodenya menjadi seperti berikut ini.

Index.jsx
import React from 'react'
import { Head, usePage, useForm } from '@inertiajs/react';
import { IconPencilCog, IconPlus } from '@tabler/icons-react';
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import Container from '@/Components/Container';
import Table from '@/Components/Table';
import Button from '@/Components/Button';
import Pagination from '@/Components/Pagination';
import Search from '@/Components/Search';
import hasAnyPermission from '@/Utils/Permissions';
import Modal from '@/Components/Modal';
import Card from '@/Components/Card';
import Input from '@/Components/Input';
import Textarea from '@/Components/Textarea';
import Swal from 'sweetalert2';
export default function Index({auth}) {

    // destruct posts props
    const { posts } = usePage().props;

    // define state with helper inertia
    const { data, setData, post, errors, transform } = useForm({
        id: '',
        title : '',
        content : '',
        modalOpen: false,
        isUpdateMode: false,
    })

    // define method handleModalUpdate
    const handleModalUpdate = (post) => {
        console.log(post);
        setData(prevData => ({
            ...prevData,
            id: post.id,
            title: post.title,
            content: post.content,
            modalOpen: true,
            isUpdateMode: true
        }))
    }

    transform((data) => ({
        ...data,
        _method: data.isUpdateMode ? 'put' : 'post',
    }))


    // define method handleStoreData
    const handleStoreData = async (e) => {
        e.preventDefault();

        post(route('posts.store'), {
            onSuccess: () => {
                setData('modalOpen', !data.modalOpen);
                Swal.fire({
                    title: 'Success!',
                    text: 'Data created successfully!',
                    icon: 'success',
                    showConfirmButton: false,
                    timer: 1500
                });
            }
        });
    }

    // define method handleUpdateData
    const handleUpdateData = async (e) => {
        e.preventDefault();

        post(route('posts.update', data.id), {
            onSuccess: () => {
                setData('modalOpen', !data.modalOpen);
                Swal.fire({
                    title: 'Success!',
                    text: 'Data updated successfully!',
                    icon: 'success',
                    showConfirmButton: false,
                    timer: 1500
                });
            }
        });
    }

    return (
        <AuthenticatedLayout
            user={auth.user}
            header={<h2 className="font-semibold text-xl text-gray-800 leading-tight">Posts</h2>}
        >
            <Head title={'Posts'}/>
            <Container>
                <Modal show={data.modalOpen} onClose={() => setData('modalOpen', false)}>
                    <Card title={data.isUpdateMode ? 'Update Post' : 'Create New Post'}>
                        <form onSubmit={data.isUpdateMode ? handleUpdateData : handleStoreData}>
                            <div className='mb-4'>
                                <Input label={'Title'} type={'text'} value={data.title} onChange={e => setData('title', e.target.value)} errors={errors.title} placeholder="Input title post.."/>
                            </div>
                            <div className='mb-4'>
                                <Textarea rows={4} label={'Content'} value={data.content} onChange={e => setData('content', e.target.value)} errors={errors.content} placeholder="Input content post.."/>
                            </div>
                            <div className='flex items-center gap-2'>
                                <Button type={'submit'}/>
                            </div>
                        </form>
                    </Card>
                </Modal>
                <div className='mb-4 flex items-center justify-between gap-4'>
                    {hasAnyPermission(['posts-create']) &&
                        <Button className={'bg-white text-gray-700 hover:bg-gray-100'} type={'modal'} onClick={() => setData({modalOpen: !data.modalOpen})}>
                            <IconPlus size={18} strokeWidth={1.5}/> <span className='hidden lg:flex'>Create New Data</span>
                        </Button>
                    }
                    <div className='w-full md:w-4/6'>
                        <Search url={route('posts.index')} placeholder={'Search posts data by title...'}/>
                    </div>
                </div>
                <Table.Card title={'Posts'}>
                    <Table>
                        <Table.Thead>
                            <tr>
                                <Table.Th>#</Table.Th>
                                <Table.Th>Title</Table.Th>
                                <Table.Th>Content</Table.Th>
                                <Table.Th>Action</Table.Th>
                            </tr>
                        </Table.Thead>
                        <Table.Tbody>
                            {posts.data.map((post, i) => (
                                <tr key={i}>
                                    <Table.Td>{++i + (posts.current_page-1) * posts.per_page}</Table.Td>
                                    <Table.Td>{post.title}</Table.Td>
                                    <Table.Td>
                                        <div className='whitespace-pre-wrap'>
                                            {post.content}
                                        </div>
                                    </Table.Td>
                                    <Table.Td>
                                        <div className='flex items-center gap-2'>
                                            {hasAnyPermission(['post-update']) &&
                                                <Button className={'bg-orange-50 text-orange-500 hover:bg-orange-100 border-0'} type={'modal'} onClick={() => handleModalUpdate(post)}>
                                                    <IconPencilCog size={18} strokeWidth={1.5}/>
                                                </Button>
                                            }
                                            {hasAnyPermission(['posts-delete']) &&
                                                <Button type={'delete'} url={route('posts.destroy', post.id)}/>
                                            }
                                        </div>
                                    </Table.Td>
                                </tr>
                            ))}
                        </Table.Tbody>
                    </Table>
                </Table.Card>
                <div className='flex items-center justify-center'>
                    {posts.last_page !== 1 && (<Pagination links={posts.links}/>)}
                </div>
            </Container>
        </AuthenticatedLayout>
    )
}

Pada kode diatas, pertama - tama kita import beberapa file yang kita butuhkan.

Index.jsx
import React from 'react'
import { Head, usePage, useForm } from '@inertiajs/react';
import { IconPencilCog, IconPlus } from '@tabler/icons-react';
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
import Container from '@/Components/Container';
import Table from '@/Components/Table';
import Button from '@/Components/Button';
import Pagination from '@/Components/Pagination';
import Search from '@/Components/Search';
import hasAnyPermission from '@/Utils/Permissions';
import Modal from '@/Components/Modal';
import Card from '@/Components/Card';
import Input from '@/Components/Input';
import Textarea from '@/Components/Textarea';
import Swal from 'sweetalert2';

Selanjutnya kita membuat sebuah React Function Component dengan props auth.

Index.jsx
export default function Index({auth})

Berikutnya kita melakukan destructing sebuah props posts yang kita dapatkan dari PostController.

Index.jsx
const { posts } = usePage().props;

Kemudian kita membuat sebuah state menggunakan form helper dari inertia.

Index.jsx
const { data, setData, post, errors, transform } = useForm({
    id: '',
    title : '',
    content : '',
    modalOpen: false,
    isUpdateMode: false,
})

Selanjutnya kita membuat sebuah method baru dengan nama handleModalUpdate dengan sebuah parameter post.

Index.jsx
const handleModalUpdate = (post) => {
    setData(prevData => ({
        ...prevData,
        id: post.id,
        title: post.title,
        content: post.content,
        modalOpen: true,
        isUpdateMode: true
    }))
}

Pada kode diatas, kita lakukan pembaruan beberapa value state yang telah kita definisikan sebelumnya menggunakan parameter post.

Index.jsx
setData(prevData => ({
    ...prevData,
    id: post.id,
    title: post.title,
    content: post.content,
    modalOpen: true,
    isUpdateMode: true
}))

Berikutnya kita memanfaatkan sebuah method transform yang telah disediakan oleh form helper dari inertia, yang fungsinya untuk memanipulasi data sebelum data dikirimkan ke server.

Index.jsx
transform((data) => ({
    ...data,
    _method: data.isUpdateMode ? 'put' : 'post',
}))

Pada kode diatas, kita memanggil sebuah method transform dengan menerima sebuah callback state data, kemudian kita melakukan clone state data menggunakan spread operator ...data, kemudian kita juga menambahkan state baru dengan nama _method.

Selanjutnya kita membuat sebuah method baru dengan nama handleStoreData yang kita gunakan untuk mengirimkan data kita ke server menggunakan form helper yang telah disediakan oleh inertia.

Index.jsx
const handleStoreData = async (e) => {
    e.preventDefault();

    post(route('posts.store'), {
        onSuccess: () => {
            setData('modalOpen', !data.modalOpen);
            Swal.fire({
                title: 'Success!',
                text: 'Data created successfully!',
                icon: 'success',
                showConfirmButton: false,
                timer: 1500
            });
        }
    });
}

JIka kita perhatikan, pada method post kita arahakan ke sebuah route yang bernama posts.store, dan ketika data berhasil dikirimkan, kita melakukan update sebuah state modalOpen dan memanggil sebuah sweet alert.

Index.jsx
post(route('posts.store'), {
    onSuccess: () => {
        setData('modalOpen', !data.modalOpen);
        Swal.fire({
            title: 'Success!',
            text: 'Data created successfully!',
            icon: 'success',
            showConfirmButton: false,
            timer: 1500
        });
    }
});

Berikutnya kita juga membuat sebuah method baru dengan nama handleUpdateData yang kita gunakan untuk melakukan proses update data dengan memanfaatkan form helper dari inertia.

Index.jsx
const handleUpdateData = async (e) => {
    e.preventDefault();

    post(route('posts.update', data.id), {
        onSuccess: () => {
            setData('modalOpen', !data.modalOpen);
            Swal.fire({
                title: 'Success!',
                text: 'Data updated successfully!',
                icon: 'success',
                showConfirmButton: false,
                timer: 1500
            });
        }
    });
}

Sama halnya dengan penjelasan method handleStoreData, disini kita hanya melakukan perubahan route menjadi posts.update dengan menambahkan value dari state data.id.

Setelah membuat beberapa logic diatas, kemudian kita melakukan return sebuah component layout, dengan nama AuthenticatedLayout dengan props user dan header.

Index.jsx
<AuthenticatedLayout
    user={auth.user}
    header={<h2 className="font-semibold text-xl text-gray-800 leading-tight">Posts</h2>}
>

</AuthenticatedLayout>

Berikutnya kita juga memanggil sebuah head component yang telah disedikan oleh inertia untuk mengganti sebuah title dari halaman kita.

Index.jsx
<Head title={'Posts'}/>

Kemudian kita memanggil sebuah component modal dengan beberapa props diantarnya show dan onClose.

Index.jsx
<Modal show={data.modalOpen} onClose={() => setData('modalOpen', false)}>
    <Card title={data.isUpdateMode ? 'Update Post' : 'Create New Post'}>
        <form onSubmit={data.isUpdateMode ? handleUpdateData : handleStoreData}>
            <div className='mb-4'>
                <Input label={'Title'} type={'text'} value={data.title} onChange={e => setData('title', e.target.value)} errors={errors.title} placeholder="Input title post.."/>
            </div>
            <div className='mb-4'>
                <Textarea rows={4} label={'Content'} value={data.content} onChange={e => setData('content', e.target.value)} errors={errors.content} placeholder="Input content post.."/>
            </div>
            <div className='flex items-center gap-2'>
                <Button type={'submit'}/>
            </div>
        </form>
    </Card>
</Modal>

Didalam component modal kita memanggil beberapa component yang telah kita buat seperti :

  • Card
  • Input
  • Textarea
  • Button

Selanjutnya kita memanggil sebuah utils hasAnyPermissions yang telah kita buat sebelumnya untuk melakukan pengecekan apakah user memiliki hak akses untuk melakukan penambahan data posts.

Index.jsx
{hasAnyPermission(['posts-create']) &&
  <Button className={'bg-white text-gray-700 hover:bg-gray-100'} type={'modal'} onClick={() => setData({modalOpen: !data.modalOpen})}>
      <IconPlus size={18} strokeWidth={1.5}/> <span className='hidden lg:flex'>Create New Data</span>
  </Button>
}

Kemudian kita memanggil sebuah component dengan nama Search dengan props url dan placeholder.

Index.jsx
<Search url={route('posts.index')} placeholder={'Search posts data by title...'}/>

Pada props url, kita arahkan pencarian data kita ke sebuah route yang bernama posts.index.

Berikutnya kita juga memanggil sebuah component table dengan props title.

Index.jsx
<Table.Card title={'Posts'}>
    <Table>
        <Table.Thead>
            <tr>
                <Table.Th>#</Table.Th>
                <Table.Th>Title</Table.Th>
                <Table.Th>Content</Table.Th>
                <Table.Th>Action</Table.Th>
            </tr>
        </Table.Thead>
        <Table.Tbody>
            {posts.data.map((post, i) => (
                <tr key={i}>
                    <Table.Td>{++i + (posts.current_page-1) * posts.per_page}</Table.Td>
                    <Table.Td>{post.title}</Table.Td>
                    <Table.Td>
                        <div className='whitespace-pre-wrap'>
                            {post.content}
                        </div>
                    </Table.Td>
                    <Table.Td>
                        <div className='flex items-center gap-2'>
                            {hasAnyPermission(['post-update']) &&
                                <Button className={'bg-orange-50 text-orange-500 hover:bg-orange-100 border-0'} type={'modal'} onClick={() => handleModalUpdate(post)}>
                                    <IconPencilCog size={18} strokeWidth={1.5}/>
                                </Button>
                            }
                            {hasAnyPermission(['posts-delete']) &&
                                <Button type={'delete'} url={route('posts.destroy', post.id)}/>
                            }
                        </div>
                    </Table.Td>
                </tr>
            ))}
        </Table.Tbody>
    </Table>
</Table.Card>

Pada kode diatas, kita melakukan perulangan data posts menggunakan method map.

Index.jsx
{posts.data.map((post, i) => (
	.....
))}

Selanjutnya kita juga melakukan pengecekan sebuah permissions yang dimiliki oleh users dengan menggunakan utils yang telah kita buat sebelumnya.

Index.jsx
{hasAnyPermission(['post-update']) &&
  <Button className={'bg-orange-50 text-orange-500 hover:bg-orange-100 border-0'} type={'modal'} onClick={() => handleModalUpdate(post)}>
      <IconPencilCog size={18} strokeWidth={1.5}/>
  </Button>
}
{hasAnyPermission(['posts-delete']) &&
  <Button type={'delete'} url={route('posts.destroy', post.id)}/>
}

Terakhir kita juga memanggil sebuah component pagination dengan sebuah props links.

Index.jsx
{posts.last_page !== 1 && (<Pagination links={posts.links}/>)}

Screenshoot Hasil

posts-view

posts-add

posts-update

posts-delete

Penutup

Pada artikel kali ini kita telah berhasil menyelesaikan pembuatan module posts, selanjutnya kita akan lanjutkan untuk pembuatan module permissions.

Artikel Lainnya

Beberapa artikel rekomendasi lainnya untuk menambah pengetahuan.

JurnalKoding

Mulai asah skill dengan berbagai macam teknologi - teknologi terbaru seperti Laravel, React, Vue, Inertia, Tailwind CSS, dan masih banyak lagi.

© 2025 JurnalKoding, Inc. All rights reserved.