Laravel 12 Nuxt UI #8 Modal EditFinance

Belajar cara membangun aplikasi fullstack modern menggunakan Laravel 12, Inertia.js, Nuxt UI, dan Tailwind CSS. Tutorial ini membahas langkah demi langkah mulai dari instalasi, konfigurasi, hingga integrasi Laravel dengan Nuxt tanpa perlu membuat REST API. Cocok untuk pemula yang ingin memahami konsep fullstack Laravel dengan tampilan modern dan reaktif.

✅ Telah dilihat 967 kali

Rating: 5.00 ⭐

... 07 November 2025, 08:14

Konfigurasi FinanceEditModal.vue

Silakan buka file FinanceEditModal.vue kemudian masukkan kode berikut ini:

<script setup lang="ts">
import { reactive, ref, watch } from 'vue'
import { router } from '@inertiajs/vue3'
import type { FormSubmitEvent } from '@nuxt/ui'
import * as z from 'zod'

// Props Finance Record untuk edit
const props = defineProps<{
  record: {
    id: number
    date: string
    income: number | string
    expense: number | string
  }
  modelValue?: boolean
}>()

const emit = defineEmits(['update:modelValue', 'updated'])
const open = defineModel<boolean>('open', { default: true })

// Schema validasi
const schema = z.object({
  date: z.string().min(1, 'Tanggal harus diisi'),
  income: z
    .number({ invalid_type_error: 'Uang masuk harus berupa angka' })
    .nonnegative('Uang masuk tidak boleh negatif'),
  expense: z
    .number({ invalid_type_error: 'Uang keluar harus berupa angka' })
    .nonnegative('Uang keluar tidak boleh negatif'),
})
type Schema = z.output<typeof schema>

// State form
const state = reactive({
  date: '',
  income: 0,
  expense: 0,
})

// Normalisasi data dari props ke state
watch(
  () => props.record,
  (record) => {
    if (record) {
      state.date = record.date
      console.log('Date from backend:', record.date) 

      state.income = Number(String(record.income).replace(/,/g, '')) || 0
      state.expense = Number(String(record.expense).replace(/,/g, '')) || 0
    }
  },
  { immediate: true }
)


// State alert
const showAlert = ref(false)
const alertMessage = ref('')
const alertColor = ref<'success' | 'error'>('success')

// Submit handler
async function onSubmit(event: FormSubmitEvent<typeof schema>) {
  const formData = new FormData()
  formData.append('date', state.date)
  formData.append('income', String(state.income))
  formData.append('expense', String(state.expense))
  formData.append('_method', 'PUT')

  router.post(`/finance-records/${props.record.id}`, formData, {
    forceFormData: true,
    onSuccess: () => {
      alertMessage.value = 'Catatan keuangan berhasil diperbarui.'
      alertColor.value = 'success'
      showAlert.value = true
      emit('updated')

      setTimeout(() => (open.value = false), 500)
      setTimeout(() => (showAlert.value = false), 3000)
    },
    onError: (errors: any) => {
      const allErrors =
        Object.values(errors || {}).flat().join(' | ') ||
        'Gagal memperbarui catatan keuangan.'
      alertMessage.value = allErrors
      alertColor.value = 'error'
      showAlert.value = true
      setTimeout(() => (showAlert.value = false), 3000)
    },
  })
}
</script>

