Membuat Chained Combobox di Laravel Menggunakan VueJS


Ada banyak tutorial yang membahas pembuatan chained combobox/selectbox di PHP. Tutorialnya pun beragam, mulai dari mengunakan plain PHP dan plain JavaScript, ada juga yang menggunakan framework PHP yang dikombinasikan dengan librari JavaScript, seperti jQuery misalnya. 

Sebagai salah satu librari JavaScript paling populer, tentu saja pembuatan chained combobx dengan librari jQuery sudah banyak dijelaskan. Namun, berbeda dengan VueJS. Masih sedikit pemrogram yang berbagi ilmu seputar penggunaan chained combobox di VueJS. Walau fitur dan logikanya sama, namun implementasinya di skrip jauh berbeda.

Oh ya, bagi yang masih bingung apa itu chained combobox, contoh langsungnya dapat dilihat di gambar.

Chained combobox

Chained combobox

Secara harfiah, chained combobox bermakna combobox (pilihan) yang saling terkait atau berantai. Contoh paling sering digunakan adalah pemilihan provinsi pada form. Di mana, ketika provinsi dipilih, maka akan menampilkan kota/kabupaten yang ada dalam provinsi tersebut. Hal yang sama juga terjadi ketika kita memilih kota/kabupaten tertentu, maka akan menampilkan daftar kecamatan pada kota/kabupaten terpilih.

Walau implementasinya bisa bermacam-macam, namun di tulisan ini kita akan membuat fitur yang serupa dengan diatas, yaitu combobox berantai antara provinsi, kota/kabupaten, dan kecamatan.

Mempersiapkan Pangkalan Data (Database)

Struktur dan data yang ada pada pangkalan data tidak kita buat dari awal, namun memanfaatkan data dari repositori Edward Samuel Pasaribu. Kebetulan juga, sistem penamannya sudah sesuai dengan aturan Laravel, sehingga kita tidak perlu mengubah struktur data atau menambahkan properti baru pada model.

Unduh berkas indonesia.sql, kemudian buat pangkalan data baru di MySQL, dan impor berkas SQL tersebut ke dalam pangkalan data yang sudah dibuat sebelumnya. Maka, pangkalan data sudah siap digunakan dalam aplikasi.

Ada empat tabel dalam pangkalan data tersebut, ialah provinces, regencies, districts, dan villages. Khusus untuk villages tidak digunakan dalam aplikasi ini.

Struktur tabel tersebut masing-masing dapat dilihat pada media di bawah.

$ SHOW COLUMNS FROM provinces;
+-------+--------------+------+-----+---------+-------+
| Field | Type         | Null | Key | Default | Extra |
+-------+--------------+------+-----+---------+-------+
| id    | char(2)      | NO   | PRI | NULL    |       |
| name  | varchar(255) | NO   |     | NULL    |       |
+-------+--------------+------+-----+---------+-------+
$ SHOW COLUMNS FROM regencies;
+-------------+--------------+------+-----+---------+-------+
| Field       | Type         | Null | Key | Default | Extra |
+-------------+--------------+------+-----+---------+-------+
| id          | char(4)      | NO   | PRI | NULL    |       |
| province_id | char(2)      | NO   | MUL | NULL    |       |
| name        | varchar(255) | NO   |     | NULL    |       |
+-------------+--------------+------+-----+---------+-------+
$ SHOW COLUMNS FROM districts;
+------------+--------------+------+-----+---------+-------+
| Field      | Type         | Null | Key | Default | Extra |
+------------+--------------+------+-----+---------+-------+
| id         | char(7)      | NO   | PRI | NULL    |       |
| regency_id | char(4)      | NO   | MUL | NULL    |       |
| name       | varchar(255) | NO   |     | NULL    |       |
+------------+--------------+------+-----+---------+-------+

Instalasi Laravel dan Laravel Mix

Instal Laravel menggunakan Composer dengan perintah di bawah. Jangan lupa sesuaikan konfigurasi pangkalan data pada berkas .env.

