- Sudah mengerti dan menggunakan
create-vue
- Mengerti dasar Vue 3 (
Options API
) - Mengerti dasar penggunaan CSS (pada pembelajaran ini menggunakan
tailwind
)
Dikutip dari situs Pinia, Pinia ini adalah Vue Store yang menyenangkan untuk digunakan.
Lebih detilnya, Pinia ini adalah library untuk Store di dalam Vue, yang membuat si developer dapat nge-share suatu state ke dalam suatu Components / Pages.
Masih bingung? Mari kita coba dengan bahasa yang lebih manusiawi lagi.
Pinia adalah suatu library untuk me-manage state pada Vue.
Anggap saja Pinia sebagai sebuah "wadah" untuk menyimpan data secara global pada Vue, sehingga kita tidak perlu menaruh data global pada App.vue
atau pada Component Utama
lagi DAN kita tidak perlu lagi untuk mem-passing data terlalu dalam / melakukan props
terlalu banyak a.k.a props drilling
.
Sudah mulai terdengar seru? mari kita coba install Pinia sekarang yah !
Untungnya Pinia ini sudah diincludekan di dalam template bawaan dari create-vue
, sehingga cara instalasinya pun menjadi cukup mudah:
- Menggunakan perintah
npm init vue@latest
- Masukkan
project name
yang diinginkan - Pilih opsi untuk menggunakan
Typescript
atau tidak (pada pembelajaran ini No) - Pilih opsi untuk menggunakan
JSX
atau tidak (pada pembelajaran ini No) - Pilih opsi untuk menggunakan
Vue Router
atau tidak (pada pembelajaran ini Yes) - Pilih opsi untuk menggunakan
Pinia
atau tidak (tentunya YES) - Pilih opsi untuk menggunakan
Vitest
atau tidak (pada pembelajaran ini No) - Pilih opsi untuk menggunakan
Cypress
atau tidak (pada pembelajaran ini No) - Pilih opsi untuk menggunakan
ESLint
atau tidak (pada pembelajaran ini Yes) - Pilih opsi untuk menggunakan
Prettier
atau tidak (pada pembelajaran ini Yes)
Sampai di tahap ini artinya initial project untuk Vue yang menggunakan Pinia
sudah berhasil dibuat, selanjutnya kita akan memasang dan menjalankan starter pack
dengan cara:
- Menggunakan perintah
cd nama_project_yang_dibuat
- Menginstall package yang dibutuhkan dengan perintah
npm install
- Menjalankan project dengan perintah
npm run dev
Dan kita bisa melihat starter pack
yang disediakan pada http://localhost:5173
(port default Vite
)
Selanjutnya kita akan belajar bagaimana cara menggunakan Pinia
Di dalam Pinia ini, akan ada beberapa istilah baru yang harus kita pelajari, yaitu:
- Store
- State
- Actions
- Getters
- Plugins
Sesuai namanya, Store ini adalah tempat di mana kita meletakkan keseluruhan kode yang mengandung global state management di dalam Pinia ini.
WARNING:
- Hati hati terhadap id yang harus digunakan di dalam store ini. HARUS bersifat unik !
Mari buka file src/stores/counter.js
untuk penjelasan lebih lanjut
defineStore({
id: 'counter',
...
})
Perhatikan id counter
, ini harus bersifat untuk dan tidak boleh sama antar store satu dengan store lainnya (bila nantinya ingin memecah store menjadi banyak).
State merupakan core dari Pinia ini, adalah data global yang disimpan pada Pinia yang nantinya akan digunakan oleh Component / Pages manapun di dalam Vue.
Ingat kata magicnya: MANAPUN
Actions ini anggapannya seperti methods
yang ada pada Vue "normal" yang digunakan dan boleh bersifat synchronous ataupun asynchronous.
Artinya di actions ini, kita bisa mendefinisikan methods
yang butuh melakukan proses dengan waktu yang tidak tentu seperti:
- Fetching data ...
- Fetching data
- FETCHING DATA !
Dan nantinya Actions ini juga bisa langsung di-teleport-kan ke dalam Component / Pages yang membutuhkan.
Oh ya, di dalam Actions ini pun, kita bisa mendefinisikan logic untuk mengubah State yang ada di dalam Store, mantap kan?
Getters, sesuai namanya, ini merupakan Getter (pengambil) State. Loh kenapa harus ada ini, padahal kan State boleh diambil secara langsung?
Anggap saja Getters ini adalah computed yang ada pada Vue, yaitu membaca state dan mengembalikan data hasil komputasinya.
mis, dari data State yang merupakan Array of Object, kita ingin memfilter sesuatu berdasarkan id nya, atau kata kunci lainnya, maka kita bisa menggunakan Getter untuk hal tersebut ๐
Pada pembelajaran ini tidak didemokan yah !
Yuk yuk sudah cukup teorinya, mari sekarang kita akan mulai untuk menggunakan Pinia ke dalam aplikasi kita yah !
Pada pembelajaran ini kita akan membuat sebuah aplikasi berbasis Pinia yang memiliki beberapa fitur utama:
- Sebuah button yang berfungsi untuk menambahkan suatu angka, yang dibuat dalam 2 component yang berbeda (Kiri: button, kanan: tampilan hasil penambahan angka)
- Sebuah form dan hasil panggilan formnya, dibuat dalam 2 component yang berbeda juga. (Kiri: Form, Kanan, tampilan hasil form setelah disubmit)
- Sebuah tabel yang akan menampilkan list jokes dari situs https://v2.jokeapi.dev/, terdiri dari 2 component berupa
TableList
yang didalamnya terdapat componentTableContent
.
Keseluruhan aplikasi ini akhirnya akan memanfaatkan Pinia yah !
Bagaimanakah Caranya?
- Menggunakan perintah
npm init vue@latest
untuk membuat project awal Vue yang menggunakanVue Router
danPinia
- Karena pada pembelajaran ini menggunakan tailwind, maka di sini juga menggunakan tutorial tambahan dari situs Tailwind secara langsung yang dapat dilihat di sini untuk menginstall tailwind css
- Selanjutnya karena pembelajaran ini juga menggunakan fetcher data
axios
, tambahkan packageaxios
dengan menggunakan perintahnpm install axios
Ya ! Pada pembelajaran ini, templatenya akan kita buat sendiri yah (tenang, dipandu untuk kode-nya kok supaya tidak berbusa otak dan mulutnya ๐)
- Membuat beberapa components (pada folder
src/components
) dengan nama seperti di bawah ini:IncrementalLeft.vue
IncrementalRight.vue
FormLeft.vue
FormRight.vue
TableList.vue
TableContent.vue
- Memodifikasi File
IncrementalLeft.vue
sebagai berikut:<script> export default { name: "IncrementalLeft", }; </script> <template> <button class="bg-gray-100 py-2 px-4 rounded-md hover:bg-gray-300 hover:shadow-md" > Increment </button> </template> <style scoped></style>
- Memodifikasi File
IncrementalRight.vue
sebagai berikut:<script> export default { name: "IncrementalRight", }; </script> <template> <div> <p class="font-semibold">0</p> </div> </template> <style scoped></style>
- Memodifikasi File
FormLeft.vue
sebagai berikut:<script> export default { name: "FormLeft", }; </script> <template> <form action="#"> <div> <input type="text" placeholder="Just write me" class="shadow appearance-none border rounded py-2 px-3 text-gray-700" /> </div> <div class="mt-4"> <button type="submit" class="bg-gray-100 py-2 px-4 rounded-md hover:bg-gray-300 hover:shadow-md" > Transfer Me </button> </div> </form> </template> <style scoped></style>
- Memodifikasi File
FormRight.vue
sebagai berikut:<script> export default { name: "FormRight", }; </script> <template> <div> <p class="font-semibold">Placeholder for the Form Value</p> </div> </template> <style scoped></style>
- Memodifikasi File
TableList.vue
sebagai berikut:<script> import TableContent from "./TableContent.vue"; export default { components: { TableContent }, name: "TableList", }; </script> <template> <table class="table-fixed w-full border-collapse border border-gray-300"> <thead> <tr class="bg-gray-100"> <th class="w-1/6 border border-gray-300">ID</th> <th class="w-1/3 border border-gray-300">Setup</th> <th class="w-1/3 border border-gray-300">Punchline</th> </tr> </thead> <table-content></table-content> </table> </template> <style scoped></style>
- Memodifikasi File
TableContent.vue
sebagai berikut:<script> export default { name: "TableContent", }; </script> <template> <tbody> <tr> <td class="border border-gray-300">Placeholder ID</td> <td class="border border-gray-300">Placeholder Setup</td> <td class="border border-gray-300">Placeholder Punchline</td> </tr> </tbody> </template> <style scoped></style>
- Memodifikasi File
src/views/AboutView.vue
sebagai berikut:<script> import FormLeft from "../components/FormLeft.vue"; import FormRight from "../components/FormRight.vue"; import IncrementalLeft from "../components/IncrementalLeft.vue"; import IncrementalRight from "../components/IncrementalRight.vue"; import TableList from "../components/TableList.vue"; export default { components: { IncrementalLeft, IncrementalRight, FormLeft, FormRight, TableList, }, }; </script> <template> <div class="bg-gray-200 text-gray-700 p-4"> <div class="container flex flex-col mx-auto"> <!-- Incremental Section --> <div class="mx-auto py-4"> <h3 class="font-bold">Incremental Section</h3> </div> <div class="flex flex-row mx-auto py-4"> <incremental-left class="mr-4"></incremental-left> <incremental-right class="ml-4 my-auto"></incremental-right> </div> <!-- Form Section --> <div class="mx-auto py-4"> <h3 class="font-bold">Form Section</h3> </div> <div class="flex flex-row mx-auto py-4"> <form-left class="mr-4"></form-left> <form-right class="ml-4 my-auto"></form-right> </div> <!-- Table Section --> <div class="mx-auto py-4"> <h3 class="font-bold">Table Section</h3> </div> <div class="mx-auto"> <table-list></table-list> </div> </div> </div> </template> <style scoped></style>
Sampai pada tahap ini artinya kita sudah membuat seluruh template yang digunakan untuk pembelajaran ini dan kita siap untuk masuk ke dalam pembuatan kode dengan Pinia !
Pada langkah ini kita akan menyelesaikan logika untuk Incremental pada component
IncrementalLeft
dan IncrementalRight
.
Ketika button pada IncrementalLeft
ditekan, akan menambahkan angka yang ada pada IncrementalRight
sebesar 10000
.
Sekarang untuk bisa menyelesaikan permasalahan ini, kita akan membutuhkan sebuah state
pada store
agar dapat digunakan di component yang akan digunakan.
Langkah pengerjaannya adalah sebagai berikut:
- Membuat sebuah stores yang baru dengan nama
src/stores/custom.js
- Memodifikasi kode pada
src/stores/custom.js
sebagai berikut:// import defineStore dari pinia import { defineStore } from "pinia"; // export store yang dibuat agar dapat digunakan // defineStore adalah sebuah fungsi yang menerima options export const customStore = defineStore({ // salah satu options-nya wajib ada 'id' id: "custom", // di sini kita akan declare state yang ada di dalam store ini // state adalah sebuah fungsi (disarankan dengan arrow function) // fungsi ini akan mereturn sebuah object (mirip data pada component vue) state: () => ({ // declare state yang dibutuhkan initialNumber: 10, }), });
- Selanjutnya kita akan menyambungkan state yang ada pada
src/stores/custom.js
ini pada componentIncrementalRight.vue
, namun dimanakah kita menaruh stateinitialNumber
ini? Karena data ini bisa berubah terus menerus, maka tentunya kita tidak bisa menaruh state yang ada di stores ini di dalamdata
, tapi kita harus menaruhnya di dalam ...computed
! - Cara untuk meletakkannya adalah dengan membaca dokumentasi pinia state dapat dilihat di sini).
- Memodifikasi file
IncrementalRight.vue
sebagai berikut:<script> // import mapState dari pinia import { mapState } from "pinia"; // import store yang digunakan import { customStore } from "../stores/custom"; export default { name: "IncrementalRight", // state dari pinia akan kita letakkan pada "computed" // yang ada di component computed: { // gunakan spread agar bisa dikombinasikan dengan computed // pada local component ini. // mapState menerima 2 parameter: // parameter 1 adalah store yang digunakan // parameter 2 adalah array of string dari state yang ingin // igunakan dari store ...mapState(customStore, ["initialNumber"]), }, }; </script> <template> <div> <p class="font-semibold"> <!-- di sini kita akan menggunakan computed-nya --> {{ initialNumber }} </p> </div> </template> <style scoped></style>
- Dan
voila !
, kita sudah berhasil untuk membaca data state dan menaruhnya pada componentIncrementalRight.vue
ini ! - Selanjutnya kita akan mencoba untuk membuat fungsi untuk menambahkan state ketika tombol yang ada pada
IncrementalLeft.vue
ini ditekan. Untuk itu, sekarang kita harus memodifikasisrc/stores/custom.js
untuk membuat fungsi tersebut. Fungsi ini akan kita buat pada bagianactions
yang ada pada stores, dan modifikasi kodenya adalah sebagai berikut:import { defineStore } from "pinia"; export const customStore = defineStore({ id: "custom", state: () => ({ initialNumber: 10, }), // di sini kita akan declare sebuah fungsi (methods) yang akan menambahkan initialNumber sebesar 10000 // dengan menggunakan "actions". // anggap ini seperti "methods" pada component Vue actions: { // di sini kita akan membuat method-nya // karena ini akan mengubah state secara langsung // maka kita tidak membutuhkan `async` incrementInitialNumber() { // untuk akses state tinggal menggunakan "this" this.initialNumber += 10000; }, }, });
- Selanjutnya kita tinggal menggunakan
actions
yang sudah kita buat ini padaIncrementalLeft.vue
. MemodifikasiIncrementalLeft.vue
sebagai berikut:<script> // di sini kita akan import mapActions import { mapActions } from "pinia"; // jangan lupa import store yang digunakan import { customStore } from "../stores/custom"; export default { name: "IncrementalLeft", // actions akan kita letakkan pada.... methods ! methods: { // gunakan spread seperti mapState ...mapActions(customStore, ["incrementInitialNumber"]), // kita coba juga membuat local methodsnya buttonOnClickHandler() { console.log("Tombol tertekan"); // invoke actions this.incrementInitialNumber(); }, }, }; </script> <template> <!-- Sambungkan event onclick dengan methods buttonOnClickHandler --> <button class="bg-gray-100 py-2 px-4 rounded-md hover:bg-gray-300 hover:shadow-md" v-on:click="buttonOnClickHandler" > Increment </button> </template> <style scoped></style>
Sampai pada tahap ini artinya kita sudah berhasil menggunakan state
dan actions
dari Pinia pada 2 component yang terpisah, IncrementalLeft.vue
dan IncrementalRight.vue
.
Cukup menakjubkan bukan? Langsung nyambung loh, padahal adanya di store
Pada bagian ini kita akan mencoba untuk menyelesaikan bagian Form, dimana pada saat form yang ada pada FormLeft
ini di-submit
, maka akan memengaruhi konten yang ditampilkan pada FormRight
.
Langkah-langkah pengerjaannya adalah sebagai berikut:
- Memodifikasi file
src/stores/custom.js
untuk menambahkan sebuahstate
baru dengan namaformData
dan sebuahactions
untuk mengubahformData
bernamaformHandler
, kode modifikasinya adalah sebagai berikut:import { defineStore } from "pinia"; export const customStore = defineStore({ id: "custom", state: () => ({ initialNumber: 10, // declare state untuk handle form formData: { // karena bisa banyak valuenya, kita bentuk dalam object value1: "Placeholder", }, }), actions: { incrementInitialNumber() { this.initialNumber += 10000; }, // method Actions untuk menghandle Form // karena sekarang di sini kita membutuhkan input // maka kita bisa menggunakan parameter di dalam fungsi // yang dibuat // sebut saja nama parameternya adalah "payload" formHandler(payload) { // di sini kita akan mengubah keseluruhan dari formData this.formData = payload; }, }, });
- Selanjutnya kita akan menggunakan state
formData.value1
pada componentFormRight.vue
. MemodifikasiFormRight.vue
sehingga menjadi seperti ini:<script> // import mapState import { mapState } from "pinia"; // import store import { customStore } from "../stores/custom"; export default { name: "FormRight", // state pada pinia = computed di component computed: { ...mapState(customStore, ["formData"]), }, }; </script> <template> <div> <!-- di sini kita akan membaca formData.value1 --> <p class="font-semibold">{{ formData.value1 }}</p> </div> </template> <style scoped></style>
- Selanjutnya kita akan menggunakan actions
formHandler
pada componentFormLeft.vue
. MemodifikasiFormLeft.vue
sehingga menjadi seperti ini:<script> // import mapActions import { mapActions } from "pinia"; // import store import { customStore } from "../stores/custom"; export default { name: "FormLeft", // karena di sini ada menggunakan form // maka sekarang kita juga membutuhkan local state data() { return { // sebut saja namanya adalah localFormData // karena kemungkinan ada banyak, kita bentuk dalam object localFormData: { input1: "", }, }; }, // gunakan actions pada methods methods: { ...mapActions(customStore, ["formHandler"]), // tambahkan local methods untuk memproses form submission localFormHandler() { // di sinilah kita akan menggunakan actions // ingat formHandler menerima sebuah "payload" bukan? // bagaimana cara kita mengirimnya? // ya, lewat invoke dan kirim via args nya ! // ingat payload menerima object primitif yah ! this.formHandler({ // ingat object nya menerima sebuah props dengan nama "value1" yah // lihat src/stores/custom.js pada state "formData" bila lupa value1: this.localFormData.input1, }); }, }, }; </script> <template> <!-- binding event localFormHandler pada form submission event --> <form action="#" v-on:submit.prevent="localFormHandler"> <div> <!-- jangan lupa binding 2 arah input ini dengan local data (v-model) --> <input type="text" placeholder="Just write me" v-model="localFormData.input1" class="shadow appearance-none border rounded py-2 px-3 text-gray-700" /> </div> <div class="mt-4"> <button type="submit" class="bg-gray-100 py-2 px-4 rounded-md hover:bg-gray-300 hover:shadow-md" > Transfer Me </button> </div> </form> </template> <style scoped></style>
Sampai pada tahap ini artinya kita sudah berhasil untuk membuat sebuah form submission yang mana akan langsung mengubah state dan perubahan dari state tersebut akan mengubah tulisan yang ada pada komponen lainnya secara langsung.
Asik kan Pinia?
Selanjutnya kita akan mulai bermain dengan tarikan data !
Pada bagian ini kita akan mencoba untuk menyelesaikan bagian pada Table
, dimana table ini akan menampilkan data dari eksternal (Jokes API), kemudian akan ditampilkan pada Tabel yang sudah kita buat sebelumnya.
Langkah-langkah pengerjaannya adalah sebagai berikut:
- Menyiapkan data provider berupa
axios instance
terlebih dahulu. Membuat folder/src/apis
dan membuat sebuah file dengan namajokes.js
- Memodifikasi kode pada
/src/apis/jokes.js
sebagai berikut:// import axios import axios from "axios"; // membuat instance axios berdasarkan api yang digunakan // https://v2.jokeapi.dev const instance = axios.create({ baseURL: "https://v2.jokeapi.dev/", // apabila membutuhkan header authorization // (bearer / token / basic) // bisa diletakkan di sini }); // jangan lupa diexport karena akan digunakan di tempat lainny export default instance;
- Membuat sebuah method untuk mengambil data dari instance yang sudah dibuat. Sebut saja nama
method
nya adalahfetchJokes
, dan karena kita sedang menggunakan pinia, makamethod
ini akan kita letakkan padaactions
yang ada pada store pinia (src/stores/custom.js
, bagian propsactions
). Namun supaya ada data yang dapat disimpan juga, maka kita juga akan membutuhkan sebuahstate
untuk menampung data kembalian dari pengambilan data dari API ini (sebut saja namanya adalah sebuahstate
dengan namajokes
). - Memodifikasi file
src/stores/custom.js
menjadi seperti berikutimport { defineStore } from "pinia"; // import instance axios di sini import jokesInstance from "../apis/jokes"; export const customStore = defineStore({ id: "custom", state: () => ({ initialNumber: 10, formData: { value1: "Placeholder", }, // declare state untuk berisi kumpulan dari jokes jokes: [], }), actions: { incrementInitialNumber() { this.initialNumber += 10000; }, formHandler(payload) { this.formData = payload; }, // method Actions untuk mengambil data dari API Jokes // karena di sini akan mengambil data, yang mana durasi // pengerjaannya tidak menentu, maka kita akan menggunakan // logicnya secara asynchronous // tambahkan kata kata async di depan actions yang dibuat async fetchJokes() { try { // kita ambil data dari jokes api const response = await jokesInstance.get( "/joke/Any?blacklistFlags=nsfw,religious,political,racist,sexist,explicit&type=twopart&amount=10" ); // apabila berhasil mendapatkan response (dunia sempurna) // maka kita akan set state jokesnya // data jokes ada pada Object punya props jokes this.jokes = response.data.jokes; } catch (err) { // error sederhana dengan console log // bila dibutuhkan bisa menggunakan sebuah state error // atau langsung memberikan toast / swal console.log(err.response); } }, }, });
- Selanjutnya kita akan menyambungkan logic yang sudah dibuat pada stores ini dengan component yang sudah dibuat, yaitu
TableContent
danTableList
- Memodifikasi component
TableList.vue
untuk memanggil fungsifetchJokes
pada saat component ini ada (created
)<script> // import mapActions import { mapActions } from "pinia"; // import stores import { customStore } from "../stores/custom"; import TableContent from "./TableContent.vue"; export default { components: { TableContent }, name: "TableList", // declare actions pada methods methods: { ...mapActions(customStore, ["fetchJokes"]), }, // panggil methods pada created created() { this.fetchJokes(); }, }; </script> <template> <table class="table-fixed w-full border-collapse border border-gray-300"> <thead> <tr class="bg-gray-100"> <th class="w-1/6 border border-gray-300">ID</th> <th class="w-1/3 border border-gray-300">Setup</th> <th class="w-1/3 border border-gray-300">Punchline</th> </tr> </thead> <table-content></table-content> </table> </template> <style scoped></style>
- Sampai pada titik ini, pada saat component
TableList
akan dirender, maka akan memanggilactions
bernamafetchJokes
dan akan mengubah statejokes
. Namunjokes
ini masih nganggur. Selanjutnya kita akan membaca statejokes
ini dan menampilkannya padaTableContent
. Ingat bahwaTableContent
ini adalah data per baris, sehingga harus dilakukan looping. - Memodifikasi file
TableList.vue
lagi untuk membaca statejokes
:<script> // import mapActions dan mapState import { mapActions, mapState } from "pinia"; import { customStore } from "../stores/custom"; import TableContent from "./TableContent.vue"; export default { components: { TableContent }, name: "TableList", methods: { ...mapActions(customStore, ["fetchJokes"]), }, // declare state pada computed computed: { ...mapState(customStore, ["jokes"]), }, created() { this.fetchJokes(); }, }; </script> <template> <table class="table-fixed w-full border-collapse border border-gray-300"> <thead> <tr class="bg-gray-100"> <th class="w-1/6 border border-gray-300">ID</th> <th class="w-1/3 border border-gray-300">Setup</th> <th class="w-1/3 border border-gray-300">Punchline</th> </tr> </thead> <!-- karena sekarang kita butuh looping --> <!-- Maka kita akan menggunkan v-for dan anggap state jokes seperti --> <!-- computed pada umumnya --> <table-content v-for="joke in jokes" v-bind:key="joke.id" v-bind:jokeYangDilempar="joke" ></table-content> </table> </template> <style scoped></style>
- Perhatikan pada kode di atas, bahwa pada akhirnya kita menggunakan cara normal pada Vue (menggunakan v-bind custom attributes), yang mana artinya sekarang kita akan memodifikasi
TableContent.vue
untuk menerima custom attributes tersebut melalui ...props
! Memodifikasi kodeTableContent.vue
sebagai berikut:<script> export default { name: "TableContent", // menerima props props: ["jokeYangDilempar"], }; </script> <template> <tbody> <tr> <!-- Kita gunakan props punya data di sini --> <td class="border border-gray-300">{{ jokeYangDilempar.id }}</td> <td class="border border-gray-300">{{ jokeYangDilempar.setup }}</td> <td class="border border-gray-300">{{ jokeYangDilempar.delivery }}</td> </tr> </tbody> </template> <style scoped></style>
- Lessons Learned: Jadi tidak pasti bahwa dengan menggunakan Pinia artinya kita benar benar 100% meninggalkan props yah ๐, hanya penggunaannya jadi lebih berkurang !
- Harus mengetahui kapan menggunakan
state
,action
, dangetter
- Ingat bahwa Pinia memiliki built in function untuk mempermudah hidup (
mapXXX
) - Untuk mengkombinasikan dengan router, bisa menggunakan
markRaw
(lihat referensi di bawah untukmarkRaw
)