Tutorial Vue JS: Membuat Todo List App CRUD Lengkap - Eps 4

Yo, sebelumnya kita udah belajar konsep dasar Vue di Part 3. Tahu, reactive data, direktif-direktif, dan semua theory yang penting banget. Kalau belum baca, langsung aja cek Vue JS Reactive Data: Konsep Dasar dan Directives - Eps 3 dulu ya.

Nah, sekarang saatnya buat aplikasi yang bener-bener bisa dipakai. Hari ini kita bikin Todo List app yang lengkap dengan semua fitur CRUD, biar semua teori yang udah kita pelajari langsung keliatan hasilnya di layar. Todo List itu pilot project paling sempurna karena punya semua elemen yang perlu kita kuasai.

Nggak usah takut, ini nggak sesulit yang dibayangkan. Dengan aplikasi ini, kita pake ref untuk reactive data, v-for untuk nampilin list, v-model untuk input, dan computed properties untuk filter sama counter. Semuanya bekerja bareng-bareng dalam satu aplikasi yang bermanfaat banget.

Setelah selesai, kamu udah punya portfolio project yang bisa dibilang ke orang lain bahwa kamu bisa membuat aplikasi yang beneran berfungsi. Ini skill fundamental sebelum lanjut ke project-project yang lebih complex. Yang penting sekarang adalah fokus, follow langkah demi langkah dengan teliti, dan jangan ragu buat coba-coba sendiri. Kita jelasin setiap bagian dengan detail kok.

Yuk langsung lanjut dan lihat fitur-fitur apa aja yang bakal kita implementasikan.

Fitur Todo List: Apa Saja yang Bakal Kita Buat

Udah saatnya kita lihat apa saja yang bakal kita buat dalam aplikasi todo list ini. Ini penting biar kita punya gambaran jelas sebelum mulai ngoding. Jangan khawatir, semua fitur yang kita buat nanti benar-benar praktis dan sering dipakai di aplikasi todo yang sudah terkenal.

Yang Bakal Kita Buat

  • Input dan add todo, user bisa ketik dan tekan enter atau klik tombol add. Fitur pertama dari CRUD yang kita butuhkan.
  • Display list todos, semua todo yang ditambahkan ditampilin dalam list yang rapi. Ini bagian read dari CRUD.
  • Mark complete atau incomplete, checkbox untuk tandain todo yang udah selesai atau belum. User bisa toggle kapan aja.
  • Delete todo, tombol buat hapus todo yang gak perlu lagi. Ini bagian delete dari CRUD.
  • Counter untuk total, active, dan completed todo. Biar user bisa lihat progress mereka dengan cepat.
  • Filter dengan opsi all, active, dan completed. Berguna banget kalau todo list-nya udah panjang.

Jadi semua ini adalah fitur-fitur yang sederhana tapi sangat berguna. Kita bakal buat semuanya step by step dan kamu akan lihat gimana semuanya bekerja.

Konsep Vue yang Dipraktekkan

Sekarang kita lihat konsep Vue apa aja yang bakal dipake di project ini:

  • Reactive data pakai ref untuk menyimpan array todo, input text, dan filter status. Ini adalah kunci Vue biar data dan UI selalu sinkron.
  • V-for untuk loop semua todo dan tampilin satu-satu di halaman. Berguna banget buat render list dinamis.
  • V-model yang hubungin input field sama data. Jadi setiap kali user ketik, data otomatis terupdate.
  • V-if untuk conditional rendering, kayak tampilin pesan kalau gak ada todo. Ini bikin UX lebih bagus.
  • Methods buat handle event kayak add todo, delete todo, dan toggle status. Ini operasi yang kita jalankan dari template.
  • Array manipulation seperti push, filter, dan map. Penting banget buat ubah-ubah data todo sesuai kebutuhan.
  • Computed properties buat hitung total todo, berapa selesai, berapa aktif, dan filter based on status. Ini otomatis update kalau dependency berubah.

Semua konsep ini bakal kerja bareng-bareng buat menciptakan aplikasi yang benar-benar berfungsi. Jangan khawatir kalau ngerasa masih belum paham, kita akan implementasi satu-satu dengan penjelasan yang jelas. Yuk langsung lanjut ke setup data structure dan mulai coding.

Setup dan Data Structure

Oke, mulai dari sini kita nulis kode beneran. Pertama, buat file baru bernama TodoApp.vue di folder /src/views/. File ini bakal jadi komponen utama aplikasi todo list kita. Kalau kamu udah setup project Vue.js dengan create-vue atau Vite, struktur folder udah ada, tinggal tambah file ini aja.

Membuat File TodoApp.vue

Buka VS Code dan bikin file baru TodoApp.vue di dalam /src/views/. Kalau foldernya belum ada, buat dulu. Ini struktur standard yang direkomendasikan BuildWithAngga untuk project Vue yang profesional dan mudah di-maintain.

Struktur Data yang Kita Butuhkan

Sekarang kita lihat struktur data apa aja yang dipakai. Ini fondasi aplikasi kita, jadi penting banget untuk paham setiap property-nya.

const todos = ref([])
const newTodo = ref('')
const filter = ref('all') // all, active, completed

// Struktur object todo:
{
  id: Date.now(),
  text: 'Todo text',
  completed: false
}

Penjelasan Setiap Property

  • todos: Array yang nyimpan semua todo dari user. Pakai ref() supaya reaktif, jadi perubahan langsung keliatan di UI tanpa kode tambahan.
  • newTodo: String yang nyimpan value dari input field. User ketik todo baru di sini sebelum ditambahkan. Pakai ref() agar input selalu sinkron sama data.
  • filter: String yang nyimpan status filter, bisa 'all', 'active', atau 'completed'. Pakai ref() biar filter bisa berubah dengan reactive dan UI langsung ter-update.
  • id: Identifier unik untuk setiap todo. Kita pakai Date.now() karena sederhana dan jarang bentrok di aplikasi kecil. Penting banget buat :key di v-for nantinya.
  • text: Konten dari todo yang ditulis user. Sederhana tapi penting karena ini yang bakal ditampilin di halaman sebagai teks todo.
  • completed: Boolean yang tunjukin apakah todo udah selesai atau belum. Default-nya false karena todo baru pasti belum selesai. Ini bakal di-toggle kalau user klik checkbox.

