CRUD Laravel 8 dan InertiaJs

CRUD Laravel 8 dan InertiaJs

Pendahuluan

Meskipun Laravel 8 telah release sejak September 2020 dan telah banyak dibahas diberbagai forum, tapi tidak ada salahnya kita mengangkat kembali materi ini dari perspektif Daengweb.id. Pada pembahasan kali ini, perpaduan Laravel 8 & InertiaJS akan menjadi topik bahasan dengan membuat fitur CRUD (Create Read Update & Delete).

Jika Laravel 8 sudah sangat populer dengan sequelnya hingga versi ke-8, lalu InertiaJs itu apa?. InertiaJs adalah sebuah teknik yang berbeda dalam membuat aplikasi SPA atau Single Page Application tanpa membutuhkan sebuah API atau InertiaJs menyebut dirinya sebagai modern monolith.

InertiaJs itu sebuah framework baru? Dalam official site-nya, InertiaJs menolak untuk disebut sebagai sebuah framework, sehingga bukan bertugas untuk menggantikan server-side atau client-side frameworks tapi di-design untuk bekerja bersama. Mbak Iner menggambarkan dirinya sebagai sebuah lem yang menghubungkan keduanya. Adapun client-side adapternya adalah React, Vue dan Svelte, sedangkan server-side adapternya adalah Laravel & Rails.

Baca Juga: https://daengweb.id/membuat-crud-laravel-8-jetstream-livewire

Instalasi Laravel 8 & Konfigurasi

Tahap pertama, install Laravel dengan command

composer create-project laravel/laravel inertiajs-crud

Kemudian masuk ke dalam direktori Laravel yang baru saja ter-install. Lalu, install Jetstream dengan command

composer require laravel/jetstream

Generate authentication untuk membuat fitur login, register dan email verification. Jika kita membutuhkan team management, tambahkan parameter --teams.

php artisan jetstream:install inertia

Install seluruh npm package dan compile dengan command

npm install && npm run dev

Buka file .env dan modifikasi informasi database sesuai dengan database Anda. Lalu jalankan migration.

php artisan migrate

Menampilkan Data Produk

Sebelum menyelesaikan fitur menampilkan data produk, buat migration baru untuk men-generate table products. Dari command line, jalankan command

php artisan make:model Product -m

Buka file migration yang baru saja di-generate dan modifikasi menjadi

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateProductsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('products', function (Blueprint $table) {
            $table->id();
            $table->string('code');
            $table->string('name');
            $table->string('price');
            $table->string('weight');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('products');
    }
}

Eksekusi migration-nya dengan command php artisan migrate. Lalu buka file Product.php dan modifikasi menjadi

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    use HasFactory;
    protected $guarded = [];
}

Table products sudah tersedia, saatnya untuk membuat route. Buka file routes/web.php dan modifikasi menjadi

<?php

use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;

use App\Http\Controllers\ProductController; //LOAD CONTROLLER PRODUCT

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/
Route::get('/', function () {
    return Inertia::render('Welcome', [
        'canLogin' => Route::has('login'),
        'canRegister' => Route::has('register'),
        'laravelVersion' => Application::VERSION,
        'phpVersion' => PHP_VERSION,
    ]);
});

//GROUPING DENGAN MENGGUNAKAN MIDDLEWARE
Route::group(['middleware' => ['auth:sanctum', 'verified']], function() {
    //ROUTING UNTUK HALAMAN DASHBOARD
    Route::get('/dashboard', function() {
        return Inertia::render('Dashboard');
    })->name('dashboard');
  
    //RESTFUL ROUTING UNTUK HALAMAN PRODUCT
    Route::resource('product', ProductController::class);
});

Sebagaimana yang terlihat pada use statement diatas, terdapat sebuah controller yang belum kita buat. Maka tugas berikutnya adalah generate controller tersebut dengan command

php artisan make:controller ProductController

Buka file ProductController dan tambahkan code berikut

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Product;
use Inertia\Inertia;
use Illuminate\Support\Facades\Validator;

class ProductController extends Controller
{
    public function index()
    {
        $products = Product::all(); //MEMBUAT QUERY UNTUK MENGAMBIL DATA DARI TABLE PRODUCTS
        return Inertia::render('product', ['data' => $products]); //RENDER DATA TERSEBUT MENGGUNAKAN INERTIA. 
        //PARAMETER PERTAMA YAKNI product ADALAH NAMA FILE .VUE YANG NANTINYA AKAN KITA BUAT
        //PARAMETER BERIKUTNYA ADALAH DATA YANG AKAN DIPASSING BERISI SELURUH DATA PRODUCTS
    }
}

