Logo Catatan Kader Logo Catatan Kader
  • Beranda
Beranda
Modul 3 • Form & Data Mutation

Interaksi Pengguna: v-model & POST Request

Form and Data Entry
v-model @submit.prevent Axios POST

"Menangkap Input, Memvalidasi Niat, dan Mengirimkannya ke Server"

1 Keajaiban Two-Way Binding (v-model)

Jika di JavaScript murni (Vanilla JS) kita harus repot-repot menggunakan document.getElementById().value lalu menambahkan addEventListener('input', ...) untuk menangkap ketikan user, di Vue kita hanya perlu satu direktif: v-model.

v-model menciptakan Two-Way Data Binding (ikatan dua arah). Artinya:

  • Jika user mengetik di input box, variabel di JavaScript (state) otomatis terupdate.
  • Jika Anda mengubah variabel di JavaScript secara terprogram, teks di input box otomatis berubah.

<script setup>
import { ref } from 'vue';

const username = ref('');
</script>

<template>
  <!-- Input diikat dengan variabel username -->
  <input v-model="username" type="text" placeholder="Masukkan nama" />

  <!-- Paragraf ini akan langsung berubah setiap kali Anda mengetik -->
  <p>Halo, {{ username }}!</p>
</template>

Praktik Terbaik: Gunakan `reactive` untuk Form

Daripada membuat puluhan ref() untuk setiap inputan (nama, email, password), lebih baik kumpulkan mereka ke dalam satu reactive object bernama form. Ini membuat kode lebih rapi dan sangat mempermudah pengiriman data API nantinya.

2 Menahan Refresh dengan @submit.prevent

Sifat bawaan (default) HTML ketika sebuah form di-submit adalah melakukan page reload (me-refresh seluruh halaman) dan mengirimkan data lewat URL. Di aplikasi Vue (SPA - Single Page Application), kita tidak ingin halaman ter-refresh karena akan mereset semua state aplikasi kita.

Kita mengatasi ini menggunakan Event Modifier milik Vue: @submit.prevent="namaFungsi". Ini sama persis fungsinya dengan event.preventDefault() di Vanilla JS.

3 Axios POST: Mengirim Payload

Jika `axios.get()` digunakan untuk meminta data, `axios.post()` digunakan untuk mengirim data baru ke server (biasanya untuk membuat *record* baru di database). Argumen kedua dari `axios.post()` adalah **Payload** (data yang ingin dikirim).

const submitForm = async () => {
  try {
    // 1. Parameter pertama: URL Endpoint
    // 2. Parameter kedua: Payload data (form yang kita buat pakai reactive)
    const response = await axios.post('https://api.example.com/register', form);
    
    console.log("Sukses!", response.data);
  } catch (error) {
    console.error("Gagal mengirim data:", error);
  }
};

4. Masterclass: Form Registrasi Kader Kompleks

Berikut adalah kode utuh (*Single File Component*) yang merangkum semua materi Modul 3. Form ini memiliki validasi, status loading, dan mengombinasikan berbagai tipe input (text, select, checkbox multiple, checkbox boolean).

KaderRegistration.vue
<!-- 
  ========================================================================
  KOMPONEN: KaderRegistration.vue
  DESKRIPSI: Form registrasi komprehensif menggunakan reactive object,
             v-model multi-type, frontend validation, dan simulasi POST.
  AUTHOR: Catatan Kader
  ======================================================================== 
-->

<script setup>
import { reactive, ref } from 'vue';
import axios from 'axios';

// ---------------------------------------------------------
// 1. STATE FORM & VALIDASI
// ---------------------------------------------------------
// Menggunakan reactive untuk mengelompokkan data payload form
const form = reactive({
  fullName: '',
  email: '',
  division: '',
  skills: [],       // v-model untuk multi-checkbox menghasilkan Array
  agreeTerms: false // v-model untuk single-checkbox menghasilkan Boolean
});

// State khusus untuk menampung pesan error per-field
const errors = reactive({
  fullName: '',
  email: '',
  division: '',
  agreeTerms: ''
});

// State untuk UI Experience (Feedback Form)
const isSubmitting = ref(false);
const submitStatus = ref(null); // 'success' | 'error' | null
const responseMessage = ref('');