Dengan struktur data yang jelas gini, kita bisa melakukan operasi CRUD dengan mudah nantinya. Setiap property punya peran yang jelas dan nggak ada yang redundan. Ini adalah best practice buat struktur data yang scalable dan mudah di-maintain.

Step-by-Step Implementation

Sekarang mulai implementasi fitur-fitur yang udah kita bahas. Kita buat satu per satu dengan detail, jadi follow dari awal sampai akhir ya. Setiap step penting banget dan jadi dasar dari aplikasi kita.

Step 1: Input Form untuk Tambah Todo

Pertama kita buat form input yang bisa menerima input dari user. Ini adalah langkah paling dasar untuk fitur create dalam CRUD. Form ini punya input field dan tombol add untuk menambahkan todo baru.

Template Input Form

Mari kita lihat struktur HTML untuk input form. Kita pakai v-model untuk ikat input field sama reactive data newTodo, dan @keyup.enter untuk jalankan function addTodo kalau user tekan enter.

<div class="todo-input">
  <input
    v-model="newTodo"
    @keyup.enter="addTodo"
    type="text"
    placeholder="Ketik todo baru..."
  />
  <button @click="addTodo">Add</button>
</div>

Templete di atas simple dan mudah dipahami. Input field terikat sama newTodo pakai v-model, jadi setiap kali user ketik, data otomatis ter-update. Button bisa diklik atau tekan enter untuk submit.

Method addTodo Lengkap

Sekarang kita lihat function addTodo yang tangani logika ketika user tambah todo. Function ini harus lakukan tiga hal penting: validasi, push ke array, dan reset input.

const addTodo = () => {
  // Validasi: cek apakah input kosong
  if (newTodo.value.trim() === '') {
    alert('Todo tidak boleh kosong!')
    return
  }

  // Buat object todo baru
  const todoItem = {
    id: Date.now(),
    text: newTodo.value,
    completed: false
  }

  // Push todo ke array
  todos.value.push(todoItem)

  // Reset input field
  newTodo.value = ''
}

Mari kita bahas apa yang terjadi di function ini. Pertama, validasi kalau input kosong atau hanya spasi. Kalau kosong, tampilkan alert dan return jadi nggak ada todo kosong yang ditambahkan.

Kedua, buat object baru dengan struktur yang sudah kita tentukan sebelumnya. Property id pake Date.now() yang hasilkan timestamp unik. Property text dari newTodo.value. Property completed default false karena todo baru pasti belum selesai.

Ketiga, push object ini ke array todos pakai todos.value.push(). Ini tambah todo baru ke list. Vue otomatis detect perubahan dan re-render component.

Keempat, reset input field dengan set newTodo.value jadi string kosong. Penting supaya input field empty dan siap untuk input todo yang baru.

Penempatan di Component

Sekarang kita lihat gimana penempatan method ini di component yang lengkap. Method ini harus ditempatkan di script setup section bersama sama dengan reactive data yang udah kita buat sebelumnya.

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

// Data
const todos = ref([])
const newTodo = ref('')
const filter = ref('all')

// Methods
const addTodo = () => {
  if (newTodo.value.trim() === '') {
    alert('Todo tidak boleh kosong!')
    return
  }

  const todoItem = {
    id: Date.now(),
    text: newTodo.value,
    completed: false
  }

  todos.value.push(todoItem)
  newTodo.value = ''
}
</script>

<template>
  <div class="todo-input">
    <input
      v-model="newTodo"
      @keyup.enter="addTodo"
      type="text"
      placeholder="Ketik todo baru..."
    />
    <button @click="addTodo">Add</button>
  </div>
</template>

<style scoped>
/* Style akan dilengkapi di bagian Styling & UX nanti */
</style>

Struktur component di atas udah lengkap untuk step pertama. Kamu bisa copy-paste kode ini ke file TodoApp.vue yang udah kita buat di /src/views/ sebelumnya. Pastikan kamu paham setiap bagian, bukan hanya copy-paste aja tanpa ngerti. Untuk styling, kita bakal lengkapin nanti di bagian khusus yang membahas styling dan user experience.

Testing Input Form

Muncul alert ketika submit input kosong
Muncul alert ketika submit input kosong

Setelah copy kode ini, saatnya test apakah input form bekerja baik. Buka browser dan coba ketik beberapa todo. Kamu seharusnya bisa lihat bahwa input field ter-update saat kamu ketik, dan ketika tekan enter atau klik add, input field bakal reset kosong. Kalau coba submit dengan input kosong, bakal muncul alert bahwa todo tidak boleh kosong.

Kalau semuanya udah berjalan dengan baik, berarti step 1 ini udah selesai dan berhasil. Kita bakal lanjut ke step 2 untuk tampilin todo-todo yang udah kita buat dalam bentuk list yang rapi.

Step 2: Display List Todos

Sekarang kita lanjut ke step kedua, yaitu tampilin semua todo yang udah ditambahkan dalam bentuk list. Ini bagian read dari CRUD. Kita pakai directive v-for dari Vue untuk loop semua todo.

Template untuk Display List

Mari kita lihat struktur HTML untuk tampilin list todos:

<div class="todo-list">
  <ul>
    <li v-for="todo in filteredTodos" :key="todo.id">
      {{ todo.text }}
    </li>
  </ul>
</div>

v-for bakal loop setiap item dari array filteredTodos dan render sebuah <li> element untuk setiap todo. Di dalam setiap <li>, kita tampilin text dari todo pakai interpolation {{ todo.text }}.

Perhatikan :key="todo.id". Ini paling penting dan sering dilupakan pemula.

Mengapa :key Attribute Penting

Attribute :key di Vue untuk beri identitas unik kepada setiap element dalam list. Ketika list berubah, Vue perlu tahu element mana yang berubah, ditambah, atau dihapus. Tanpa :key, Vue bakal re-render semua element, bahkan yang tidak berubah. Ini jelek untuk performa.

Dengan :key, Vue bisa identify mana element yang berubah dan hanya re-render yang perlu. Ini bikin aplikasi lebih efisien dan responsif.