Buat file product.vue di dalam folder resources/js/Pages dan tambahkan code berikut

<template>
    <app-layout>
        <template #header>
            <h2 class="font-semibold text-xl text-gray-800 leading-tight">
                Kelola Data Produk
            </h2>
        </template>
        <div class="py-12">
            <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
                <div class="bg-white overflow-hidden shadow-xl sm:rounded-lg px-4 py-4">
                  
                    <!-- FLASH MESSAGE AKAN DITEMPATKAN DISINI -->
                  
                    <button @click="openModal()" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded my-3">Tambah Produk</button>
                    <table class="table-fixed w-full">
                        <thead>
                            <tr class="bg-gray-100">
                                <th class="px-4 py-2">Kode</th>
                                <th class="px-4 py-2">Nama Produk</th>
                                <th class="px-4 py-2">Harga</th>
                                <th class="px-4 py-2">Berat</th>
                                <th class="px-4 py-2">Aksi</th>
                            </tr>
                        </thead>
                        <tbody v-if="data.length > 0">
                            <tr v-for="(row, index) in data" :key="index">
                                <td class="border px-4 py-2">{{ row.code }}</td>
                                <td class="border px-4 py-2">{{ row.name }}</td>
                                <td class="border px-4 py-2">{{ row.price }}</td>
                                <td class="border px-4 py-2">{{ row.weight }} gr</td>
                                <td class="border px-4 py-2">
                                    <button @click="edit(row)" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded mr-2">Edit</button>
                                    <button @click="deleteRow(row)" class="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded">Delete</button>
                                </td>
                            </tr>
                        </tbody>
                        <tbody v-else>
                            <tr>
                                <td colspan="5" class="text-center">Tidak ada data</td>
                            </tr>
                        </tbody>
                    </table>
                    
                  	<!-- MODAL TAMBAH & EDIT PRODUK AKAN DITEMPATKAN DISINI -->
                </div>
            </div>
        </div>
    </app-layout>
</template>
<script>
    import AppLayout from '@/Layouts/AppLayout'
    import Welcome from '@/Jetstream/Welcome'
    export default {
        components: {
            AppLayout,
            Welcome,
        },
        props: ['data'],
        data() {
            return {
                editMode: false,
                isOpen: false,
                form: {
                    code: null,
                    name: null,
                    price: null,
                    weight: null,
                },
            }
        },
        methods: {
            openModal() {
                
            },
            closeModal() {
                
            },
            reset() {
                
            },
            save() {
                //CODE FUNGSI SAVE AKAN DISIMPAN DISINI
            },
            edit(data) {
                //CODE BUKA MODAL EDIT DATA AKAN DISIMPAN DISINI
            },
            update() {
              //CODE UPDATE DATA AKAN DISIMPAN DISINI
            },
            deleteRow(data) {
                //CODE HAPUS DATA DISINI
            }
        }
    }
</script>

Sampai dengan tahap ini, fitur menampilkan data sudah selesai. Tugas terakhir dari module ini adalah membuat menu navigasi agar bisa di akses melalui dashboard bawaan. Buka file resources/js/Layouts/AppLayout.vue dan tambahkan code berikut dibawah code menu dashboard

<jet-nav-link :href="route('product.index')" :active="route().current('product.index')">
    Produk
</jet-nav-link>

Silahkan buka halaman /products, jika belum punya account, silahkan register pada halaman /register.

CRUD Laravel 8 dan InertiaJs - show data

Tambah Data Produk

Jika module di atas hanya menampilkan data, maka tugas kita kali ini adalah bagaimana membuat fitur untuk menambahkan data agar terdapat data yang bisa ditampilkan oleh module sebelumnya. Struktur file .vue-nya sudah tersedia, kita hanya perlu melengkapi nya saja. Bagian pertama, buat modalnya terlebih dahulu, replace komentar untuk module tambah & edit data dengan code