$ composer create-project laravel/laravel chained-combobox -vvv

Direktori baru dengan nama chained-combobox berisi framework berhasil dibuat. Masuk ke dalam direktori tersebut dan instal Laravel Mix beserta dependensinya.

$ npm install

// atau menggunakan Yarn
$ yarn install

Laravel Mix berfungsi untuk mengkompilasi asset yang kita butuhkan nantinya, mulai dari LESS, SASS/SCSS, JavaScript, sampai dengan komponen VueJS dengan ekstensi berkas .vue.

Dari direktori yang sama, jalankan perintah di bawah untuk mengkompilasi asset bawaan Laravel dan mengkompilasi ulang asset dalam direktori resources/assets jika ada perubahan konten.

$ npm run watch-poll

Membuat Model

Buatlah tiga buah model yang mewakili ketiga tabel di atas. Untuk mempercepat, kita bisa menggunakan Artisan console untuk meng-generate-nya.

$ php artisan make:model Location/Province
$ php artisan make:model Location/Regency
$ php artisan make:model Location/District

Tambahkan properti $fillable pada masing-masing model yang berisi kolom pada tabel.

Sebagai informasi, kita tidak perlu menambahkan relasi dalam model tersebut untuk membuat combobox berantai. Walau, pada akhirnya, relasi Eloquent akan digunakan pada fitur lain dalam aplikasi.

Membuat Controller dan View

Buat contoller baru dengan nama LocationController.

$ php artisan make:controller LocationController

Dalam controller, terdpat lima buah method dengan berbagai fungsi.

<?php

namespace App\Http\Controllers;

use App\Location\District;
use App\Location\Province;
use App\Location\Regency;
use Illuminate\Http\Request;
use Illuminate\View\View;
use Illuminate\Http\JsonResponse;

class LocationController extends Controller
{

    /**
     * Get all province
     *
     * @return string JSON
     */
    public function province(): JsonResponse
    {
        return response()->json(Province::orderBy('name', 'ASC')->get());
    }

    /**
     * Get cities based on selected province
     *
     * @return string JSON
     */
    public function regency(): JsonResponse
    {
        $regencies = Regency::whereProvinceId(request('province'))
            ->orderBy('name', 'ASC')
            ->get();

        return response()->json($regencies);
    }

    public function district(): JsonResponse
    {
        $districts = District::whereRegencyId(request('regency'))
            ->orderBy('name', 'ASC')
            ->get();

        return response()->json($districts);
    }

    public function form(): View
    {
        return view('location.form')
            ->withTitle('Lokasi');
    }

    public function submit(Request $request)
    {
        $this->validate($request, [
            'province' => 'required|integer|exists:provinces,id',
            'regency' => 'required|integer|exists:regencies,id',
            'district' => 'required|integer|exists:districts,id',
        ]);

        // do something here

        return response()->json([
            'status' => true,
            'message' => 'Semua data valid.',
        ]);
    }
}

Mulai dari method form() misalnya, method ini hanya berfungsi untuk memuat view Blade, kemudian menampilkannya di peramban (browser). Sedangkan method submit() berfungsi untuk memvalidasi dan memroses data form yang dikirim.

Berturut-turut method province(), regency(), dan district() berfungsi untuk menampilkan data dari pangkalan data dan mengembalikannya dalam format JSON.

Perhatikan pada method regency() dan district(), di mana kita menambahkan filter pada query. Semisal, untuk mencari data regency (kota/kabupaten), kita membutuhkan id dari province. Begitu juga dengan query district (kecamatan) yang membutuhkan id dari regency.

Khusus untuk data province (provinsi), tidak kita sertakan sebagai variabel dalam method form(), tapi dimuat dengan AJAX pada komponen VueJS nantinya.

Jangan lupa mendefinisikan controller tersebut pada route agar dapat diakses.

Route::get('/location', 'LocationController@form');
Route::post('/location', 'LocationController@submit');