Kenapa Harus Unique, Bukan Index

Banyak yang pakai index sebagai :key. Ini kesalahan besar. Kalau kamu pakai index sebagai :key dan kemudian hapus todo pertama, semua index bakal bergeser. Vue bakal bingung karena :key-nya berubah padahal item-nya mungkin sama. Ini bisa bikin bug yang sulit dideteksi.

Solusinya pakai identifier yang truly unique kayak todo.id. ID kita dibuat pakai Date.now(), jadi bakal selalu unik dan tidak berubah. Ini best practice dan direkomendasikan Vue.js documentation.

Kode Lengkap untuk Step 2

Ini kode dari step 1 yang ditambah display list:

<script setup>
import { ref, computed } from 'vue'

// Data
const todos = ref([])
const newTodo = ref('')
const filter = ref('all')

// Methods
const addTodo = () => {
  if (newTodo.value.trim() === '') {
    alert('Todo tidak boleh kosong!')
    return
  }

  const todoItem = {
    id: Date.now(),
    text: newTodo.value,
    completed: false
  }

  todos.value.push(todoItem)
  newTodo.value = ''
}

// Computed
const filteredTodos = computed(() => {
  return todos.value
})
</script>

<template>
  <div class="todo-container">
    <div class="todo-input">
      <input
        v-model="newTodo"
        @keyup.enter="addTodo"
        type="text"
        placeholder="Ketik todo baru..."
      />
      <button @click="addTodo">Add</button>
    </div>

    <div class="todo-list">
      <ul>
        <li v-for="todo in filteredTodos" :key="todo.id">
          {{ todo.text }}
        </li>
      </ul>
    </div>
  </div>
</template>

<style scoped>
/* Style akan dilengkapi di bagian Styling & UX nanti */
</style>

Testing Display List

Menampilkan todo list
Menampilkan todo list

Coba add beberapa todo dan lihat apakah muncul di list di bawah input form. Kalau ada banyak todo, pastikan semuanya ter-render dengan baik dan nggak ada error di browser console. Kalau oke, berarti step 2 udah sukses dan kita siap lanjut ke step 3 untuk tambah fitur toggle complete.

Step 3: Toggle Complete Status Todo

Sekarang kita lanjut ke step ketiga, yaitu tambah kemampuan untuk mark todo sebagai selesai atau belum. Ini bagian update dari CRUD. Kita pakai checkbox dan v-model untuk ikat checkbox sama property completed.

Template dengan Checkbox

Mari kita lihat struktur HTML untuk tambah checkbox di setiap todo item:

<li v-for="todo in filteredTodos" :key="todo.id">
  <input
    type="checkbox"
    v-model="todo.completed"
  />
  <span :class="{ completed: todo.completed }">
    {{ todo.text }}
  </span>
</li>

v-model di sini ikat checkbox sama property completed. Kalau user klik checkbox, property berubah otomatis. Ini dua arah binding yang powerful.

Perhatikan juga :class="{ completed: todo.completed }". Class completed bakal ditambah ke span kalau todo.completed true. Ini buat visual todo berubah.

Conditional Styling dengan Class Binding

Kita pakai :class dengan object syntax. Key adalah nama class, value adalah kondisi boolean.

<span :class="{ completed: todo.completed }">
  {{ todo.text }}
</span>

Kalau todo.completed true, class completed ditambah. Kalau false, class dihapus. Sangat simple.

CSS yang kita butuhkan:

.completed {
  text-decoration: line-through;
  opacity: 0.6;
}

Ini bikin todo text punya garis coretan dan opacity lebih rendah. Jadi visual-nya jelas bahwa todo udah selesai.

Kode Lengkap untuk Step 3

Ini adalah kode dari step 1 dan 2, sekarang ditambah checkbox dan conditional styling:

<script setup>
import { ref, computed } from 'vue'

// Data
const todos = ref([])
const newTodo = ref('')
const filter = ref('all')

// Methods
const addTodo = () => {
  if (newTodo.value.trim() === '') {
    alert('Todo tidak boleh kosong!')
    return
  }

  const todoItem = {
    id: Date.now(),
    text: newTodo.value,
    completed: false
  }

  todos.value.push(todoItem)
  newTodo.value = ''
}

// Computed
const filteredTodos = computed(() => {
  return todos.value
})
</script>

<template>
  <div class="todo-container">
    <div class="todo-input">
      <input
        v-model="newTodo"
        @keyup.enter="addTodo"
        type="text"
        placeholder="Ketik todo baru..."
      />
      <button @click="addTodo">Add</button>
    </div>

    <div class="todo-list">
      <ul>
        <li v-for="todo in filteredTodos" :key="todo.id">
          <input
            type="checkbox"
            v-model="todo.completed"
          />
          <span :class="{ completed: todo.completed }">
            {{ todo.text }}
          </span>
        </li>
      </ul>
    </div>
  </div>
</template>

<style scoped>
.completed {
  text-decoration: line-through;
  opacity: 0.6;
}
/* Style lainnya akan dilengkapi di bagian Styling & UX nanti */
</style>

Testing Toggle Complete

Toggle complate
Toggle complate

Coba add beberapa todo, terus klik checkbox di samping salah satu todo. Seharusnya text berubah sama garis coretan dan warna lebih pudar. Klik lagi checkbox bakal kembali ke normal. Itu tunjukin toggle complete udah bekerja.

Sekarang step 3 udah selesai. Lanjut ke step 4 untuk tambah fitur delete todo.

Step 4: Delete Todo

Sekarang kita lanjut ke step keempat, yaitu tambah fitur delete todo. Ini adalah bagian delete dari CRUD. Kita bikin tombol delete di samping setiap todo, dan ketika diklik, todo bakal dihapus dari list selamanya.

Template dengan Delete Button

Mari kita lihat struktur HTML untuk tambah delete button:

<li v-for="todo in filteredTodos" :key="todo.id">
  <input
    type="checkbox"
    v-model="todo.completed"
  />
  <span :class="{ completed: todo.completed }">
    {{ todo.text }}
  </span>
  <button @click="deleteTodo(todo.id)">Delete</button>
</li>

Button delete punya @click listener yang panggil function deleteTodo dan pass todo.id sebagai parameter. Ini important karena kita perlu tahu todo mana yang bakal dihapus.

