Proyek: Todo App — Vue

Bangun Todo App dengan Vue! Gabungkan semua konsep yang sudah dipelajari: const todos = ref([]) const input = ref("") const addTodo = () => { if…

Proyek Vue menggabungkan semua yang sudah kamu pelajari: komponen dengan props dan emits, data reaktif, computed, v-model, dan v-for, dibagi ke beberapa file dalam satu folder proyek. Bayangin hari pembukaan Warung Bu Sri. Dapur, papan menu, kasir, dan buku pesanan sudah dicoba satu per satu. Sekarang semuanya dipasang di satu warung, dan yang penting adalah siapa memegang apa: papan menu cuma menampilkan, kasir yang memegang buku pesanan.

🧪 Tebakan cepat: Di warung yang sudah jadi, ada tiga MenuCard dan satu daftar keranjang. Data keranjang sebaiknya disimpan di setiap MenuCard, atau di App? Tebak dulu, jawabannya ada di bagian "Siapa memegang data".

Membuat proyek dengan create-vue

create-vue adalah alat resmi untuk membuat proyek Vue baru dengan Vite:

npm create vue@latest

Perintah itu bertanya nama proyek dan fitur apa saja yang dipasang: TypeScript, JSX, Router, Pinia, Vitest, dan lainnya. Fiturnya juga bisa langsung disebut sebagai flag. Ini keluaran sungguhan create-vue 3.24 untuk proyek dengan Router dan Pinia:

$ npm create vue@latest warung-bu-sri -- --router --pinia

┌  Vue.js - The Progressive JavaScript Framework

Scaffolding project in /home/sri/warung-bu-sri...
│
└  Done. Now run:

   cd warung-bu-sri
   npm install
   npm run dev

npm run dev menyalakan server Vite, dan aplikasinya terbuka di http://localhost:5173. Struktur yang dibuat (sebagian):

warung-bu-sri/
├── index.html
├── package.json
├── vite.config.js
├── public/
└── src/
    ├── main.js
    ├── App.vue
    ├── assets/
    ├── components/
    ├── router/
    │   └── index.js
    ├── stores/
    │   └── counter.js
    └── views/
        ├── HomeView.vue
        └── AboutView.vue

Merakit Warung Bu Sri

App.vue                 cari, menu, keranjang, total
└── MenuCard.vue × 3    props: nama, harga, stok · emit: tambah
<!-- src/components/MenuCard.vue -->
<script setup>
defineProps({ nama: String, harga: Number, stok: Number })
defineEmits(["tambah"])
</script>

<template>
  <div class="kartu">
    <h3>{{ nama }}</h3>
    <p>Rp{{ harga }}</p>
    <button :disabled="stok === 0" @click="$emit('tambah', nama, harga)">
      {{ stok === 0 ? "Habis" : "Tambah" }}
    </button>
  </div>
</template>
<!-- src/App.vue -->
<script setup>
import { ref, computed } from "vue"
import MenuCard from "./components/MenuCard.vue"

const cari = ref("")
const menu = ref([
  { id: 1, nama: "Nasi Goreng", harga: 15000, stok: 8 },
  { id: 2, nama: "Mie Ayam", harga: 12000, stok: 0 },
  { id: 3, nama: "Es Teh", harga: 5000, stok: 20 },
])
const keranjang = ref([])
let nextId = 1

const menuTampil = computed(() =>
  menu.value.filter((m) => m.nama.toLowerCase().includes(cari.value.toLowerCase()))
)
const total = computed(() => keranjang.value.reduce((jumlah, item) => jumlah + item.harga, 0))

function tambahKeKeranjang(nama, harga) {
  keranjang.value.push({ id: nextId++, nama, harga })
}
</script>

<template>
  <input v-model="cari" placeholder="Cari menu" />
  <MenuCard
    v-for="m in menuTampil"
    :key="m.id"
    :nama="m.nama"
    :harga="m.harga"
    :stok="m.stok"
    @tambah="tambahKeKeranjang"
  />
  <ul>
    <li v-for="item in keranjang" :key="item.id">{{ item.nama }}, Rp{{ item.harga }}</li>
  </ul>
  <p>Total: Rp{{ total }}</p>
</template>
Aksi cari Menu yang tampil keranjang total
awal "" Nasi Goreng, Mie Ayam (Habis), Es Teh kosong 0
Tambah Nasi Goreng dua kali "" tiga-tiganya 2 item 30000
Tambah Es Teh "" tiga-tiganya 3 item 35000
ketik es "es" Es Teh 3 item 35000
Bagian Pelajaran asalnya
ref untuk cari, menu, keranjang reactive data
menuTampil dan total computed properties
v-model="cari" form dan v-model
v-for dengan :key="m.id" list rendering
:nama, :harga, :stok props
$emit('tambah', ...) dan @tambah emits
:disabled dan teks Habis template dan conditional rendering

Siapa memegang data

keranjang disimpan di App, satu tempat untuk satu data. MenuCard gak memegang keranjang; dia cuma mengirim tambah bersama nama dan harganya, dan App yang menambah ke keranjang lalu menghitung total. Itu jawaban tebakannya. Kalau setiap MenuCard menyimpan keranjangnya sendiri, ada tiga keranjang terpisah, dan gak ada satu tempat pun yang tahu totalnya.

Begitu warungnya punya beberapa halaman, misalnya halaman Menu dan halaman Keranjang lewat router, keranjang pindah ke store Pinia di src/stores/, seperti di pelajaran Pinia, supaya semua halaman membaca data yang sama.

Di editor latihan

Editor latihan menjalankan satu komponen, jadi seluruh warung ditulis di satu tempat dengan MenuCard sebagai objek dan <component :is="MenuCard">. Kode Playground di Lab berisi versi itu.

Latihannya versi paling kecil dari pola yang sama: Todo App. Starter-nya sudah punya todos, input, dan addTodo yang menambah isi kolom lalu mengosongkannya. Kamu cukup menulis template-nya:

<div>
  <input id="todo-input" v-model="input" />
  <button id="add-btn" @click="addTodo">Tambah</button>
  <ul id="todo-list">
    <li v-for="(todo, i) in todos" :key="i">{{ todo }}</li>
  </ul>
</div>

Pemeriksanya mengetik Belajar Vue ke #todo-input, mengklik #add-btn, lalu memastikan Belajar Vue muncul di #todo-list dan kolom ketiknya kembali kosong.

⚠️ Jebakan yang sering ketemu

TL;DR: npm create vue@latest membuat proyek Vue dengan Vite; fitur seperti Router dan Pinia dipilih saat dibuat, lalu npm install dan npm run dev membuka aplikasinya di http://localhost:5173. index.html di akar, src/main.js membuat dan memasang aplikasinya, components/ untuk potongan yang dipakai ulang, views/ untuk halaman yang dipetakan router/index.js, dan stores/ untuk Pinia. Warung Bu Sri dirakit dari ref, computed, v-model, v-for dengan :key, props, dan emits, dengan satu pemilik untuk setiap data: App memegang keranjang, MenuCard cuma mengirim tambah.

Yang akan kamu pelajari