// ajax request only
Route::get('/location/province', 'LocationController@province');
Route::get('/location/regency', 'LocationController@regency');
Route::get('/location/district', 'LocationController@district');

Dari controller, berpindah ke view.

Pada aplikasi kita bisa memanfaatkan layout template yang disediakan Laravel dengan menjalankan perintah php artisan make:auth. Sebenarnya, perintah tersebut tidak spesifik membuat layout, tapi lebih kepada pembuatan fitur autentikasi di Laravel. Nah, dari penggunaan autentikasi ini, maka layout juga otomatis disertakan dalam aplikasi.

Dengan menggunakan layout bawaan, buat berkas dengan nama form.blade.php dalam direktori resources/views/location.

@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            <div class="panel panel-default">
                <div class="panel-heading">{{ $title }}</div>

                <div class="panel-body">
                    <location-form></location-form>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

Dalam view di atas, kita menggunakan tag <location-form>. Tag spesial ini pada dasarnya merupakan komponen VueJS yang akan kita buat pada berkas terpisah dan didaftarkan pada aplikasi.

Membuat Form VueJS

Langkah selanjutnya, membuat komponen VueJS (yang secara umum) dapat digunakan di mana saja dalam aplikasi, selama dalam rentang instance VueJS tentunya.

Buat berkas baru dengan nama Form.vue dalam direktori resources/assets/js/components/location.

<template>
  <form @submit.prevent="submit" method="post" action="/location">

    <div v-if="alert.message" :class="['alert alert-' + alert.type]">
      <p>{{ alert.message }}</p>
    </div>

    <div :class="['form-group', { 'has-error' : errors.province }]">
      <label class="control-label">Provinsi</label>
      <select @change="province" v-model="state.province" class="form-control">
        <option value="">Pilih Provinsi</option>
        <option v-for="province in provinces" :value="province.id">
          {{ province.name }}
        </option>
      </select>
      <span v-if="errors.province" class="label label-danger">
        {{ errors.province[0] }}
      </span>
    </div>

    <div :class="['form-group', { 'has-error' : errors.regency }]">
      <label class="control-label">Kota/kabupaten</label>
      <select @change="regency" v-model="state.regency" class="form-control">
        <option value="">Pilih Kota/Kabupaten</option>
        <option v-for="regency in regencies" :value="regency.id">
          {{ regency.name }}
        </option>
      </select>
      <span v-if="errors.regency" class="label label-danger">
        {{ errors.regency[0] }}
      </span>
    </div>

    <div :class="['form-group', { 'has-error' : errors.district }]">
      <label class="control-label">Kecamatan</label>
      <select v-model="state.district" class="form-control">
        <option value="">Pilih Kecamatan</option>
        <option v-for="district in districts" :value="district.id">
          {{ district.name }}
        </option>
      </select>
      <span v-if="errors.district" class="label label-danger">
        {{ errors.district[0] }}
      </span>
    </div>

    <div class="form-group">
      <button class="btn btn-primary">Kirim</button>
    </div>
  </form>
</template>

<script>
  export default {
    name: 'LocationForm',

    data() {
      return {
        alert: {},
        errors: [],
        state: {
          province: '',
          regency: '',
          district: ''
        },
        provinces: [],
        regencies: [],
        districts: []
      }
    },

    mounted() {
      // get all provinces data
      axios.get('/location/province').then(response => {
        this.provinces = response.data;
      }).catch(error => console.error(error));
    },

    methods: {
      submit(e) {
        this.errors = [];
        this.alert = {};

        axios.post(e.target.action, this.state).then(response => {
          if (response.data.status) {
            this.alert = {
              type: 'success',
              message: response.data.message
            }

            this.errors = [];
          }
        }).catch(error => {
          if (error) {
            if (error.response.status == 422) {
              this.errors = error.response.data;
            }
          }
        });
      },

      province() {
        this.state.regency = '';

        // set params
        const params = {
          province: this.state.province
        }

        // url /location/regency?province=xxx
        axios.get('/location/regency', {params}).then(response => {
          this.regencies = response.data;
        }).catch(error => console.error(error));
      },

      regency() {
        this.state.district = '';

        // set params
        const params = {
          regency: this.state.regency
        };

        axios.get('/location/district', {params}).then(response => {
          this.districts = response.data;
        }).catch(error => console.error(error));
      }
    }
  }