<div class="fixed z-10 inset-0 overflow-y-auto ease-out duration-400" v-if="isOpen">
    <div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
        <div class="fixed inset-0 transition-opacity">
            <div class="absolute inset-0 bg-gray-500 opacity-75"></div>
        </div>
        <span class="hidden sm:inline-block sm:align-middle sm:h-screen"></span>​
        <div class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full" role="dialog" aria-modal="true" aria-labelledby="modal-headline">
            <form>
                <div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
                    <div class="">
                        <div class="mb-4">
                            <label for="code" class="block text-gray-700 text-sm font-bold mb-2">Kode Produk</label>
                            <input type="text" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" id="code" v-model="form.code">
                            <div v-if="errors.code" class="text-red-500">{{ errors.code }}</div>
                        </div>
                        <div class="mb-4">
                            <label for="name" class="block text-gray-700 text-sm font-bold mb-2">Nama Produk</label>
                            <input type="text" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" id="name" v-model="form.name">
                            <div v-if="errors.name" class="text-red-500">{{ errors.name }}</div>
                        </div>
                        <div class="mb-4">
                            <label for="price" class="block text-gray-700 text-sm font-bold mb-2">Harga</label>
                            <input type="text" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" id="price" v-model="form.price">
                            <div v-if="errors.price" class="text-red-500">{{ errors.price }}</div>
                        </div>
                        <div class="mb-4">
                            <label for="weight" class="block text-gray-700 text-sm font-bold mb-2">Berat</label>
                            <input type="text" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" id="weight" v-model="form.weight">
                            <div v-if="errors.weight" class="text-red-500">{{ errors.weight }}</div>
                        </div>
                    </div>
                </div>
                <div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
                    <span class="flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto">
                        <button type="button" class="inline-flex justify-center w-full rounded-md border border-transparent px-4 py-2 bg-green-600 text-base leading-6 font-medium text-white shadow-sm hover:bg-green-500 focus:outline-none focus:border-green-700 focus:shadow-outline-green transition ease-in-out duration-150 sm:text-sm sm:leading-5" v-show="!editMode" @click="save">
                            Save
                        </button>
                    </span>
                    <span class="flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto">
                        <button type="button" class="inline-flex justify-center w-full rounded-md border border-transparent px-4 py-2 bg-green-600 text-base leading-6 font-medium text-white shadow-sm hover:bg-green-500 focus:outline-none focus:border-green-700 focus:shadow-outline-green transition ease-in-out duration-150 sm:text-sm sm:leading-5" v-show="editMode" @click="update">
                            Update
                        </button>
                    </span>
                    <span class="mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto">
                        <button @click="closeModal()" type="button" class="inline-flex justify-center w-full rounded-md border border-gray-300 px-4 py-2 bg-white text-base leading-6 font-medium text-gray-700 shadow-sm hover:text-gray-500 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue transition ease-in-out duration-150 sm:text-sm sm:leading-5">
                            Cancel
                        </button>
                    </span>
                </div>
            </form>
        </div>
    </div>
</div>

Note: Modal di atas akan dibuka jika variable isOpen bernilai true. Adapun codingan lainnya hanya berisi tag untuk membuat form input-an.

Jika tombol tambah produk di-klik, maka akan menjalankan methods openModal. Tugas kita adalah mengubah value isOpen menjadi true.

openModal() {
    this.isOpen = true;
},

Ada 3 buah tombol yang akan dilengkapi fungsinya, pertama adalah method save() dimana method ini berfungsi untuk mengirimkan data ke server-side untuk kemudian disimpan ke dalam database.

this.$inertia.post('/product', this.form, {
    preserveState: (page) => Object.keys(page.props.errors).length,
    onError: (page) => {
        if (Object.keys(page).length == 0) {
            this.reset();
            this.closeModal();
            this.editMode = false;  
        }
    }
})

Method kedua adalah closeModal(), dimana method ini berfungsi untuk menutup modal tambah/edit data.

closeModal() {
    this.isOpen = false; //TUTUP MODAL
    this.reset(); //RESET FORM
    this.editMode=false; //SET EDIT MODAL MENJADI FALSE JUGA
},

Adapun tombol update() akan kita kerjakan pada module berikutnya. Jika perhatikan code di-atas, terdapat method reset() yang dijalankan. Fungsinya adalah untuk mengosongkan form

reset() {
    this.form = {
        code: null,
        name: null,
        price: null,
        weight: null,
    }
},

Sebelum melangkah ke server-side, jika kita perhatikan, terdapat tag untuk menampilkan errors message validation melalui variable errors. Modifikasi props untuk menangkap value errors sekaligus flash yang nantinya digunakan untuk menampilkan flash message jika data berhasil ditambahkan, diperbaharui atau dihapus.

props: ['data', 'errors', 'flash']

Sedangkan untuk flash message-nya, replace komentar flash message dengan code