<template>
  <!-- Alert pojok kanan bawah -->
  <div class="fixed bottom-4 right-4 z-50">
    <UAlert
      v-if="showAlert"
      :title="alertMessage"
      :color="alertColor"
      variant="subtle"
      :icon="
        alertColor === 'success'
          ? 'i-lucide-check-circle'
          : 'i-lucide-alert-circle'
      "
      close
      @update:open="showAlert = false"
    />
  </div>

  <!-- Modal Edit Finance Record -->
  <UModal
    v-model:open="open"
    title="Edit Catatan Keuangan"
    description="Perbarui tanggal, jumlah uang masuk, dan uang keluar."
    class="max-w-lg"
  >
    <template #body>
      <UForm :schema="schema" :state="state" class="space-y-5" @submit="onSubmit">
        <!-- Tanggal -->
        <UFormField name="date" label="Tanggal" required class="w-full">
          <UInput v-model="state.date" type="date" class="w-full" />
        </UFormField>

        <!-- Uang Masuk -->
        <UFormField name="income" label="Uang Masuk (Rp)" required class="w-full">
          <UInput
            v-model.number="state.income"
            type="number"
            min="0"
            step="0.01"
            placeholder="Masukkan jumlah uang masuk"
            class="w-full"
          />
        </UFormField>

        <!-- Uang Keluar -->
        <UFormField name="expense" label="Uang Keluar (Rp)" required class="w-full">
          <UInput
            v-model.number="state.expense"
            type="number"
            min="0"
            step="0.01"
            placeholder="Masukkan jumlah uang keluar"
            class="w-full"
          />
        </UFormField>

        <USeparator />

        <!-- Tombol aksi -->
        <div class="flex justify-end gap-3 mt-4">
          <UButton
            label="Batal"
            color="neutral"
            variant="subtle"
            @click="open = false"
          />
          <UButton label="Perbarui" color="primary" variant="solid" type="submit" />
        </div>
      </UForm>
    </template>
  </UModal>
</template>

Komponen ini berfungsi untuk mengedit catatan keuangan (finance record) yang sudah tersimpan di database. Secara umum, logikanya mirip dengan FinanceAddModal.vue, tetapi terdapat tambahan penting seperti binding data awal dari propspenggunaan watch, dan update data menggunakan method PUT melalui Inertia.js.

Komponen ini juga menggunakan:

  • Vue 3 Composition API (refreactivewatch),
  • Inertia.js router untuk komunikasi dengan backend Laravel,
  • Zod untuk validasi form,
  • dan komponen antarmuka Nuxt UI (UModalUFormUAlert, dll).

Bagian <script setup lang="ts">

1. Import Library

import { reactive, ref, watch } from 'vue'
import { router } from '@inertiajs/vue3'
import type { FormSubmitEvent } from '@nuxt/ui'
import * as z from 'zod'

Penjelasan:

  • reactive dan ref digunakan untuk membuat variabel reaktif.
  • watch digunakan untuk memantau perubahan data dari props dan menyalinnya ke form.
  • router dari Inertia digunakan untuk mengirim data ke backend Laravel.
  • FormSubmitEvent memberikan tipe data event form (karena TypeScript).
  • zod digunakan untuk validasi schema form.

2. Definisi Props dan Emit

const props = defineProps<{
  record: {
    id: number
    date: string
    income: number | string
    expense: number | string
  }
  modelValue?: boolean
}>()

const emit = defineEmits(['update:modelValue', 'updated'])
const open = defineModel<boolean>('open', { default: true })

Penjelasan:

  • props.record berisi data catatan keuangan yang akan diedit. Nilainya dikirim dari parent component.
  • defineEmits digunakan untuk mengirim event ke parent:
    • update:modelValue → untuk mengontrol status modal (buka/tutup).
    • updated → sebagai sinyal bahwa data berhasil diperbarui.
  • defineModel('open') memudahkan binding dua arah pada status modal (v-model:open).

3. Skema Validasi dengan Zod

const schema = z.object({
  date: z.string().min(1, 'Tanggal harus diisi'),
  income: z
    .number({ invalid_type_error: 'Uang masuk harus berupa angka' })
    .nonnegative('Uang masuk tidak boleh negatif'),
  expense: z
    .number({ invalid_type_error: 'Uang keluar harus berupa angka' })
    .nonnegative('Uang keluar tidak boleh negatif'),
})

Validasi ini sama dengan versi tambah data:

  • date harus diisi,
  • income dan expense harus berupa angka,
  • dan nilainya tidak boleh negatif.

Tipe data dideklarasikan dengan:

type Schema = z.output<typeof schema>

4. State Form

const state = reactive({
  date: '',
  income: 0,
  expense: 0,
})

state digunakan untuk menyimpan nilai form yang sedang diedit oleh pengguna.


5. Sinkronisasi Data Props ke State

