Bagaimana Membuat Dynamic Correlation View pada Vue JS 2 untuk Optimasi UX

Menampilkan data dalam tabel dengan kolom-kolom informasi yang saling berkorelasi membutuhkan penanganan khusus, terutama untuk analisis dan pengambilan keputusan yang cepat. 

Jika hanya mengandalkan tabel konvensional bawaan framework seperti Bootstrap atau lainnya, informasi yang ditampilkan bisa terasa kurang optimal dari segi user experience. 

Oleh karena itu, diperlukan pendekatan yang lebih fleksibel agar data dapat lebih mudah dipahami dan diolah sesuai kebutuhan pengguna.

Salah satu solusi untuk permasalahan tersebut adalah menerapkan correlation-view, atau secara sederhana, menyusun tabel yang dikelompokkan berdasarkan opsi pengelompokan yang diinginkan, dengan setiap level grup saling berkorelasi.

Untuk lebih mudah memahami, pada use case kali ini, kita akan mengelompokkan data mobil berdasarkan empat opsi, yaitu Merk, Negara Asal, Tahun Berdiri, dan Tipe Kendaraan

Dengan pengelompokan ini, data dapat lebih mudah dianalisis dan dibandingkan sesuai dengan kebutuhan pengguna.

Sebagai contoh, berikut adalah format data yang akan kita gunakan:

[
    {
    	"no": 1,
    	"merk": "BMW",
    	"negara_asal": "Jerman",
    	"tipe_kendaraan": "Mobil Mewah & Sport",
    	"tahun_berdiri": 1916
    }
]

Sebelum membuat code atau implementasi teknisnya, penting untuk memahami apa hasil akhir yang ingin kita capai. 

Berikut adalah gambaran UI yang akan kita coba terapkan dari pendekatan correlation-view, data ditampilkan dengan struktur yang lebih jelas dan terorganisir sesuai dengan kelompoknya.

Untuk membangun tampilan, tech stack yang akan kita gunakan kali ini adalah Vue JS 2 dan juga Bootstrap, selanjutnya kita perlu menyusun struktur data yang fleksibel dan menentukan bagaimana data tersebut akan dikelompokkan di dalam komponen.

Yang perlu dilakukan pertama adalah membuat fungsi untuk mendapatkan label pengelompokan:

  1. Label akan ditampilkan pada row sesuai dengan urutan opsi grouping yang dipilih.
  2. Fungsi tersebut menerima 2 argument berupa item dan kemudian adalah groupKey.
  3. GroupKey akan menjadi referensi bahwa value yang akan di ambil dari item dan dijadikan sebagai label adalah value yang ada pada key yang sama dengan groupKey. berikut ini adalah code fungsinya:
getGroupLabel(item, groupKey) {
            switch (groupKey) {
                case 'tahun_berdiri':
                    return item.tahun_berdiri ?? 'Unknown';
                case 'merk':
                    return item.merk ?? 'Unknown';
                case 'negara_asal':
                    return item.negara_asal ?? 'Unknown';
                case 'tipe_kendaraan':
                    return item.tipe_kendaraan ?? 'Unknown';
                default:
                    return 'Unknown';
            }}

Setelah berhasil mengekstrak label dari setiap item, langkah berikutnya adalah mengelompokkan data berdasarkan opsi grouping yang dipilih. 

Berikutnya, kita akan menggunakan metode reduce() dari JavaScript, yang memudahkan penyusunan data dalam format object {} agar lebih terstruktur. 

Proses grouping berdasarkan label, ini akan memanfaatkan fungsi getGroupLabel yang telah kita buat sebelumnya untuk menentukan key pengelompokan. Berikut adalah implementasinya:

groupItemsByKey(items, groupKey) {
            return items.reduce((groups, item) => {
                const key = this.getGroupLabel(item, groupKey);
                if (!groups[key]) groups[key] = [];
                groups[key].push(item);
                return groups;
            }, {});
        },

Berikut ini adalah contoh output dari fungsi tersebut berdasarkan dengan 2 opsi grouping yang yang kita pilih:

Setelah berhasil mengelompokkan data berdasarkan key atau label, langkah berikutnya adalah membangun struktur data yang tidak hanya mengelompokkan item, tetapi juga memungkinkan hubungan antara parent dan sub-group dengan level hierarki yang jelas.