<div class="bg-green-100 border-t-4 border-green-500 rounded-b text-green-900 px-4 py-3 shadow-md my-3" role="alert" v-if="flash.message">
    <div class="flex">
        <div>
            <p class="text-sm">{{ flash.message }}</p>
        </div>
    </div>
</div>

Sisi frontend sudah selesai, sekarang kita buat fungsi untuk mengolah data yang dikirim dan disimpan ke database. Buka file ProductController.php dan tambahkan code

public function store(Request $request)
{
    //MEMBUAT VALIDASI DATA
    Validator::make($request->all(), [
        'code' => ['required', 'string', 'unique:products,code'],
        'name' => ['required', 'string'],
        'price' => ['required', 'integer'],
        'weight' => ['required', 'integer'],
    ])->validate();

    Product::create($request->all()); //MEMBUAT QUERY UNTUK MENYIMPAN DATA
    return redirect()->back()->with(['message' => 'Produk: ' . $request->name . ' Ditambahkan']); //MENGIRIMKAN FLASH MESSAGE 
}

Bagian terakhir adalah sharing Inertia variable message dan errors, buka file AppServiceProvider.php dan modifikasi method boot()

public function boot()
{
    Inertia::share([
        'errors' => function () {
            return Session::get('errors')
                ? Session::get('errors')->getBag('default')->getMessages()
                : (object) [];
        },
    ]);

    Inertia::share('flash', function () {
        return [
            'message' => Session::get('message'),
        ];
    });
}

Jangan lupa untuk load Inertia menggunakan use statement use Inertia\Inertia;.

CRUD Laravel 8 dan InertiaJs - tambah data

Edit Data Produk

Fitur berikutnya adalah meng-edit data yang telah ada. Secara struktur, frontend-nya sudah lengkap. Kita hanya perlu melengkapi method-nya saja. Method pertama adalah edit() yang berfungsi untuk memasukkan data ke dalam variable form dan membuka modal. Buka file product.vue

edit(data) {
    this.form = Object.assign({}, data);
    this.editMode = true;
    this.openModal();
},

Method berikutnya adalah update() yang berfungsi untuk mengirimkan data yang akan diperbaharui.

update() {
    this.form._method = 'PUT';

    this.$inertia.post('/product/' + this.form.id, this.form, {
        preserveState: (page) => Object.keys(page.props.errors).length,
        onError: (page) => {
            if (Object.keys(page).length == 0) {
                this.reset();
                this.closeModal();
            }
        }
    })
},

Saatnya untuk mengolah data-nya dari sisi server, buka file ProductController.php dan tambahkan code

public function update(Request $request, $id)
{
  //VALIDASI DATA
    Validator::make($request->all(), [
        'code' => ['required', 'string', 'unique:products,code,' . $id],
        'name' => ['required', 'string'],
        'price' => ['required', 'integer'],
        'weight' => ['required', 'integer'],
    ])->validate();

    $product = Product::find($id); //QUERY UNTUK MENGAMBIL DATA BERDASARKAN ID
    $product->update($request->all()); //PERBAHARUI DATA
    return redirect()->back()->with(['message' => 'Produk: ' . $request->name . ' Diperbaharui']); //KIRIM FLASH MESSAGE
}

CRUD Laravel 8 dan InertiaJs - edit data

Hapus Data Produk

Module terakhir termasuk module yang singkat dalam proses mengerjakannya. Buka file product.vue dan modifikasi method deleteRow() yang berfungsi untuk mengirimkan perintah hapus berdasarkan id produk.

deleteRow(data) {
    if (!confirm('Are you sure want to remove?')) return;
    data._method = 'DELETE';
    this.$inertia.post('/product/' + data.id, data)
    this.reset();
    this.closeModal();
}

Kemudian buka file ProductController.php dan tambahkan method

public function destroy($id)
{
    $product = Product::find($id);
    $product->delete();
    return redirect()->back()->with(['message' => 'Produk: ' . $product->name . ' Di hapus']);
}

Mengirim Email Notifikasi Menggunakan SMTP Gmail Laravel 8

Kesimpulan

InertiaJs merupakan pilihan yang bisa digunakan selain Livewire dalam membuat aplikasi Single Page Application. Inertia hadir sebagai penghubung antara server-side framework dan client-side framework yang menjadi satu kesatuan dalam memenuhi kebutuhan aplikasi modern saat ini. Sepanjang artikel ini kita telah belajar banyak hal terkait fitur yang sering kali digunakan dalam sebuah aplikasi.

Dokumentasi code dari artikel ini bisa dilihat di Github

Category:
Share:

Comments