// ---------------------------------------------------------
// 2. FUNGSI VALIDASI FRONTEND
// ---------------------------------------------------------
const validateForm = () => {
  let isValid = true;
  
  // Reset errors
  errors.fullName = '';
  errors.email = '';
  errors.division = '';
  errors.agreeTerms = '';

  // Validasi Nama
  if (!form.fullName.trim()) {
    errors.fullName = 'Nama lengkap wajib diisi';
    isValid = false;
  } else if (form.fullName.length < 3) {
    errors.fullName = 'Nama minimal 3 karakter';
    isValid = false;
  }

  // Validasi Email dengan Regex sederhana
  const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  if (!form.email.trim()) {
    errors.email = 'Email wajib diisi';
    isValid = false;
  } else if (!emailPattern.test(form.email)) {
    errors.email = 'Format email tidak valid';
    isValid = false;
  }

  // Validasi Divisi
  if (!form.division) {
    errors.division = 'Silakan pilih divisi penempatan';
    isValid = false;
  }

  // Validasi Persetujuan (Checkbox Mutlak)
  if (!form.agreeTerms) {
    errors.agreeTerms = 'Anda harus menyetujui syarat & ketentuan';
    isValid = false;
  }

  return isValid;
};

// ---------------------------------------------------------
// 3. METHOD SUBMIT FORM (AXIOS POST)
// ---------------------------------------------------------
const handleRegistration = async () => {
  // 1. Jalankan fungsi validasi sebelum menembak API
  if (!validateForm()) {
    // Hentikan proses jika form tidak valid
    return; 
  }

  // 2. Mulai Loading State
  isSubmitting.value = true;
  submitStatus.value = null;

  try {
    // Menyiapkan payload. Karena form adalah objek reaktif,
    // kita bisa langsung mengirimnya atau men-destructure-nya.
    const payload = {
      name: form.fullName,
      email: form.email,
      department: form.division,
      capabilities: form.skills
    };

    // Endpoint Publik untuk simulasi POST Data (JSONPlaceholder tidak menyimpan beneran, 
    // tapi membalas dengan status 201 Created)
    const response = await axios.post('https://jsonplaceholder.typicode.com/posts', payload);
    
    // 3. Sukses Skenario
    submitStatus.value = 'success';
    responseMessage.value = `Registrasi Berhasil! ID Pendaftaran Anda: KDR-${response.data.id}`;
    
    // Kosongkan form kembali ke state awal
    form.fullName = '';
    form.email = '';
    form.division = '';
    form.skills = [];
    form.agreeTerms = false;

  } catch (error) {
    // 4. Error Skenario
    console.error('Submit Failed:', error);
    submitStatus.value = 'error';
    responseMessage.value = 'Terjadi kesalahan jaringan atau server saat menyimpan data.';
  } finally {
    // Matikan Loading State
    isSubmitting.value = false;
  }
};
</script>