Pada tahap ini, kita akan membuat fungsi groupBy, yang bertujuan untuk:

  1. Mengelompokkan data secara bertingkat berdasarkan opsi grouping yang dipilih.
  2. Menentukan level hierarki, dimulai dari level 0 sebagai parent, lalu meningkat secara dinamis sesuai jumlah opsi grouping.
  3. Menyusun sub-group jika masih ada opsi grouping yang tersisa seacara rekursif.
  4. Menyimpan properti tambahan seperti _showDetails untuk kontrol UI dan timestamp dari data pertama dalam grup.

Fungsi ini memanfaatkan groupItemsByKey yang telah kita buat sebelumnya untuk membentuk struktur data yang lebih terorganisir. Berikut implementasinya:

groupBy(items, groupings, level = 0) {
           if (!items?.length || !groupings?.length) return [];
           const currentGrouping = groupings[0];
           const groups = this.groupItemsByKey(items, currentGrouping);

           return Object.entries(groups).map(([key, groupItems]) => ({
               key,
               level,
               groupType: currentGrouping,
               items: groupings?.length === 1 ? groupItems : [],
               subGroups: groupings?.length > 1
                   ? this.groupBy(groupItems, groupings.slice(1), level + 1)
                   : [],
               _showDetails: false,
           }));
       }

Setelah berhasil membuat fungsi untuk grouping dan membentuk struktur data hierarkis, langkah selanjutnya adalah menyusun struktur tampilan menggunakan Vue.js 2.

Kita akan memulai dengan membuat layout untuk parent component, yang berfungsi sebagai wadah utama untuk menampilkan data serta memanggil child component. 

Dalam hal ini, child component yang akan kita buat adalah tabel dengan tampilan hierarkis sesuai dengan struktur data yang sudah dihasilkan.

Berikut adalah implementasi layout untuk parent component:

<template>
    <div class="wrapper p-5">
        <div class="row w-full d-flex justify-content-end">
            <!-- dropdown untuk ngeluarin opsi grouping apa aja yang bisa dipilih -->
            <b-dropdown variant="transparent" right>
                <template #button-content>
                    <div class="d-flex align-items-center">
                        <i class="fas fa-archive mr-1"></i>
                        <b>Group</b>
                        <!-- buat nampilin udah berapa opsi grouping yang dipilih -->
                        <span class="badge text-primary bg-light font-weight-semibold ml-2 px-2">
                            {{ selectedGrouping?.length ? selectedGrouping.length : '' }}
                        </span>
                    </div>
                </template>
                <b-dropdown-form class="p-3" style="width: 200px; border-radius: 6px;">
                    <div class="mb-2">
                        <!-- tombol buat ngelepas semua grouping yang dipilih -->
                        <b-button size="sm" variant="outline-danger" block @click="handleGroupingChange([])">
                            Ungroup All
                        </b-button>
                    </div>
                    <!-- checkbox buat milih grouping -->
                    <div v-for="option in groupingOptions" :key="option.id" class="mb-2">
                        <b-form-checkbox v-model="selectedGrouping" :value="option" :unchecked-value="null"
                            @change="handleGroupingChange(selectedGrouping)">
                            {{ option.text }} ({{selectedGrouping.findIndex(o => o.id === option.id) + 1 || ''}})
                        </b-form-checkbox>
                    </div>
                </b-dropdown-form>
            </b-dropdown>
        </div>
        <!--  child component buat nampilin data yang udah digrouping -->
        <GroupingTableRow v-for="group in groups" :key="group.key" :group="group" />
    </div>
</template>

Apa yang di lakukan pada code layout diatas, berikut ini penjelasannya:

  1. Dalam template vuejs tersebut terdapat wrapper sebagai elemen utama yang membungkus seluruh tampilan.
  2. Kemudian terdapat tag dropdown yang memungkinkan pengguna memilih opsi grouping
  3. Tombol ungroup-all yang berfungsi untuk membatalkan semua grouping.
  4. Checkbox untuk menampilkan opsi grouping
  5. Component <GroupingTableRow/> menggunakan looping untuk menampilkan data yang sudah di kelompokkan dan menjadi hirarki.

Setelah membuat layout pada parent component, selanjutnya kita gabungkan fungsi yang sudah kita buatkan sebelumnya pada code parent ini dengan menggunakan struktur code vue-js, berikut implementasinya:

<script>
import GroupingTableRow from './components/GroupingTableRow.vue'