Method deleteTodo

Sekarang mari kita lihat function deleteTodo yang handle logic penghapusan:

const deleteTodo = (id) => {
  todos.value = todos.value.filter(t => t.id !== id)
}

Function ini sangat simple. Kita pakai array filter method untuk create array baru yang berisi semua todo kecuali todo dengan id yang match. Filter method ini bukan menghapus dari array original, tapi create array baru, terus kita assign balik ke todos.value.

Cara kerjanya gini: filter iterate setiap todo dalam array, dan return hanya todo yang t.id !== id. Jadi semua todo yang id-nya sama dengan parameter id bakal di-exclude dari array baru ini.

Kode Lengkap untuk Step 4

Sekarang mari kita lihat kode lengkap dengan delete functionality udah diimplementasikan:

<script setup>
import { ref, computed } from 'vue'

// Data
const todos = ref([])
const newTodo = ref('')
const filter = ref('all')

// Methods
const addTodo = () => {
  if (newTodo.value.trim() === '') {
    alert('Todo tidak boleh kosong!')
    return
  }

  const todoItem = {
    id: Date.now(),
    text: newTodo.value,
    completed: false
  }

  todos.value.push(todoItem)
  newTodo.value = ''
}

const deleteTodo = (id) => {
  todos.value = todos.value.filter(t => t.id !== id)
}

// Computed
const filteredTodos = computed(() => {
  return todos.value
})
</script>

<template>
  <div class="todo-container">
    <div class="todo-input">
      <input
        v-model="newTodo"
        @keyup.enter="addTodo"
        type="text"
        placeholder="Ketik todo baru..."
      />
      <button @click="addTodo">Add</button>
    </div>

    <div class="todo-list">
      <ul>
        <li v-for="todo in filteredTodos" :key="todo.id">
          <input
            type="checkbox"
            v-model="todo.completed"
          />
          <span :class="{ completed: todo.completed }">
            {{ todo.text }}
          </span>
          <button @click="deleteTodo(todo.id)">Delete</button>
        </li>
      </ul>
    </div>
  </div>
</template>

<style scoped>
.completed {
  text-decoration: line-through;
  opacity: 0.6;
}
/* Style lainnya akan dilengkapi di bagian Styling & UX nanti */
</style>

Testing Delete Todo

Delete Tode

Coba add beberapa todo, terus klik delete button di samping salah satu todo. Todo bakal langsung hilang dari list. Coba delete beberapa todo lagi untuk pastikan semuanya bekerja dengan benar.

Sekarang step 4 udah selesai dan kita sudah punya semua operasi CRUD yang lengkap. Lanjut ke step 5 untuk tambah fitur counter.

Step 5: Counters dengan Computed Properties

Sekarang kita lanjut ke step kelima, yaitu tambah counter untuk tampilin berapa total todo, berapa aktif, dan berapa selesai. Ini fitur berguna buat user lihat progress mereka dengan cepat. Kita pakai computed properties untuk handle logika ini.

Computed Properties untuk Counter

Mari kita lihat structure computed properties yang kita butuhkan:

const totalTodos = computed(() =>
  todos.value.length
)

const completedTodos = computed(() =>
  todos.value.filter(t => t.completed).length
)

const activeTodos = computed(() =>
  totalTodos.value - completedTodos.value
)

totalTodos hitung berapa banyak todo dengan ambil length dari array todos. Simple banget.

completedTodos hitung berapa banyak todo yang selesai. Kita pakai filter method untuk ambil hanya todo yang completed true, terus hitung lengthnya.

activeTodos hitung berapa todo yang masih aktif. Caranya kurangi totalTodos sama completedTodos. Ini lebih efisien dibanding filter ulang.

Kenapa Pakai Computed Properties

Alasan kita pakai computed adalah karena mereka otomatis ter-update kalau dependency berubah. Kalau pakai function biasa, kita harus panggil terus-menerus. Dengan computed, Vue otomatis track dan update kalau ada perubahan.

Ini konsep reaktif yang powerful di Vue. Semuanya otomatis tanpa perlu manual trigger.

Template untuk Display Counter

Sekarang mari kita lihat cara tampilin counter di template:

<div class="todo-stats">
  <div class="stat">
    <strong>Total:</strong> {{ totalTodos }}
  </div>
  <div class="stat">
    <strong>Active:</strong> {{ activeTodos }}
  </div>
  <div class="stat">
    <strong>Completed:</strong> {{ completedTodos }}
  </div>
</div>

Kita tampilin setiap counter dalam bentuk div yang rapi. Pakai interpolation {{ }} untuk tampilin value dari computed. Kalau value berubah, Vue otomatis update tampilan di halaman.

Kode Lengkap untuk Step 5

Ini adalah kode lengkap dengan counter functionality udah diimplementasikan:

<script setup>
import { ref, computed } from 'vue'

// Data
const todos = ref([])
const newTodo = ref('')
const filter = ref('all')

// Methods
const addTodo = () => {
  if (newTodo.value.trim() === '') {
    alert('Todo tidak boleh kosong!')
    return
  }

  const todoItem = {
    id: Date.now(),
    text: newTodo.value,
    completed: false
  }

  todos.value.push(todoItem)
  newTodo.value = ''
}

const deleteTodo = (id) => {
  todos.value = todos.value.filter(t => t.id !== id)
}

// Computed
const totalTodos = computed(() =>
  todos.value.length
)

const completedTodos = computed(() =>
  todos.value.filter(t => t.completed).length
)

const activeTodos = computed(() =>
  totalTodos.value - completedTodos.value
)

const filteredTodos = computed(() => {
  return todos.value
})
</script>

<template>
  <div class="todo-container">
    <div class="todo-input">
      <input
        v-model="newTodo"
        @keyup.enter="addTodo"
        type="text"
        placeholder="Ketik todo baru..."
      />
      <button @click="addTodo">Add</button>
    </div>

    <div class="todo-stats">
      <div class="stat">
        <strong>Total:</strong> {{ totalTodos }}
      </div>
      <div class="stat">
        <strong>Active:</strong> {{ activeTodos }}
      </div>
      <div class="stat">
        <strong>Completed:</strong> {{ completedTodos }}
      </div>
    </div>

    <div class="todo-list">
      <ul>
        <li v-for="todo in filteredTodos" :key="todo.id">
          <input
            type="checkbox"
            v-model="todo.completed"
          />
          <span :class="{ completed: todo.completed }">
            {{ todo.text }}
          </span>
          <button @click="deleteTodo(todo.id)">Delete</button>
        </li>
      </ul>
    </div>
  </div>