<template>
  <div class="max-w-2xl mx-auto py-10">
    <div class="bg-white dark:bg-zinc-900 rounded-3xl shadow-xl border border-slate-200 dark:border-zinc-800 overflow-hidden">
      
      <!-- Form Header -->
      <div class="bg-brand px-8 py-10 text-white text-center relative overflow-hidden">
        <!-- Dekorasi Geometris Header -->
        <div class="absolute -right-10 -top-10 w-40 h-40 bg-white opacity-10 rounded-full blur-2xl"></div>
        <div class="absolute -left-10 -bottom-10 w-32 h-32 bg-white opacity-10 rounded-full blur-xl"></div>
        
        <h2 class="text-3xl font-black mb-2 relative z-10">Registrasi Kader Baru</h2>
        <p class="text-brand-light opacity-90 relative z-10">Isi formulir di bawah ini dengan data valid.</p>
      </div>

      <!-- 
        Penggunaan @submit.prevent sangat penting di sini.
        Ini mencegah browser melakukan reload halaman secara otomatis.
      -->
      <form @submit.prevent="handleRegistration" class="p-8 sm:p-10 space-y-6">
        
        <!-- 1. Input Text: Nama Lengkap -->
        <div>
          <label class="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-2">Nama Lengkap <span class="text-red-500">*</span></label>
          <input 
            v-model="form.fullName"
            type="text" 
            class="form-input-transition w-full px-4 py-3 rounded-xl border bg-slate-50 dark:bg-zinc-800 text-slate-900 dark:text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-brand focus:bg-white dark:focus:bg-zinc-900"
            :class="errors.fullName ? 'border-red-500 focus:ring-red-500' : 'border-slate-200 dark:border-zinc-700'"
            placeholder="Cth: John Doe"
          >
          <!-- Pesan Error Dinamis -->
          <p v-if="errors.fullName" class="mt-2 text-sm text-red-500 flex items-center gap-1">
            <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path></svg>
            {{ errors.fullName }}
          </p>
        </div>

        <!-- 2. Input Email -->
        <div>
          <label class="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-2">Alamat Email <span class="text-red-500">*</span></label>
          <input 
            v-model="form.email"
            type="email" 
            class="form-input-transition w-full px-4 py-3 rounded-xl border bg-slate-50 dark:bg-zinc-800 text-slate-900 dark:text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-brand focus:bg-white"
            :class="errors.email ? 'border-red-500 focus:ring-red-500' : 'border-slate-200 dark:border-zinc-700'"
            placeholder="john@example.com"
          >
          <p v-if="errors.email" class="mt-2 text-sm text-red-500 flex items-center gap-1">
            <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path></svg>
            {{ errors.email }}
          </p>
        </div>

        <!-- 3. Select Dropdown: Divisi -->
        <div>
          <label class="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-2">Pilih Divisi <span class="text-red-500">*</span></label>
          <div class="relative">
            <select 
              v-model="form.division"
              class="form-input-transition appearance-none w-full px-4 py-3 rounded-xl border bg-slate-50 dark:bg-zinc-800 text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-brand focus:bg-white cursor-pointer"
              :class="errors.division ? 'border-red-500 focus:ring-red-500' : 'border-slate-200 dark:border-zinc-700'"
            >
              <option disabled value="">-- Pilih Divisi --</option>
              <option value="engineering">Software Engineering</option>
              <option value="design">UI/UX Design</option>
              <option value="marketing">Digital Marketing</option>
              <option value="product">Product Management</option>
            </select>
            <!-- Custom Caret Icon karena kita hide default browser appearance -->
            <div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-4 text-slate-500">
              <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>
            </div>
          </div>
          <p v-if="errors.division" class="mt-2 text-sm text-red-500 flex items-center gap-1">
            <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path></svg>
            {{ errors.division }}
          </p>
        </div>

        <!-- 4. Checkbox Multiple (Array v-model) -->
        <div>
          <label class="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-3">Keahlian Tambahan (Opsional)</label>
          <div class="grid grid-cols-2 gap-3">
            <!-- Looping manual untuk efisiensi visual di sini, bisa pakai v-for -->
            <label class="flex items-center space-x-3 cursor-pointer p-3 border border-slate-200 dark:border-zinc-700 rounded-xl hover:bg-slate-50 dark:hover:bg-zinc-800 transition-colors">
              <input v-model="form.skills" type="checkbox" value="Vue.js" class="w-5 h-5 text-brand rounded focus:ring-brand border-slate-300">
              <span class="text-slate-700 dark:text-slate-300 text-sm font-medium">Vue.js</span>
            </label>
            <label class="flex items-center space-x-3 cursor-pointer p-3 border border-slate-200 dark:border-zinc-700 rounded-xl hover:bg-slate-50 dark:hover:bg-zinc-800 transition-colors">
              <input v-model="form.skills" type="checkbox" value="Tailwind" class="w-5 h-5 text-brand rounded focus:ring-brand border-slate-300">
              <span class="text-slate-700 dark:text-slate-300 text-sm font-medium">Tailwind CSS</span>
            </label>
            <label class="flex items-center space-x-3 cursor-pointer p-3 border border-slate-200 dark:border-zinc-700 rounded-xl hover:bg-slate-50 dark:hover:bg-zinc-800 transition-colors">
              <input v-model="form.skills" type="checkbox" value="Node.js" class="w-5 h-5 text-brand rounded focus:ring-brand border-slate-300">
              <span class="text-slate-700 dark:text-slate-300 text-sm font-medium">Node.js API</span>
            </label>
            <label class="flex items-center space-x-3 cursor-pointer p-3 border border-slate-200 dark:border-zinc-700 rounded-xl hover:bg-slate-50 dark:hover:bg-zinc-800 transition-colors">
              <input v-model="form.skills" type="checkbox" value="Figma" class="w-5 h-5 text-brand rounded focus:ring-brand border-slate-300">
              <span class="text-slate-700 dark:text-slate-300 text-sm font-medium">Figma Design</span>
            </label>
          </div>
        </div>

        <!-- 5. Checkbox Boolean (Syarat & Ketentuan) -->
        <div class="pt-4 border-t border-slate-200 dark:border-zinc-800">
          <label class="relative flex items-start cursor-pointer">
            <div class="flex items-center h-5">
              <!-- v-model pada single checkbox akan mengembalikan true / false -->
              <input 
                v-model="form.agreeTerms" 
                type="checkbox" 
                class="w-5 h-5 border border-slate-300 rounded text-brand focus:ring-brand bg-slate-50 transition-all"
              >
            </div>
            <div class="ml-3 text-sm">
              <span class="font-medium text-slate-700 dark:text-slate-300">Saya menyetujui seluruh <a href="#" class="text-brand hover:underline">Syarat & Ketentuan</a> serta Kebijakan Privasi perusahaan.</span>
              <p v-if="errors.agreeTerms" class="mt-1 text-sm text-red-500 font-semibold">{{ errors.agreeTerms }}</p>
            </div>
          </label>
        </div>

        <!-- Status Alert (Akan muncul setelah submit) -->
        <div v-if="submitStatus === 'success'" class="p-4 bg-green-50 border-l-4 border-green-500 rounded-r-xl">
          <div class="flex items-center">
            <svg class="w-6 h-6 text-green-500 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
            <p class="text-green-800 font-bold">{{ responseMessage }}</p>
          </div>
        </div>
        
        <div v-if="submitStatus === 'error'" class="p-4 bg-red-50 border-l-4 border-red-500 rounded-r-xl">
          <div class="flex items-center">
            <svg class="w-6 h-6 text-red-500 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
            <p class="text-red-800 font-bold">{{ responseMessage }}</p>
          </div>
        </div>

        <!-- Action Button -->
        <div class="pt-4">
          <!-- Tombol dimatikan (disabled) saat isSubmitting bernilai true -->
          <button 
            type="submit" 
            :disabled="isSubmitting"
            class="w-full flex justify-center items-center py-4 px-4 border border-transparent rounded-xl shadow-lg text-lg font-bold text-white bg-brand hover:bg-brand-hover focus:outline-none focus:ring-4 focus:ring-brand/30 transition-all disabled:opacity-70 disabled:cursor-not-allowed"
          >
            <!-- Spinner Loading (Tampil hanya saat isSubmitting true) -->
            <svg v-if="isSubmitting" class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
              <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
              <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
            </svg>
            {{ isSubmitting ? 'Memproses Data...' : 'Kirim Pendaftaran' }}
          </button>
        </div>

      </form>
    </div>
  </div>
</template>
Kembali ke Modul 2 Modul Selesai

Catatan Kader

"Menulis form di Vue.js itu seperti sihir. v-model menghapus ratusan baris kode boilerplate JavaScript murni. Namun ingat aturan emasnya: Jangan pernah percaya 100% pada input pengguna. Lakukan selalu validasi di sisi Frontend dan pastikan Backend memvalidasi ulang."
- Tech Lead

Pencapaian

Seri Dasar Selesai! 🎉

Selamat! Kamu sudah menguasai reaktivitas dasar, koneksi API (GET & POST), dan manajemen Form.

Catatan Kader Catatan Kader

Catatan ini dikelola untuk keperluan dokumentasi pribadi, pengembangan kemampuan analisis logika, serta standarisasi implementasi sistem teknologi.

© 2026 Catatan Kader. Deployment Active.