Pos

Nuxt.js CRUD dalam Satu Komponen

Nuxt.js CRUD dalam Satu Komponen

Nuxt.js / Vue.js adalah salah satu framework yang selalu digunakan untuk membantu developer membina website. Ianya dikategorikan sebagai frontend builder, sama seperti Next.js/React.js dan Svelte.js. Nuxt.js adalah go-to saya dalam pembinaan website kerana ianya sangatlah mudah untuk digunakan dan difahami.

Nuxt.js menggunakan Single File Component (SFC) yang mempunyai extension .vue. SFC ini terdiri daripada 3 bahagian utama iaitu:

  • Script - untuk JavaScript logic
  • Template - untuk HTML structure
  • Style - untuk CSS styling

Dengan cara ini, sesuatu rangka SFC file boleh difahami dengan mudah dan kod lebih tersusun.

CRUD, Masalah dengan Pendekatan Konvensional

Untuk operasi CRUD, sesetengah developer akan membina satu per satu komponen untuk setiap operasi. Contohnya:

1
2
3
ContactCreate.vue
ContactRead.vue
ContactUpdate.vue

Kelemahan pendekatan ini:

  • Setiap komponen mempunyai sedikit sahaja perubahan UI dan logic
  • Susah untuk maintain - jika satu komponen diubah, semua komponen perlu diubah
  • Kod berulang (code duplication)
  • Jika terlepas pandang, setiap komponen akan hilang keserasiannya

Penyelesaian: Satu Komponen untuk Semua CRUD

Jadi dalam blog ini, saya akan kongsikan cara membina satu komponen untuk semua operasi CRUD yang lebih efficient dan mudah dimaintain.

Struktur Projek

File structure untuk projek ini (Nuxt.js V4):

1
2
3
4
App/
├── components/
│   └── Contact.vue
└── app.vue

Implementasi

1. Memanggil Komponen dalam app.vue

Dalam app.vue, kita akan panggil Contact.vue dan pass dua props:

  1. mode - untuk menentukan mode operasi (create/read/update)
  2. model-value - data untuk dipaparkan (jika bukan mode ‘new’)
1
2
3
4
5
<Contact
  :mode="mode"
  :model-value="mode !== 'new' ? client[0] : null"
  v-if="client"
/>

2. Setup Props dalam Contact Component

Dalam Contact Component, kita mulakan dengan defineProps():

1
2
3
4
5
6
7
const props = defineProps({
  mode: { type: String, default: "" },
  modelValue: {
    type: Object,
    default: null,
  },
});

3. Reactive Data Management

Seterusnya kita akan define dan load data daripada props ke component variable:

1
2
3
4
5
6
7
8
9
const form = ref({ ...props.modelValue });

// Watch untuk perubahan pada props
watch(
  () => props.modelValue,
  (val) => {
    form.value = { ...val };
  }
);

4. Mode Helpers

Load mode helper untuk memudahkan conditional rendering:

1
2
3
const isView = computed(() => props.mode === "view");
const isEdit = computed(() => props.mode === "edit");
const isNew = computed(() => props.mode === "new");

Kelebihan Pendekatan Ini

DRY (Don’t Repeat Yourself) - Kod tidak berulang
Mudah dimaintain - Satu tempat untuk semua logic
Konsisten - UI dan UX yang seragam
Flexible - Mudah untuk add mode baru
Efficient - Kurang file untuk diurus

5. Contoh Lengkap File Contact.vue

Berikut adalah implementasi lengkap untuk Contact.vue yang menggabungkan semua konsep di atas:

5.1 Script Section

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
<script setup>
// Props definition
const props = defineProps({
  mode: { type: String, default: "" },
  modelValue: {
    type: Object,
    default: null,
  },
});

// PocketBase instance
const pb = useNuxtApp().$pb;

// Reactive form data
const form = ref({ ...props.modelValue });

// Watch untuk reload form value jika props berubah
watch(
  () => props.modelValue,
  (val) => {
    form.value = { ...val };
  }
);

// Mode helpers untuk conditional rendering
const isView = computed(() => props.mode === "view");
const isEdit = computed(() => props.mode === "edit");
const isNew = computed(() => props.mode === "new");

// File upload handler
const handleFileChange = (event, colName) => {
  const files = event.target.files;
  form.value[colName] = files[0];
};

// Main submit handler
const handleSubmit = async () => {
  if (isNew.value) {
    await handleCreate();
  } else if (isEdit.value) {
    await handleUpdate();
  }
};

// Create operation
const handleCreate = async () => {
  const formData = new FormData();
  Object.entries(form.value).forEach(([key, value]) => {
    formData.append(key, value);
  });

  try {
    await pb.collection("contact").create(formData);
    reloadNuxtApp();
  } catch (err) {
    console.error("Create error:", err);
  }
};

// Update operation
const handleUpdate = async () => {
  const formData = new FormData();
  Object.entries(form.value).forEach(([key, value]) => {
    formData.append(key, value);
  });

  try {
    await pb.collection("contact").update(form.value.id, formData);
    reloadNuxtApp();
  } catch (err) {
    console.error("Update error:", err);
  }
};
</script>

5.2 Template Section

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
<template>
  <div>
    <form @submit.prevent="handleSubmit">
      <!-- Dynamic Title -->
      <h2>
        
      </h2>

      <!-- Name Field -->
      <div>
        <label for="name">Name</label>
        <input
          type="text"
          id="name"
          placeholder="Enter your name"
          v-model="form.name"
          :readonly="isView"
          required
        />
      </div>

      <!-- Photo Field -->
      <div>
        <label for="photo">Photo</label>

        <!-- View Mode: Show link to existing photo -->
        <div v-if="isView">
          <NuxtLink
            :to="`${pb.baseURL}/api/files/${props.modelValue.collectionId}/${props.modelValue.id}/${props.modelValue.photo}`"
            target="_blank"
          >
            
          </NuxtLink>
        </div>

        <!-- Edit/Create Mode: File input -->
        <div v-else>
          <input
            type="file"
            id="photo"
            accept="image/*"
            @change="handleFileChange($event, 'photo')"
          />

          <!-- Show existing file in edit mode -->
          <div v-if="isEdit && props.modelValue?.photo">
            Current file:
            <NuxtLink
              :to="`${pb.baseURL}/api/files/${props.modelValue.collectionId}/${props.modelValue.id}/${props.modelValue.photo}`"
              target="_blank"
            >
              
            </NuxtLink>
          </div>
        </div>
      </div>

      <!-- Submit Button (hanya untuk Create/Edit) -->
      <button v-if="isEdit || isNew" type="submit">
        
      </button>
    </form>
  </div>
</template>
Pos ini dilesenkan di bawah CC BY 4.0 oleh penulis.