</template>

<style scoped>
.completed {
  text-decoration: line-through;
  opacity: 0.6;
}
/* Style lainnya akan dilengkapi di bagian Styling & UX nanti */
</style>

Testing Counter

Menambahkan counter
Menambahkan counter

Coba add beberapa todo dan lihat apakah counter total berubah. Terus klik checkbox di beberapa todo dan lihat apakah counter completed dan active berubah sesuai. Setiap kali kamu change status atau add todo, counter harus otomatis ter-update tanpa perlu refresh.

Sekarang step 5 udah selesai dan counter-nya udah berjalan sempurna. Lanjut ke step 6 untuk tambah fitur filter todo berdasarkan status.

Step 6: Filter Todos Berdasarkan Status

Sekarang kita lanjut ke step keenam, yaitu tambah fitur filter untuk tampilin todo berdasarkan status mereka. User bisa filter untuk lihat semua todo, hanya yang aktif, atau hanya yang selesai. Ini fitur berguna kalau todo list sudah panjang.

Filter Buttons

Mari kita lihat struktur HTML untuk buat filter buttons:

<div class="filter-buttons">
  <button
    :class="{ active: filter === 'all' }"
    @click="filter = 'all'"
  >
    All ({{ totalTodos }})
  </button>
  <button
    :class="{ active: filter === 'active' }"
    @click="filter = 'active'"
  >
    Active ({{ activeTodos }})
  </button>
  <button
    :class="{ active: filter === 'completed' }"
    @click="filter = 'completed'"
  >
    Completed ({{ completedTodos }})
  </button>
</div>

Setiap button punya conditional class binding :class="{ active: filter === 'all' }" untuk highlight button yang aktif. Ketika user klik button, kita update reactive data filter dengan nilai baru.

Kita juga tampilin counter di dalam setiap button kayak "All (5)" atau "Active (3)". Jadi user bisa lihat berapa item dalam setiap kategori.

Computed filteredTodos

Sekarang mari kita lihat bagaimana computed filteredTodos bekerja. Ini yang paling penting karena ini yang tentuin mana todo yang ditampilin di list:

const filteredTodos = computed(() => {
  if (filter.value === 'active') {
    return todos.value.filter(t => !t.completed)
  }
  if (filter.value === 'completed') {
    return todos.value.filter(t => t.completed)
  }
  return todos.value // 'all'
})

Logic-nya simple: kalau filter adalah 'active', kita return hanya todo yang completed false. Kalau filter adalah 'completed', kita return hanya todo yang completed true. Kalau filter adalah 'all', kita return semua todo.

Kita pakai filter method dengan kondisi yang berbeda. Untuk active, kita pakai !t.completed yang berarti not completed. Untuk completed, kita pakai t.completed yang berarti completed true.

Kode Lengkap untuk Step 6

Ini adalah kode lengkap dengan filter functionality udah diimplementasikan:

<script setup>
import { ref, computed } from 'vue'

// Data
const todos = ref([])
const newTodo = ref('')
const filter = ref('all')

// Methods
const addTodo = () => {
  if (newTodo.value.trim() === '') {
    alert('Todo tidak boleh kosong!')
    return
  }

  const todoItem = {
    id: Date.now(),
    text: newTodo.value,
    completed: false
  }

  todos.value.push(todoItem)
  newTodo.value = ''
}

const deleteTodo = (id) => {
  todos.value = todos.value.filter(t => t.id !== id)
}

// Computed
const totalTodos = computed(() =>
  todos.value.length
)

const completedTodos = computed(() =>
  todos.value.filter(t => t.completed).length
)

const activeTodos = computed(() =>
  totalTodos.value - completedTodos.value
)

const filteredTodos = computed(() => {
  if (filter.value === 'active') {
    return todos.value.filter(t => !t.completed)
  }
  if (filter.value === 'completed') {
    return todos.value.filter(t => t.completed)
  }
  return todos.value
})
</script>

<template>
  <div class="todo-container">
    <div class="todo-input">
      <input
        v-model="newTodo"
        @keyup.enter="addTodo"
        type="text"
        placeholder="Ketik todo baru..."
      />
      <button @click="addTodo">Add</button>
    </div>

    <div class="todo-stats">
      <div class="stat">
        <strong>Total:</strong> {{ totalTodos }}
      </div>
      <div class="stat">
        <strong>Active:</strong> {{ activeTodos }}
      </div>
      <div class="stat">
        <strong>Completed:</strong> {{ completedTodos }}
      </div>
    </div>

    <div class="filter-buttons">
      <button
        :class="{ active: filter === 'all' }"
        @click="filter = 'all'"
      >
        All ({{ totalTodos }})
      </button>
      <button
        :class="{ active: filter === 'active' }"
        @click="filter = 'active'"
      >
        Active ({{ activeTodos }})
      </button>
      <button
        :class="{ active: filter === 'completed' }"
        @click="filter = 'completed'"
      >
        Completed ({{ completedTodos }})
      </button>
    </div>

    <div class="todo-list">
      <ul>
        <li v-for="todo in filteredTodos" :key="todo.id">
          <input
            type="checkbox"
            v-model="todo.completed"
          />
          <span :class="{ completed: todo.completed }">
            {{ todo.text }}
          </span>
          <button @click="deleteTodo(todo.id)">Delete</button>
        </li>
      </ul>
    </div>
  </div>
</template>

<style scoped>
.completed {
  text-decoration: line-through;
  opacity: 0.6;
}
/* Style lainnya akan dilengkapi di bagian Styling & UX nanti */
</style>

Testing Filter

Filtetr Todo List
Filtetr Todo List