export default {
    components: {
        GroupingTableRow,
},
    data() {
        return {
            groups: [],
            data: [
                {
                    "no": 1,
                    "merk": "BMW",
                    "negara_asal": "Jerman",
                    "tipe_kendaraan": "Mobil Mewah & Sport",
                    "tahun_berdiri": 1916,
                },
            ]
            ,
            groupingOptions: [
                { id: 'tahun_berdiri', text: 'Tahun' },
                { id: 'merk', text: 'Merk' },
                { id: 'negara_asal', text: 'Negara' },
                { id: 'tipe_kendaraan', text: 'Tipe' },
            ],
            selectedGrouping: [],
        }
    },
    methods: {
        groupBy(items, groupings, level = 0) {
            if (!items?.length || !groupings?.length) return [];
 
            const currentGrouping = groupings[0];
            const groups = this.groupItemsByKey(items, currentGrouping);
 
            return Object.entries(groups).map(([key, groupItems]) => ({
                key,
                level,
                groupType: currentGrouping,
                items: groupings?.length === 1 ? groupItems : [],
                subGroups: groupings?.length > 1
                    ? this.groupBy(groupItems, groupings.slice(1), level + 1)
                    : [],
                _showDetails: false,
            }));
        },
 
        groupItemsByKey(items, groupKey) {
            return items.reduce((groups, item) => {
                const key = this.getGroupLabel(item, groupKey);
                if (!groups[key]) groups[key] = [];
                groups[key].push(item);
                return groups;
            }, {});
        },
 
        getGroupLabel(item, groupKey) {
            switch (groupKey) {
                case 'tahun_berdiri':
                    return item.tahun_berdiri;
                case 'merk':
                    return item.merk ?? 'Unknown';
                case 'negara_asal':
                    return item.negara_asal ?? 'Unknown';
                case 'tipe_kendaraan':
                    return item.tipe_kendaraan ?? 'Unknown';
                default:
                    return 'Unknown';
            }
        },
 
        onGroupingChange() {
            this.selectedItems = [];
            this.selectedGroups = [];
            this.updateGroups();
        },
 
        handleGroupingChange(newGrouping) {
            this.selectedGrouping = newGrouping;
            this.onGroupingChange();
        },
 
        updateGroups() {
            if (!this.selectedGrouping?.length) {
                this.groups = [];
                return;
            }
 
            this.groups = this.groupBy(this.data, this.selectedGrouping.map(group => group.id));
        },
    },
    watch: {
        selectedGrouping() {
            this.onGroupingChange();
        },
    }
}
</script>

Pada implementasi kode tersebut dalam tag script terdapat beberapa tambahan fungsi berikut ini adalah tujuannya:

  1. Fungsi updateGroups()digunakan untuk memperbarui daftar grup sesuai dengan opsi grouping yang dipilih.
  2. Fungsi onGroupingChange() cara kerjanya adalah setiap kali opsi grouping berubah, fungsi ini akan memastikan bahwa data sebelumnya dibersihkan dan grup yang baru akan dibuat ulang.
  3. Fungsi handleGroupingChange(newGrouping) ini dipanggil saat pengguna mengubah opsi grouping dalam dropdown.
  4. Kemudian fungsi ada watcher selectedGrouping() tujuannya adalah untuk memantau perubahan yang terjadi pada selectedGrouping, Jika selectedGrouping berubah, otomatis memanggil onGroupingChange() untuk melakukan reset dan pembaruan data.

Setelah semua fungsi berhasil dibuat, langkah berikutnya adalah membuat komponen untuk menampilkan data grouping secara hierarkis. 

Dalam implementasinya, kita akan menggunakan teknik rekursif pada Vue.js agar dapat menampilkan struktur grup dan subgrup secara dinamis. 

Komponen ini kita beri nama <GroupingTableRow/>, yang sebelumnya sudah diimpor di parent component. Berikut adalah implementasinya:

<template>
    <div>
        <div class="group-header d-flex align-items-center justify-content-between border-primary"
            @click="group._showDetails = !group._showDetails">
            <div class="wrapper__ d-flex align-items-center" @click.stop>
                <i :class="['fas mr-2', group._showDetails ? 'fa-chevron-down' : 'fa-chevron-right']"
                    @click="group._showDetails = !group._showDetails"></i>
                <label class="d-flex align-items-center mb-0 " style="cursor: pointer;">
                    <input type="checkbox" style="width: 18px; height: 18px;">
                    <span class="group-title ml-2" style="font-size: 16px; font-weight: 500;">{{ group.key }}</span>
                </label>
            </div>
            <div class="d-flex align-items-center">
                <div class="count-badge">
                    This Group Level: {{ group?.level }}
                </div>
            </div>
        </div>
 
        <div v-if="group._showDetails" class="pl-4" style="cursor: pointer;">
            <template v-if="isLastLevel">
                <b-table :items="group.items" :fields="visibleFields" responsive="sm" table-class="table-striped bg-white"
                    thead-class="bg-primary text-white">
                    <template #cell(checkbox)="data">
                        <input type="checkbox" style="width: 18px; height: 18px;"
                            @change="$emit('toggle-selection', data.item)">
                    </template>
                </b-table>
            </template>
 
            <GroupingTableRow v-for="subGroup in group.subGroups" :key="subGroup.key" :group="subGroup">
                <template #action-btn="slotProps">
                    <slot name="action-btn" :item="slotProps.item"></slot>
                </template>
            </GroupingTableRow>
 
        </div>
    </div>
</template>
 
<script>
 
export default {
    name: 'GroupingTableRow',
    props: {
        group: {
            type: Object,
            required: true
        },
    },
 
    data() {
        return {
            visibleFields: [
                { key: "checkbox", label: "", sortable: false, class: "w-8" },
                { key: 'merk', label: 'Merk' },
                { key: 'tahun_berdiri', label: 'Tahun Berdiri' },
                { key: 'negara_asal', label: 'Negara' },
                { key: 'tipe_kendaraan', label: 'Tipe' },
            ],
        }
    },
 
    computed: {
        isLastLevel() {
            return !this.group.subGroups || this.group.subGroups.length === 0;
        },
 
    },
};
</script>
 
<style scoped>
.group-header {
    display: flex;
    align-items: center;
    padding: 0.75rem;
    background-color: #f8fafc;
    border: 1px solid #e2e8f0;
    border-radius: 0.375rem;
    cursor: pointer;
    margin-bottom: 0.5rem;
}
</style>

berikut ini adalah tampilan untuk component GroupingTableRow :

Row Hasil Group By

Komponen GroupingTableRow kita gunakan untuk menampilkan data yang telah dikelompokkan secara hierarkis, digunakan secara rekursif, sehingga setiap grup dapat memiliki subgrup tanpa batasan level. Berikut penjelasannya:

  1. Terdapat struktur template vue js dengan <div class=”group-header”> di dalamnya elemen tersebut bertindak sebagai header untuk setiap group.
  2. Pada event click @click=”group._showDetails = !group._showDetails” yang terjadi adalah grup akan ditampilkan atau disembunyikan dengan mengubah value dari _showdetails.
  3. Pada saat group._showDetails bernilai true akan ada dua kemungkinan tampilan jika isLastLevel bernilai true maka akan menampilkan data menggunakan <b-table>, jika isLastLevel bernilai false maka memanggil kembali komponen <GroupingTableRow/> untuk setiap subgrup, proses ini berlanjut secara rekursif sampai level terakhir dicapai.
  4. Terdapat props object grup yang berisi informasi data dan subgrup.
  5. Pada data terdapat visibleFields untuk menentukan kolom-kolom yang akan ditampilkan di tabel untuk level terakhir.
  6. Computed property isLastlevel Jika tidak ada subgrup (group.subGroups.length === 0), maka grup ini dianggap level terakhir dan ditampilkan dalam bentuk tabel.
  7. Yang terakhir adalah sedikit  styeling untuk componentnya.

Setelah pembuatan component selesai, selanjutnya kita save untuk ujicoba fitur grouping nya, berikut adalah video singkat nya:

Preview Correlation View

Kesimpulan

Dengan Dynamic Correlation View, pengalaman pengguna jadi lebih nyaman. Sekarang, pengguna bisa dengan mudah mengelompokkan, menelusuri, dan memahami data tanpa harus repot mencari satu per satu. 

Navigasi yang fleksibel, tampilan yang rapi, dan interaksi yang responsif bikin semuanya terasa lebih mudah. engga cuma bikin kerja lebih efisien, tapi juga lebih asyik! 😀 

Gimana menurut kalian? Ada saran atau ide biar fitur ini makin keren? Drop pendapat kalian di kolom komentar ya!

Categorized in:

Case Study,