Integrasi Vuejs dengan Django
Sebelum masuk ke inti tulisan, ada beberapa disclaimer terlebih dahulu
- Di sini tidak membahas struktur membuat template di django
- Menggunakan vue js versi 2 (dan axios untuk ajax call ) yang versi CDN
- Untuk kebutuhan REST saya menggunakan https://dummyapi.io/
Alasan menggunakan vue dan disambungkan dengan django agar bisa mendapatkan reaktivitas dari vue (dan axios) yang jika menggunakan jquery akan cukup “sulit”, sedangkan oleh vue menjadi cukup jauh lebih mudah. Ruang lingkup tulisan ini akan membahas:
- Memasang vue
- “Hello World”
- Membuat Halaman Post dan User
- Membuat Mixin sebagai global fungsi
Memasang Vue
Karena saya menggunakan yang versi cdn baik itu untuk vue dan axios, maka integrasi cukup mudah. Cukup tambahkan link cdn dari vue dan axios di “layout.html” atau file yang dijadikan base layout/template dari aplikasi django.
<script src="https://cdn.jsdelivr.net/npm/vue@2"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.21.1/axios.min.js" integrity="sha512-bZS47S7sPOxkjU/4Bt0zrhEtWx0y0CRkhEp8IckzK+ltifIIE9EMIMTuT/mEzoIMewUINruDBIR/jJnbguonqQ==" crossorigin="anonymous"></script>
“Hello World”
Seperti contoh programming pada umumnya semua dimulai dengan “hello world” atau pada kasus ini lebih tepat “hello vue“.
Sebelum bisa mengeluarkan “hello vue” hal yang perlu dipersiapkan adalah lokasi di mana vue js “bisa bekerja”, buka kembali “layout.html” atau file manapun yang dijadikan base layout/template dari aplikasi django. Pada bagian “content” tambahkan id “render” atau apapun namanya disesuaikan saja, id ini akan berfungsi sebagai elemen yang akan jadi “tempat kerja” vue.
<div class="container" id="render"> {% block content %} {% endblock %} </div>
Saya asumsikan kita sudah punya satu halaman yang bisa dituju, misal halaman “users” dan “posts“. Buka berkas html salah satu halaman, saya memilih halaman users, dan isikan seperti berikut
{% extends "layout.html" %} {% block content %} <div class="row"> [[ message ]] </div> {% endblock %} {% block javascript %} <script type="application/javascript"> var app = new Vue({ el: '#render', delimiters: ["[[", "]]"], data: { message: 'Hello Vue!' } }) </script> {% endblock %}
Mari bahas dulu beberapa hal, pertama di bagian vuejs ada key berupa “el” yang bernilai “#render” sama dengan id yang tadi dipasang di html. Fungsi bagian ini adalah untuk memberi tahu di mana vue js akan ditampilkan.
Lalu yang kedua ada “delimiters“, saya ubah “delimiters” vue menjadi double kurung siku terbuka ( [[ ) dan tertutup ( ]] ), fungsinya agar tidak bentrok dengan dengan django template language, karena default dari vue menggunakan double kurung kurawal ( {{ }} ) dan itu sama yang dipakai oleh django template, agar tidak bentrok maka saya harus ganti dari sisi vue.
Ketiga dibagian data ada message, biasanya ini disebut vue model, ini bisa saja saya terlalu menyederhanakan, tapi saya cukup sering menyebut ini sebagai “variabel” saja. Dan terakhir, di bagian content di sana saya melakukan print out message dengan delimiter yang sudah ditentukan [[ message ]]
Sehingga saat diakses akan menampilkan seperti berikut.
Membuat Halaman Users dan Posts
Tujuan dari halaman ini adalah:
- Halaman ini akan menampilkan daftar “user” maupun “post” yang didapat dari REST
- Halaman ini juga akan memiliki fitur pagination sederhana ( next &
previous )
Pertama-tama yang kita ubah adalah bagian “content“, ubah sebagai berikut
{% block content %} <div class="row "> <nav aria-label="Page navigation example "> <ul class="pagination"> <li class="page-item"><a class="page-link">Previous</a></li> <li class="page-item"><a class="page-link">Next</a></li> </ul> </nav> </div> <div class="row"> <div class="card mb-4 box-shadow"> <img class="card-img-top" data-src="holder.js/100px225?theme=thumb&bg=55595c&fg=eceeef&text=Thumbnail" alt="Thumbnail [100%x225]" style="height: 225px; width: 100%; display: block;" src="data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%22348%22%20height%3D%22225%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20348%20225%22%20preserveAspectRatio%3D%22none%22%3E%3Cdefs%3E%3Cstyle%20type%3D%22text%2Fcss%22%3E%23holder_177e62b95eb%20text%20%7B%20fill%3A%23eceeef%3Bfont-weight%3Abold%3Bfont-family%3AArial%2C%20Helvetica%2C%20Open%20Sans%2C%20sans-serif%2C%20monospace%3Bfont-size%3A17pt%20%7D%20%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cg%20id%3D%22holder_177e62b95eb%22%3E%3Crect%20width%3D%22348%22%20height%3D%22225%22%20fill%3D%22%2355595c%22%3E%3C%2Frect%3E%3Cg%3E%3Ctext%20x%3D%22116.68333435058594%22%20y%3D%22120.3%22%3EThumbnail%3C%2Ftext%3E%3C%2Fg%3E%3C%2Fg%3E%3C%2Fsvg%3E" data-holder-rendered="true"> <div class="card-body"> <p class="card-text">Name</p> </div> </div> </div> {% endblock %}
Sehingga ketika kita akses kembali halaman “users” akan terlihat seperti ini
Selanjutnya bagian block “javascript“, pada bagian data atau vue model, saya mempersiapkan beberapa “variabel“:
- objects yang berupa list/array untuk menampung hasil dari REST
- filter dengan isi page dan limit, ini nantinya akan jadi querystring
- previous_page untuk penanda pindah ke halaman sebelumnya
- next_page untuk penanda pindah ke halaman selanjutnya
Selain vue model yang dipersiapkan selanjutnya adalah, methods, karena ini cukup sederhana, maka saya hanya menyiapkan dua method saja:
- apiCall(): Sesuai namanya fungsi ini yang nantinya akan bertanggung jawab untuk melakukan panggilan ke REST
- switchPage(page): Cukup explisit, fungsi ini nantinya berfungsi untuk pagination
Terakhir saya meyiapkan fungsi “created()” untuk memangil apiCall(). Fungsi “created” ini sendiri berada di luar method, fungsinya dari “created” ini adalah, saat kita masuk ke halaman “users” atau manapun, maka fungsi “created” ini yang pertama kali dijalankan, jika dibandingkan dengan jquery, ini mirp document.ready().
Sehingga isi dari “user.html” menjadi seperti berikut
{% extends "layout.html" %} {% block content %} <div class="row "> <nav aria-label="Page navigation example "> <ul class="pagination"> <li class="page-item"><a class="page-link">Previous</a></li> <li class="page-item"><a class="page-link">Next</a></li> </ul> </nav> </div> <div class="row"> <div class="card mb-4 box-shadow"> <img class="card-img-top" data-src="holder.js/100px225?theme=thumb&bg=55595c&fg=eceeef&text=Thumbnail" alt="Thumbnail [100%x225]" style="height: 225px; width: 100%; display: block;" src="data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%22348%22%20height%3D%22225%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20348%20225%22%20preserveAspectRatio%3D%22none%22%3E%3Cdefs%3E%3Cstyle%20type%3D%22text%2Fcss%22%3E%23holder_177e62b95eb%20text%20%7B%20fill%3A%23eceeef%3Bfont-weight%3Abold%3Bfont-family%3AArial%2C%20Helvetica%2C%20Open%20Sans%2C%20sans-serif%2C%20monospace%3Bfont-size%3A17pt%20%7D%20%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cg%20id%3D%22holder_177e62b95eb%22%3E%3Crect%20width%3D%22348%22%20height%3D%22225%22%20fill%3D%22%2355595c%22%3E%3C%2Frect%3E%3Cg%3E%3Ctext%20x%3D%22116.68333435058594%22%20y%3D%22120.3%22%3EThumbnail%3C%2Ftext%3E%3C%2Fg%3E%3C%2Fg%3E%3C%2Fsvg%3E" data-holder-rendered="true"> <div class="card-body"> <p class="card-text">Name</p> </div> </div> {% endblock %} {% block javascript %} <script type="application/javascript"> var app = new Vue({ el: '#render', delimiters: ["[[", "]]"], data: { objects: [], message: null, filter: { page: 1, limit: 6 }, previous_page: 1, next_page: 1 }, methods: { apiCall() { }, switchPage(page) { } }, created() { this.apiCall() } }) </script> {% endblock %}
Sekarang saatnya membuat komunikasi ke REST, saya menggunakan FAKE REST dari https://dummyapi.io/. Fungsinya menggunakan method GET di mana dia menggunakan query string untuk kebutuhan page dan limit, page dan limit sendiri sudah disiapkan di vue model filter, untuk mengubahnya menjadi query string saya menggunakan script berikut
apiCall() { filter = this.filter qs = Object.keys(filter).map(key => key + '=' + filter[key]).join('&') } ## yang dihasilkan page=1&limit=6
Pada contoh kasus yang sedikit berbeda, misal selain page dan limit kita ingin juga mengrimkan query pencarian, sehingga di vue model filter menjadi seperti berikut
filter: { q: null, page: 1, limit: 6 }
agar tetap rapi dan hanya query string yang bernilai saja, saya menambahkan script untuk membersihkan dahulu object yang bernilai null atau kosong.
apiCall() { filter = this.filter Object.keys(filter).forEach(k => (filter[k] && typeof filter[k] === 'object') && cleanObj(filter[k]) || (!filter[k] && filter[k] !== undefined) && delete filter[k] ); qs = Object.keys(filter).map(key => key + '=' + filter[key]).join('&') } ## jika pada filter q memiliki nilai hasilnya jadi seperti berikut page=1&limit=6&q=string ## jika q bernilai kosong maka seperti berikut page=1&limit=6
Sekarang tambahkan fungsi axios.get untuk melakukan komunikasi ke server REST
apiCall() { local = this filter = this.filter Object.keys(filter).forEach(k => (filter[k] && typeof filter[k] === 'object') && cleanObj(filter[k]) || (!filter[k] && filter[k] !== undefined) && delete filter[k] ); qs = Object.keys(filter).map(key => key + '=' + filter[key]).join('&') url = "https://dummyapi.io/data/api/user" + "?" + qs axios.get(url, { headers: { 'app-id': "app-id" } }).then(function (response) { res = response.data local.objects = res.data local.previous_page = parseInt(local.filter["page"]) - 1 local.next_page = parseInt(local.filter["page"]) + 1 }).catch(function (error) { local.message = 'Something went wrong!' }) }
Mari bahas satu-satu
local = this
: Berfungsi nanti untuk update vue model, fungsi “this” di dalam response axios tidak binding ke vue model, jadi perlu di-assign ke variabel dulu.
url = "https://dummyapi.io/data/api/user" + "?" + qs
: Cukup jelas di sini, fungsi di sini untuk membentuk url yang dibutuhkan
axios.get(url, {headers: {'app-id': "app-id"}}) .then(function (response) { res = response.data local.objects = res.data local.previous_page = parseInt(local.filter["page"]) - 1 local.next_page = parseInt(local.filter["page"]) + 1 }).catch(function (error) { local.message = 'Something went wrong!' })
Fungsi di atas adalah fungsi axios.get, beberapa yang bisa diperhatikan adalah:
{headers: {'app-id': "app-id"}}
: headers untuk kebutuhan dari server rest, bisa diisi seperti token, atau variabel-variabel yang dibutuhkan headers dari komunikasi ajax.
local.objects = res.data
: Hasil data dari ajax call akan di-assign ke vue-model objects
local.previous_page = parseInt(local.filter["page"]) - 1
: Membuat prev page merupakan page sekarang dikurang -1
local.next_page = parseInt(local.filter["page"]) + 1
: Membuat next page merupakan page sekarang ditambah 1
Untuk informasi axios bisa dibaca di dokumentasi axiosnya.
Sekarang untuk html agar bisa menampilkan sesuai yang diinginkan ubah htmlnya menjadi seperti berikut
<div class="row "> <nav aria-label="Page navigation example "> <ul class="pagination"> <li class="page-item"><a class="page-link" @click="switchPage(previous_page)">Previous</a></li> <li class="page-item"><a class="page-link" @click="switchPage(next_page)">Next</a></li> </ul> </nav> </div> <div class="row"> <div class="col-md-4" v-for="obj in objects" :key="obj.id"> <div class="card mb-4 box-shadow"> <img v-bind:src="obj.picture" styles="height: 100px;width: 50%;" alt="Card image cap"> <div class="card-body"> <p class="card-text">[[ obj.firstName ]]</p> </div> </div> </div> </div>
Yang perlu diperhatikan di atas adalah:
@click="switchPage(previous_page)" & @click="switchPage(previous_page)"
: Fungsi @click di sini mirip dengan on click pada jquery, pada contoh ini, saat tombol diklik dia akan menjalankan fungsi switchPage() dengan parameter next_page atau previous_page, oleh karena itu fungsi switchPage() cukup diisi seperti berikut
switchPage(page) { if (page > 0 ) { this.filter['page'] = page this.apiCall() } }
v-for="obj in objects" :key="obj.id"
: Ini berfungsi untuk melakukan looping berdasarkan vue model objects
v-bind:src="obj.picture"
: ini untuk menampilkan gambar dari response ke tag html, v-bind ini juga bisa digunakan untuk href misalnya, tidak selalu src
<p class="card-text">[[ obj.firstName ]]</p>
: Ini mirip dengan “hello vue” tadi, sebagai printout.
Oh iya “picture” dan “firstName” di sana itu adalah hasil dari REST jadi sesuaikan saja dengan response REST yang didapat, kebetulan response dari REST dummy data ini seperti berikut
{ "data": [ { "id": "5aZRSdkcBOM6j3lkWEoP", "picture": "https://randomuser.me/api/portraits/women/50.jpg", "email": "[email protected]", "lastName": "Lampinen", "firstName": "Lilja", "title": "ms" }, { "id": "5tVxgsqPCjv2Ul5Rc7gw", "email": "[email protected]", "lastName": "Liu", "title": "miss", "picture": "https://randomuser.me/api/portraits/women/83.jpg", "firstName": "Abigail" }, { "id": "6wy6UNkZueJfIUfq88d5", "picture": "https://randomuser.me/api/portraits/women/32.jpg", "firstName": "Melanie", "email": "[email protected]", "title": "miss", "lastName": "Pilz" }, { "id": "7DbXNPWlNDR4QYVvFZjr", "email": "[email protected]", "firstName": "Evan", "picture": "https://randomuser.me/api/portraits/men/80.jpg", "lastName": "Carlson", "title": "mr" }, { "id": "8RQd4OVqvmV0I4UlWETQ", "email": "[email protected]", "title": "ms", "firstName": "Kitty", "picture": "https://randomuser.me/api/portraits/women/78.jpg", "lastName": "Steward" }, { "id": "8UfTdB7ctWt3Fl87d88Q", "firstName": "Vanessa", "picture": "https://randomuser.me/api/portraits/women/33.jpg", "email": "[email protected]", "lastName": "Ramos", "title": "ms" } ], "total": 100, "page": 1, "limit": 6, "offset": 6 }
Dan ini hasil akhir dari “user.html”
{% extends "layout.html" %} {% block content %} <div class="row "> <nav aria-label="Page navigation example "> <ul class="pagination"> <li class="page-item"><a class="page-link" @click="switchPage(previous_page)">Previous</a></li> <li class="page-item"><a class="page-link" @click="switchPage(next_page)">Next</a></li> </ul> </nav> </div> <div class="row"> <div class="col-md-4" v-for="obj in objects" :key="obj.id"> <div class="card mb-4 box-shadow"> <img v-bind:src="obj.picture" styles="height: 100px;width: 50%;" alt="Card image cap"> <div class="card-body"> <p class="card-text">[[ obj.firstName ]]</p> </div> </div> </div> </div> {% endblock %} {% block javascript %} <script type="application/javascript"> var app = new Vue({ el: '#render', delimiters: ["[[", "]]"], data: { objects: [], message: null, filter: { page: 1, limit: 6 }, previous_page: 1, next_page: 1 }, methods: { apiCall() { local = this filter = this.filter Object.keys(filter).forEach(k => (filter[k] && typeof filter[k] === 'object') && cleanObj(filter[k]) || (!filter[k] && filter[k] !== undefined) && delete filter[k] ); qs = Object.keys(filter).map(key => key + '=' + filter[key]).join('&') url = "https://dummyapi.io/data/api/user" + "?" + qs axios.get(url, { headers: { 'app-id': "603a70d3dc3a34dd34abad18" } }).then(function (response) { res = response.data local.objects = res.data local.previous_page = parseInt(local.filter["page"]) - 1 local.next_page = parseInt(local.filter["page"]) + 1 }).catch(function (error) { local.message = 'Something went wrong!' }) }, switchPage(page) { if (page > 0 ) { this.filter['page'] = page this.apiCall() } } }, created() { this.apiCall() } }) </script> {% endblock %}
Untuk halaman”Post” kita bisa cukup copas dan ubah beberapa agar sesuai,
{% extends "layout.html" %} {% block content %} <div class="row "> <nav aria-label="Page navigation example "> <ul class="pagination"> <li class="page-item"><a class="page-link" @click="switchPage(previous_page)">Previous</a></li> <li class="page-item"><a class="page-link" @click="switchPage(next_page)">Next</a></li> </ul> </nav> </div> <div class="row"> <div class="col-md-4" v-for="obj in objects" :key="obj.id"> <div class="card mb-4 box-shadow"> <img class="card-img-top" v-bind:src="obj.image"> <div class="card-body"> <p class="card-text">[[ obj.text ]]</p> <div class="d-flex justify-content-between align-items-center"> <small class="text-muted">[[ obj.likes ]] Likes</small> </div> </div> </div> </div> </div> {% endblock %} {% block javascript %} <script type="application/javascript"> var app = new Vue({ el: '#render', delimiters: ["[[", "]]"], data: { objects: [], message: null, filter: { page: 1, limit: 6 }, previous_page: 1, next_page: 1 }, methods: { apiCall() { local = this filter = this.filter Object.keys(filter).forEach(k => (filter[k] && typeof filter[k] === 'object') && cleanObj(filter[k]) || (!filter[k] && filter[k] !== undefined) && delete filter[k] ); qs = Object.keys(filter).map(key => key + '=' + filter[key]).join('&') url = "https://dummyapi.io/data/api/post" + "?" + qs axios.get(url, { headers: { 'app-id': "603a70d3dc3a34dd34abad18" } }).then(function (response) { res = response.data local.objects = res.data local.previous_page = parseInt(local.filter["page"]) - 1 local.next_page = parseInt(local.filter["page"]) + 1 }).catch(function (error) { local.message = 'Something went wrong!' }) }, switchPage(page) { if (page > 0 ) { this.filter['page'] = page this.apiCall() } } }, created() { this.apiCall() } }) </script> {% endblock %}
Di atas adalah isi dari “post.html” isinya sama, hampir identik, yang membedakan cuma
<img class="card-img-top" v-bind:src="obj.image">
: Respon yang di user “picture” di sini “image”
<small class="text-muted">[[ obj.likes ]] Likes</small>
: Menampilkan informasi jumlah like
url = "https://dummyapi.io/data/api/post" + "?" + qs
: Url yang dituju untuk
Hasilnya
Membuat Mixin Sebagai Global Fungsi
Secara tujuan, kode di atas sudah berfungsi dengan baik, tapi secara kode dan struktur kurang baik karena kita terlalu banyak menduplikasi kode di banyak file, iya contoh ini cuma dua, kalau di pekerjaan sebenarnya yang banyak file, tentu akan kesulitan kalau misal fungsi apiCall() mau kita ubah dan harus mengubah banyak file. Oleh karena itu kita perlu mixin untuk bisa membuat reusable function.
Kalau diperhatikan pada kode di atas yang berbeda cuma 3 poin struktur html, respon dari REST, dan url yang dituju, selain itu sama semua, maka dari itu mari buat mixin.
Pertama buat file “mixin.js” atau apapun namanya di folder static untuk projek django, lalu tambahkan di layout di bawah script vue js.
<script src="{% static 'js/mixin.js'%}"></script>
Pada berkas mixin kita duplikasi script vue js yang berada di halaman user atau post, terutama bagian data, methods dan create
var mixinList = { data: { objects: [], message: null, filter: { page: 1, limit: 6 }, previous_page: 1, next_page: 1 }, methods: { apiCall() { local = this filter = this.filter Object.keys(filter).forEach(k => (filter[k] && typeof filter[k] === 'object') && cleanObj(filter[k]) || (!filter[k] && filter[k] !== undefined) && delete filter[k] ); qs = Object.keys(filter).map(key => key + '=' + filter[key]).join('&') url = "https://dummyapi.io/data/api/post" + "?" + qs axios.get(url, { headers: { 'app-id': "app-id" } }).then(function (response) { res = response.data local.objects = res.data local.previous_page = parseInt(local.filter["page"]) - 1 local.next_page = parseInt(local.filter["page"]) + 1 }).catch(function (error) { local.message = 'Something went wrong!' }) }, switchPage(page) { if (page > 0 ) { this.filter['page'] = page this.apiCall() } } }, created() { this.apiCall() } }
Karena ini akan dijadikan sebagai global fungsi maka kita perlu ubah url yang hardcoded agar bisa dinamis sesuai yang dibutuhkan, dan untuk merapihkan saya pindahkan juga headers ke bagian vue model, sehingga hasil akhirnya menjadi seperti berikut
var mixinList = { data: { objects: [], message: null, url: null, filter: { page: 1, limit: 6 }, previous_page: 1, next_page: 1, headers: { headers: { 'app-id': "app-id" } } }, methods: { apiCall() { local = this filter = this.filter Object.keys(filter).forEach(k => (filter[k] && typeof filter[k] === 'object') && cleanObj(filter[k]) || (!filter[k] && filter[k] !== undefined) && delete filter[k] ); qs = Object.keys(filter).map(key => key + '=' + filter[key]).join('&') url = this.url + "?" + qs axios.get(url, this.headers).then(function (response) { res = response.data local.objects = res.data local.previous_page = parseInt(local.filter["page"]) - 1 local.next_page = parseInt(local.filter["page"]) + 1 }).catch(function (error) { local.message = 'Something went wrong!' }) }, switchPage(page) { if (page > 0 ) { this.filter['page'] = page this.apiCall() } } }, created() { this.apiCall() } }
Sekarang buka kembali “post.html” dan “user.html” lalu ubah seperti berikut pada bagian block javascript
“post.html”
{% block javascript %} <script type="application/javascript"> var app = new Vue({ mixins: [mixinList], el: '#render', delimiters: ["[[", "]]"], data: { url: "https://dummyapi.io/data/api/post" } }) </script> {% endblock %}
“user.html”
{% block javascript %} <script type="application/javascript"> var app = new Vue({ mixins: [mixinList], el: '#render', delimiters: ["[[", "]]"], data: { url: "https://dummyapi.io/data/api/user" } }) </script> {% endblock %}
Perhatikan ada key baru bernama “mixins“, di situ saya mendaftarkan “mixinList” yang dibuat di “mixin.js“, lalu saya cukup menambahkan vue-model url sesuai dengan url yang dibutuhkan di masing-masing halaman, hasilnya
Halaman “users”
Halaman “post”
Kalau misal ingin ada perilaku unik di salah satu halaman, misal di halaman user saat apiCall() disediakan dulu loading, maka kita bisa overide fungsi apiCall() di dalam halaman yang dibutuhkan saja, seperti
{% block javascript %} <script type="application/javascript"> var app = new Vue({ mixins: [mixinList], el: '#render', delimiters: ["[[", "]]"], data: { url: "https://dummyapi.io/data/api/user" }, methods: { apiCall() { alert('loading') } }, }) </script> {% endblock %}
Hasilnya
Selamat mencoba.
Referensi: https://vuejs.org/v2/guide/mixins.html