Coba add beberapa todo, terus tandai beberapa sebagai completed. Sekarang klik filter button "Active" dan seharusnya hanya todo yang belum selesai yang tampil. Klik "Completed" dan seharusnya hanya todo selesai yang tampil. Klik "All" dan semua todo bakal tampil lagi.

Button yang aktif seharusnya ter-highlight sama class active. Setiap button juga tampilin counter, jadi user bisa lihat berapa todo dalam setiap kategori. Kalau semuanya bekerja baik, berarti step 6 udah sukses dan aplikasi kita punya fitur filter yang lengkap.

Complete Code: TodoApp.vue Lengkap

Sekarang kita lihat full code dari TodoApp.vue yang udah kita buat selama 6 steps tadi. Ini adalah kombinasi dari semua step yang sudah kita lakukan sebelumnya. Jangan khawatir kalau belum begitu familiar, kita akan bahas struktur keseluruhan-nya.

Struktur Code Keseluruhan

Kode TodoApp.vue terdiri dari tiga bagian utama yang sudah kita buat di setiap step:

State Management: Tiga reactive variables - todos untuk menyimpan semua todo items, newTodo untuk input field, dan filter untuk status filter yang aktif.

Methods: Dua method utama - addTodo untuk tambah todo baru (step 1) dan deleteTodo untuk hapus todo (step 4).

Computed Properties: Lima computed values - totalTodos, completedTodos, activeTodos untuk counters (step 5), dan filteredTodos untuk logic filtering (step 6).

Template: Empat section utama - input form (step 1), counter stats (step 5), filter buttons (step 6), dan todo list dengan checkbox dan delete button (step 2, 3, 4).

Style: Basic style untuk completed class yang membuat garis coretan dan opacity (step 3), plus placeholder untuk styling lengkap nanti.

Styling dan UX: Membuat Aplikasi Lebih Cantik

Sekarang kita tambahkan styling untuk bikin aplikasi todo list kita lebih bagus dan user-friendly. Kita bakal bikin CSS yang modern dengan shadow, warna hijau Vue JS yang vibrant, dan design yang clean. Yang penting juga pastikan responsive di semua device.

CSS untuk Semua Elemen

Mari kita lihat CSS yang bisa kita tambahkan ke style section TodoApp.vue:

/* GLOBAL STYLES */
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

/* CONTAINER & LAYOUT */
.todo-container {
  max-width: 700px;
  margin: 0 auto;
  padding: 30px 20px;
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  background: #f5f7fa;
  min-height: 100vh;
}

/* FORM INPUT & BUTTON */
.todo-input {
  display: flex;
  gap: 10px;
  margin-bottom: 30px;
  background: white;
  padding: 20px;
  border-radius: 12px;
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
}

.input-field {
  flex: 1;
  padding: 12px 16px;
  border: 2px solid #e0e0e0;
  border-radius: 8px;
  font-size: 15px;
  transition: all 0.3s ease;
  outline: none;
}

.input-field:focus {
  border-color: #42b983;
  box-shadow: 0 0 0 3px rgba(66, 185, 131, 0.1);
}

.btn-add {
  padding: 12px 28px;
  background: #42b983;
  color: white;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  font-weight: 600;
  font-size: 15px;
  transition: all 0.3s ease;
  box-shadow: 0 4px 15px rgba(66, 185, 131, 0.4);
}

.btn-add:hover {
  background: #35a372;
  transform: translateY(-2px);
  box-shadow: 0 6px 20px rgba(66, 185, 131, 0.6);
}

.btn-add:active {
  transform: translateY(0);
}

/* COUNTERS DISPLAY */
.todo-stats {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 15px;
  margin: 25px 0;
}

.stat {
  background: white;
  padding: 20px;
  border-radius: 12px;
  box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08);
  text-align: center;
  transition: all 0.3s ease;
}

.stat:hover {
  transform: translateY(-5px);
  box-shadow: 0 8px 25px rgba(0, 0, 0, 0.12);
}

.stat strong {
  display: block;
  color: #42b983;
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 1px;
  margin-bottom: 8px;
}

.stat {
  font-size: 28px;
  font-weight: bold;
  color: #333;
}

/* FILTER BUTTONS */
.filter-buttons {
  display: flex;
  justify-content: center;
  gap: 10px;
  margin: 25px 0;
  flex-wrap: wrap;
}

.btn-filter {
  padding: 10px 20px;
  border: 2px solid #e0e0e0;
  background-color: white;
  color: #333;
  cursor: pointer;
  border-radius: 8px;
  font-weight: 600;
  font-size: 14px;
  transition: all 0.3s ease;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}

.btn-filter:hover {
  border-color: #42b983;
  color: #42b983;
  transform: scale(1.05);
  box-shadow: 0 4px 12px rgba(66, 185, 131, 0.2);
}

.btn-filter.active {
  background-color: #42b983;
  color: white;
  border-color: #42b983;
  box-shadow: 0 4px 15px rgba(66, 185, 131, 0.4);
}

/* TODO LIST */
.todo-list {
  background: white;
  border-radius: 12px;
  overflow: hidden;
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
}

.todo-list ul {
  list-style: none;
  padding: 0;
  margin: 0;
}

/* TODO LIST ITEMS */
.todo-item {
  padding: 16px 20px;
  border-bottom: 1px solid #f0f0f0;
  display: flex;
  align-items: center;
  gap: 12px;
  transition: all 0.3s ease;
  background: white;
}

.todo-item:hover {
  background-color: #f8f9fa;
  padding-left: 24px;
}

.todo-item:last-child {
  border-bottom: none;
}

/* CHECKBOX CUSTOM STYLING */
.checkbox {
  width: 20px;
  height: 20px;
  cursor: pointer;
  accent-color: #667eea;
  flex-shrink: 0;
}

/* TODO TEXT */
.todo-text {
  flex: 1;
  word-break: break-word;
  color: #333;
  transition: all 0.3s ease;
}

.completed {
  text-decoration: line-through;
  color: #999;
  opacity: 0.7;
}

/* DELETE BUTTON */
.btn-delete {
  background: #f5576c;
  color: white;
  border: none;
  padding: 8px 14px;
  border-radius: 6px;
  cursor: pointer;
  font-weight: 600;
  font-size: 12px;
  transition: all 0.3s ease;
  flex-shrink: 0;
  box-shadow: 0 2px 8px rgba(245, 87, 108, 0.3);
}