watch(
  () => props.record,
  (record) => {
    if (record) {
      state.date = record.date
      console.log('Date from backend:', record.date)

      state.income = Number(String(record.income).replace(/,/g, '')) || 0
      state.expense = Number(String(record.expense).replace(/,/g, '')) || 0
    }
  },
  { immediate: true }
)

Bagian ini sangat penting. watch() memantau perubahan data record yang dikirim dari parent. Setiap kali record berubah (misalnya ketika pengguna memilih catatan yang ingin diedit), maka data tersebut akan langsung disalin ke state form.

Langkah-langkah yang dilakukan:

  • Nilai date langsung diisi.
  • income dan expense dikonversi ke angka murni (jika sebelumnya berupa string dengan tanda koma, misalnya "1,000").
  • { immediate: true } artinya fungsi ini dijalankan langsung ketika komponen pertama kali dimuat.

6. State Alert

const showAlert = ref(false)
const alertMessage = ref('')
const alertColor = ref<'success' | 'error'>('success')

Digunakan untuk menampilkan notifikasi di pojok kanan bawah ketika proses update berhasil atau gagal.


7. Fungsi Submit Form

async function onSubmit(event: FormSubmitEvent<typeof schema>) {
  const formData = new FormData()
  formData.append('date', state.date)
  formData.append('income', String(state.income))
  formData.append('expense', String(state.expense))
  formData.append('_method', 'PUT')

Karena Laravel mengharuskan method PUT untuk update, maka form dikirim dengan _method: 'PUT' melalui FormData.

Kemudian dikirim menggunakan Inertia:

router.post(`/finance-records/${props.record.id}`, formData, {
  forceFormData: true,
  onSuccess: () => { ... },
  onError: (errors: any) => { ... },
})
Jika berhasil:
  • Menampilkan pesan “Catatan keuangan berhasil diperbarui.”
  • Menutup modal setelah 0.5 detik.
  • Menghilangkan alert otomatis setelah 3 detik.
  • Mengirim event updated ke parent agar tabel atau daftar data ikut diperbarui.
Jika gagal:
  • Menampilkan pesan error hasil validasi dari server.
  • Mengubah warna alert menjadi merah.
  • Menyembunyikan alert otomatis setelah 3 detik.

Bagian <template>

1. Alert Pojok Kanan Bawah

<div class="fixed bottom-4 right-4 z-50">
  <UAlert
    v-if="showAlert"
    :title="alertMessage"
    :color="alertColor"
    variant="subtle"
    :icon="alertColor === 'success' ? 'i-lucide-check-circle' : 'i-lucide-alert-circle'"
    close
    @update:open="showAlert = false"
  />
</div>

Menampilkan pesan sukses atau gagal setelah pengguna memperbarui data. Letaknya di pojok kanan bawah agar tidak mengganggu tampilan modal.


2. Modal Form Edit

<UModal
  v-model:open="open"
  title="Edit Catatan Keuangan"
  description="Perbarui tanggal, jumlah uang masuk, dan uang keluar."
  class="max-w-lg"
>

Modal ini menampilkan form untuk mengedit data keuangan. Dikontrol dengan v-model:open agar bisa dibuka dan ditutup dari parent component.


3. Form dan Field Input

<UForm :schema="schema" :state="state" @submit="onSubmit">

Form ini otomatis memvalidasi input berdasarkan schema Zod. Setiap field (dateincomeexpense) dihubungkan dengan v-model agar perubahan langsung tersimpan di state.


4. Tombol Aksi

<div class="flex justify-end gap-3 mt-4">
  <UButton label="Batal" color="neutral" variant="subtle" @click="open = false" />
  <UButton label="Perbarui" color="primary" variant="solid" type="submit" />
</div>

Terdapat dua tombol:

  • Batal: menutup modal tanpa menyimpan perubahan.
  • Perbarui: menjalankan fungsi onSubmit untuk menyimpan perubahan ke server.

Materi selanjutnya adalah membuat halaman index. Pada halaman index nantinya akan ditampilkan data keuangan beserta beberapa fitur, seperti pencarian (search)paginasi (pagination)tambah data, dan edit data untuk setiap entri.

Daftar eBook