</script>

Bagi yang belum begitu paham VueJS, mari kita bahas beberapa sintaks dan alur aplikasi.

Dimulai dari tag <form> yang berisi event @submit.prevent yang berfungsi memanggil method submit(). Method tersebut berfungsi untuk mengirim data dengan verb POST ke URI /location. Dalam controller LocationController juga terdapat method submit() yang berfungsi memroses data tersebut. Pertama, data pada form akan divalidasi, jika validasi gagal maka akan mengembalikan respon kolom yang tidak valid dengan status 422, selain itu maka akan mengambalikan respon sukses dengan status 200.

Khusus untuk AJAX Form ini, sudah saya bahas dalam tulisan lain beberapa waktu lalu.

Dalam method mounted() terdapat proses AJAX untuk mengambil semua data provinsi dan menampungnya ke dalam data this.provinces. Data ini ditampilkan dengan cara di-looping menggunakan sintaks v-forpada element <option> dalam tag <select>.

<div :class="['form-group', { 'has-error' : errors.province }]">
  <label class="control-label">Provinsi</label>
  <select @change="province" v-model="state.province" class="form-control">
    <option value="">Pilih Provinsi</option>
    <option v-for="province in provinces" :value="province.id">
      {{ province.name }}
    </option>
  </select>
  <span v-if="errors.province" class="label label-danger">
    {{ errors.province[0] }}
  </span>
</div>

Perhatikan pada tag <select> di atas. Kita menambahkan event @change di dalamnya. Event ini berfungsi untuk memicu pemanggilan method province() apabila datanya diubah (memilih provinsi). Dalam method province sendiri, terdapat proses untuk mengambil data regency (kota/kabutapen) dan menampungnya dalam data this.regencies, kemudian data regency di-looping pada element <select> regency.

Begitu seterusnya untuk mendapatkan data district, kita perlu memilih data regency yang akan memicu pemanggilan method regency() dan mengambil data district berdasarkan paramater regency.

Daftarkan berkas tersebut sebagai komponen VueJS pada resoures/assets/js/app.js dengen menambahkan potongan skrip di bawah.

Vue.component('location-form', require('./components/location/Form.vue'));

Kompilasi dan Ujicoba

Langkah terakhir, sebelum menggunakan komponen di atas, kita perlu mengkompilasinya menggunakan Laravel Mix. Jalankan perintah di bawah untuk mengkompilasi ulang assets.

Apabila ada perintah npm run watch-poll, matikan dahulu proses tersebut dari terminal. Di terminal, kita bisa menggunakan kombinasi keyboard CTRL + C untuk menghentikan proses.

$ npm run dev

Atau, jika untuk environment production, bisa menggunakan perintah di bawah.

$ php artisan production

Pastikan tidak ada sintaks yang keliru sampai kompilasi tidak menampilkan kesalahan.

Untuk mengujinya melalui peramban (browser), kalian bisa menjalankan built-in web server dengan perintah php artisan serve, kemudian mengaksesnya dengan URL http://localhost:8000.

Demo Aplikasi

Demo aplikasi langsung bisa kalian coba pada tautan berikut. Oh hei, kalian juga bisa melihat dan menggunakan skripnya dengan meng-clone repositori berikut.

Masih ada satu fitur yang kurang sebenarnya, yaitu menampilkan villages (desa) berdasarkan district (kecamatan). Bisakah kalian melanjutkannya? 😀

Selamat berkarya pemrogram! 😉

Tak Berkategori

Yugo Purwanto

Pemrogram PHP dan JavaScript yang sedang sibuk mengembangkan aplikasi Glosarium Bahasa Indonesia.

3 comments

Tinggalkan Balasan