.btn-delete:hover {
  background: #e63e5a;
  transform: scale(1.1);
  box-shadow: 0 4px 12px rgba(245, 87, 108, 0.5);
}

/* EMPTY STATE */
.empty-state {
  padding: 60px 20px;
  text-align: center;
  color: #999;
  font-size: 16px;
  background: white;
  border-radius: 12px;
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
}

/* RESPONSIVE DESIGN */
@media (max-width: 600px) {
  .todo-container {
    padding: 20px 15px;
    min-height: auto;
  }

  .todo-input {
    flex-direction: column;
    padding: 15px;
  }

  .input-field {
    width: 100%;
  }

  .btn-add {
    width: 100%;
  }

  .todo-stats {
    grid-template-columns: 1fr;
  }

  .filter-buttons {
    justify-content: center;
  }

  .btn-filter {
    font-size: 12px;
    padding: 8px 16px;
  }

  .todo-item {
    padding: 12px 15px;
  }

  .todo-item:hover {
    padding-left: 15px;
  }
}

Template Lengkap dengan Styling

Sekarang mari kita lihat bagaimana template lengkap yang sudah punya semua class styling:

<template>
  <div class="todo-container">
    <!-- INPUT FORM SECTION -->
    <div class="todo-input">
      <input
        ref="inputField"
        v-model="newTodo"
        @keyup.enter="addTodo"
        type="text"
        placeholder="Ketik todo baru..."
        class="input-field"
      />
      <button @click="addTodo" class="btn-add">Add</button>
    </div>

    <!-- COUNTER STATS SECTION -->
    <div class="todo-stats">
      <div class="stat">
        <strong>Total</strong>
        {{ totalTodos }}
      </div>
      <div class="stat">
        <strong>Active</strong>
        {{ activeTodos }}
      </div>
      <div class="stat">
        <strong>Completed</strong>
        {{ completedTodos }}
      </div>
    </div>

    <!-- FILTER BUTTONS SECTION -->
    <div class="filter-buttons">
      <button
        :class="{ active: filter === 'all' }"
        @click="filter = 'all'"
        class="btn-filter"
      >
        All ({{ totalTodos }})
      </button>
      <button
        :class="{ active: filter === 'active' }"
        @click="filter = 'active'"
        class="btn-filter"
      >
        Active ({{ activeTodos }})
      </button>
      <button
        :class="{ active: filter === 'completed' }"
        @click="filter = 'completed'"
        class="btn-filter"
      >
        Completed ({{ completedTodos }})
      </button>
    </div>

    <!-- TODO LIST SECTION -->
    <div v-if="filteredTodos.length === 0" class="empty-state">
      Belum ada todo untuk kategori ini. Mulai tambahkan todo baru sekarang!
    </div>
    <div v-else class="todo-list">
      <ul>
        <li v-for="todo in filteredTodos" :key="todo.id" class="todo-item">
          <input
            type="checkbox"
            v-model="todo.completed"
            class="checkbox"
          />
          <span :class="{ completed: todo.completed }" class="todo-text">
            {{ todo.text }}
          </span>
          <button @click="deleteTodo(todo.id)" class="btn-delete">Delete</button>
        </li>
      </ul>
    </div>
  </div>
</template>

UX Tips untuk Pengalaman Lebih Baik

Pertama, fokus ke input field setelah user tambah todo. User bisa langsung ketik todo berikutnya tanpa perlu klik input ulang. Kita bisa pakai ref dan nextTick untuk ini.

Kedua, tambahkan hover effects dan animations. Ini kasih visual feedback yang membuat aplikasi terasa lebih interactive dan modern. Transform, shadow, dan color changes bikin semuanya lebih hidup.

Ketiga, gunakan smooth transitions untuk semua perubahan. Jangan langsung berubah, tapi kasih transisi 0.3 detik biar lebih smooth. Ini bikin aplikasi terasa lebih polished dan professional.

Keempat, tampilkan pesan ketika tidak ada todo. Lebih baik daripada tampilkan list kosong yang membingungkan. User bakal paham bahwa belum ada data.

Implementasi UX Tips

Untuk focus pada input setelah add, update script dengan nextTick:

import { ref, computed, nextTick } from 'vue'

const inputField = ref(null)

const addTodo = async () => {
  if (newTodo.value.trim() === '') {
    alert('Todo tidak boleh kosong!')
    return
  }

  const todoItem = {
    id: Date.now(),
    text: newTodo.value,
    completed: false
  }

  todos.value.push(todoItem)
  newTodo.value = ''

  // Focus ke input field setelah todo ditambah
  await nextTick()
  inputField.value?.focus()
}

Dengan styling modern dan UX tips ini, aplikasi todo list kamu sekarang udah terlihat professional, eye-catching, dan menyenangkan digunakan dengan semua button styling yang sudah include.

Testing dan Debug: Memastikan Aplikasi Berjalan Sempurna

Sekarang kita test aplikasi todo list kita untuk memastikan semua fitur bekerja dengan baik. Testing penting karena membantu kita menemukan dan memperbaiki bug sebelum digunakan.

Test fitur
Test fitur

Test Fitur Satu per Satu

  • Test add todo: Ketik beberapa todo dan pastikan ter-add dengan benar. Input field harus reset otomatis. Pastikan alert muncul kalau user coba add todo kosong.
  • Test toggle complete: Klik checkbox dan pastikan status berubah. Todo yang completed seharusnya punya garis coretan dan lebih pudar.
  • Test delete todo: Klik delete button dan pastikan todo hilang dari list.
  • Test filter switching: Klik all, active, dan completed. Pastikan hanya todo yang sesuai kategori yang ditampilin.
  • Test counter accuracy: Pastikan counter total, active, dan completed ter-update setiap kali ada perubahan.

Common Bugs yang Sering Terjadi

  • :key tidak unique: Pakai todo.id sebagai key, bukan index. Index berubah saat delete dan bikin Vue bingung.
  • v-model pada array item: Pastikan v-model benar-benar bound ke property completed. Cek syntax dan pastikan property ada di object todo.
  • Filter tidak update: Pastikan click handler dengan benar ngubah filter.value.
  • Counter tidak sync: Pastikan computed properties mendengarkan dependency dengan benar.

Menggunakan Vue DevTools

Install Vue DevTools dari Chrome Web Store. Buka DevTools dan klik Vue tab. Kamu bisa:

  • Lihat component tree dan semua reactive data
  • Inspect nilai todos, filter, newTodo, dan computed properties
  • Track history dari setiap state change
  • Pause dan analyze exact state saat bug terjadi

Tips Debug Praktis

  • Gunakan console.log untuk print state. Lihat nilai exact dari todos, filter, dan computed properties.
  • Isolate masalah. Kalau counter tidak sync, fokus hanya pada counter.
  • Add console.log di berbagai tempat untuk trace kapan code dijalankan.

Dengan testing sistematis, aplikasi kamu bakal robust dan reliable.

Improvements dan Challenges: Berkembang Lebih Jauh

Sekarang kamu udah punya aplikasi todo list yang fully fungsional dengan semua fitur CRUD. Tapi ini baru permulaan. Ada banyak improvement yang bisa kamu buat untuk membuat aplikasi lebih powerful dan berguna.

Ideas untuk Improve Aplikasi

  • Edit todo text: Tambahkan fitur buat edit teks todo yang sudah dibuat. User bisa klik todo dan ubah textnya tanpa perlu delete dan tambah ulang.
  • Due dates: Tambahkan field untuk tanggal deadline. User bisa set kapan todo harus selesai dan aplikasi bisa tampilkan todo yang sudah lewat batas waktu.
  • Priority levels: Tambahkan prioritas rendah, sedang, tinggi. User bisa sort todo berdasarkan prioritas mereka.
  • Categories atau tags: Biarkan user organize todo dalam berbagai kategori atau add multiple tags. Ini lebih flexible daripada hanya filter by status.
  • Search dan sort: Tambahkan search functionality buat cari todo by text. Sort todo berdasarkan tanggal dibuat, due date, atau prioritas.
  • LocalStorage persistence: Save todos ke browser localStorage jadi data tetap ada meskipun user refresh page atau tutup browser.

Challenge untuk Kamu

Sekarang giliran kamu untuk implement 1-2 improvements sendiri. Pilih salah satu atau lebih improvement dari list di atas dan kerjakan. Ini adalah cara terbaik untuk belajar dan perdalam pemahaman kamu tentang Vue.js.

Kalau kamu stuck, coba lihat kembali ke step-step sebelumnya dan adaptasi logika-nya untuk fitur baru. Atau cek dokumentasi Vue.js resmi di vuejs.org. Developer yang bagus tahu kapan harus research dan belajar hal baru.

Tips buat Implementation

  • Start simple: Jangan langsung implementasikan semua improvements sekaligus. Pilih satu dan kerjakan dengan baik. Setelah selesai, baru mulai yang kedua.
  • Test each feature: Setiap kali selesai implementasi feature baru, test dengan detail sebelum move ke feature berikutnya. Jangan accumulate bugs.
  • Commit sebelum add features: Kalau kamu pakai git, selalu commit working code sebelum mulai feature baru. Ini safety net kalau kamu buat mistake.

Dengan implementasi improvements sendiri, kamu bakal makin skilled dan confident dalam develop aplikasi Vue.js. Selamat mencoba dan have fun!

Kesimpulan: Sudah Menguasai Vue.js dengan Proyek Real

Selamat kamu sudah menyelesaikan tutorial Vue.js episode 4 ini. Perjalanan kamu dari belajar konsep dasar sampai membangun aplikasi todo list yang fully fungsional adalah bukti dedikasi dan kerja keras. Kamu punya hak untuk bangga dengan pencapaian ini.

Recap Apa yang Sudah Kita Pelajari

  • Aplikasi todo list selesai: Kamu udah membuat aplikasi real yang bisa digunakan dengan semua fitur CRUD yang lengkap.
  • Operasi CRUD dikuasai: Kamu paham cara membuat data, membaca data, mengubah data, dan menghapus data dengan Vue.js.
  • Semua konsep Vue.js diterapkan: Dari reactive data dengan ref, directive v-for dan v-if, binding dengan v-model, computed properties, sampai conditional styling. Semuanya udah kamu praktikkan dalam satu aplikasi.
  • Aplikasi yang benar-benar berfungsi: Bukan hanya teori, tapi kamu punya aplikasi nyata yang bisa dijalankan dan digunakan.

Skills yang Sudah Kamu Kuasai

  • Reactive state management: Kamu sekarang paham bagaimana Vue.js manage state dan otomatis update UI ketika data berubah.
  • Array manipulation: Kamu bisa pakai array methods seperti push, filter, dan map untuk manipulasi data dengan efficient.
  • Computed properties: Kamu tahu kapan dan gimana pakai computed properties untuk automatic calculations dan filtering.
  • Conditional rendering: Kamu bisa menampilkan atau sembunyikan elemen berdasarkan kondisi tertentu dengan v-if dan v-show.

Apa Selanjutnya

Kamu udah siap untuk melanjutkan ke episode berikutnya tentang komponen Vue.js. Di episode 5, kita bakal belajar tentang props dan events yang adalah fundamental skills dalam component-based development. Ini bakal buka pintu untuk membuat aplikasi yang lebih complex dan modular.

Jangan berhenti di sini. Terus practice, terus belajar, dan jangan takut buat experiment dengan kode. Setiap developer yang bagus dimulai dari sini, dari belajar konsep dasar dan praktik dengan proyek real.

Belajar Lebih Lanjut di BuildWithAngga

Kalau kamu ingin mempercepat learning journey kamu, BuildWithAngga punya banyak kursus Vue.js dengan mentor support. Kamu bisa belajar dari developer berpengalaman dan praktik langsung dengan real-world projects. Ini adalah investasi yang bagus untuk skill kamu sebagai developer.

Kunjungi BuildWithAngga.com untuk explore lebih banyak kursus dan resources. Di sana kamu bakal menemukan community yang supportif dan konten berkualitas tinggi yang bakal accelerate learning kamu.

Terima kasih sudah belajar bersama kami. Good luck dengan Vue.js journey kamu selanjutnya!