Table

image-20211002210333023

Intro

Laravolt Table menyediakan UI untuk menampilkan data dalam bentuk tabular lengkap dengan fitur penunjang sebuah datatable seperti searching, sorting, dan filtering. Laravolt Table berbasis Livewire, sehingga interaksi dapat dilakukan tanpa full page refresh. Tidak perlu khawatir juga dengan Javascript karena semua tetek bengek terkait UI sudah ditangani oleh Laravolt. Kita cukup koding PHP-nya saja.

Membuat Tabel

Karena berbasis Livewire, maka semua Table disimpan di folder app\Http\Livewire\Table. Untuk membuat Table baru, cukup jalankan perintah make:table:

php artisan make:table UserTable

Selanjutnya, cukup definisikan sumber data dan kolom apa saja yang hendak dimunculkan:

class UserTable extends TableView
{
    public function data()
    {
        return \App\Models\User::where('status', 'ACTIVE')->paginate();
    }

    public function columns(): array
    {
        return [
            Text::make('nama', 'Nama'),
            Text::make('email', 'Email'),            
        ];
    }
}

Menampilkan Tabel

Untuk menampilkan tabel, cukup panggil Blade component atau Blade directive dari view:

resources/views/users/index.blade.php
<livewire:table.user-table />

// atau

@livewire('table.user-table')

// atau

@livewire(\App\Http\Livewire\Table\UserTable::class)

Referensi

  • https://laravel-livewire.com/docs/2.x/rendering-components

Sumber Data

Laravolt Table mampu mengolah beberapa macam sumber data untuk kemudian ditampilkan ke dalam tabel. Opsi yang tersedia untuk bisa dipakai sebagai return value dari method data() adalah.

  • Array
  • Collection
  • Eloquent
  • Query Builder
  • Response dari Http Client

Array

Sumber data paling sederhana adalah array.

public function data()
{
    return [
        ['name' => 'Andi', 'email' => 'andi@example.com'],
        ['name' => 'Budi', 'email' => 'budi@example.com'],
    ];
}

Collection

\Illuminate\Support\Collection juga bisa dijadikan sebagai sumber data:

public function data()
{
    $users = [
        ['name' => 'Andi', 'email' => 'andi@example.com'],
        ['name' => 'Budi', 'email' => 'budi@example.com'],
        ['name' => 'Citra', 'email' => null],        
    ];

    return collect($users)->reject(fn($user) => $user->email === null);
}

Eloquent

Sumber data yang paling umum digunakan tentu saja Eloquent:

use App\Models\User;

public function data()
{
    return User::whereNull('deleted_at')->paginate();
}

Jika tidak membutuhkan paginasi, kita bisa langsung memanggil get():

use App\Models\User;

public function data()
{
    return User::query()->whereNull('deleted_at')->get();
}

Query Builder

Jika Eloquent bukan pilihanmu dan lebih memilih Query Builder, kita tetap bisa menerapkan hal yang sama:

public function data()
{
    return \DB::table('users')->whereNull('email')->paginate();
}

 

public function data()
{
    return \DB::table('users')->get(); // tanpa paginasi
}

Response Dari HTTP Client

Untuk mendapatkan sumber data langsung dari API, kita bisa memanfaatkan HTTP Client bawaan Laravel:

use Illuminate\Support\Facades\Http;

public function data()
{
    return Http::get('https://jsonplaceholder.typicode.com/users');
}

Column Types

Avatar

Kolom Avatar digunakan untuk menampilkan gambar avatar secara otomatis dari inisial. Pada contoh di bawah ini, inisial dari email akan digunakan untuk menghasilkan gambar avatar yang sesuai.

use Laravolt\Suitable\Columns\Avatar;

public function columns(): array
{
    return [
        Avatar::make('email', 'Avatar'),
    ];
}

Dokumentasi lengkap tentang Avatar bisa dibaca di https://github.com/laravolt/avatar.

Boolean

Kolom Boolean digunakan untuk menampilkan nilai true atau false dalam bentuk ikon. Nilai yang tidak bertipe boolean akan di-casting secara otomatis ke tipe (bool).

use Laravolt\Suitable\Columns\Boolean;

public function columns(): array
{
    return [
        Boolean::make('is_active'),
    ];
}

Button

Kolom Button digunakan untuk menambahkan tombol yang berfungsi sebagai navigasi untuk berpindah ke halaman lain.

use Laravolt\Suitable\Columns\Button;

public function columns(): array
{
    return [
        Button::make('permalink', 'Info')->label('Profile')->icon('external link'),
    ];
}

Pada contoh di atas, permalink berisi sebuah url yang valid, baik itu hardcoded "http://example.com" atau hasil dari pemanggilan fungsi route() atau url() di Laravel:

class User extends Model 
{
    public function getPermalinkAttribute()
    {
        return route('users.show', $this->id);
    }
}

Button::make() juga menerima Closure untuk meng-generate url secara dinamis ketika pemanggilan:

use Laravolt\Suitable\Columns\Button;

public function columns(): array
{
    return [
        // bisa memakai sintaks baru (arrow function)
        Button::make(fn ($user) => route('users.show', $user['id'])),

        // atau cara lama yang lebih panjang
        Button::make(
            function ($user) {
                return route('users.show', $user['id']);
            }
        ),    
    ];
}

Checkall

Kolom Checkall digunakan untuk menambahkan checkbox di setiap baris yang selanjutnya bisa digunakan sebagai multiple selection. ID yang telah dipilih (dicentang) selanjutnya bisa dikirim sebagai parameter ketika kita mendefinisikan Table Action.

use Laravolt\Suitable\Columns\Checkall;

public function columns(): array
{
    return [
        Checkall::make('id'),
    ];
}

Date

Kolom Date digunakan untuk mengubah tanggal dari database dengan format format 2021-06-02 menjadi tanggal yang lebih manusiawi untuk dibaca, yaitu 2 Juni 2021.

use Laravolt\Suitable\Columns\Date;

public function columns(): array
{
    return [
        Date::make('created_at'),
    ];
}

Ada method tambahan yang tersedia untuk tipe kolom Date, yaitu:

  • format(string $format) dimana $format adalah string yang sesuai dengan format yang diterima oleh Moment.js.
  • timezone(string $timezone) untuk melakukan konversi otomatis tanggal ke timezone yang sesuai. Daftar timezone yang valid bisa dilihat di dokumentasi resmi PHP. Jika tidak memanggil timezone() secara eksplisit, maka konversi ke timezone akan dilakukan dengan melihat:
    • Atribut timezone dari user yang sedang login: auth()->user()->timezone.
    • Jika null, timezone akan dilihat dari config('app.timezone').

DateTime

Mirip seperti kolom Date, DateTime mengubah nilai 2021-06-02 22:54:00 menjadi 2 Juni 2021 pukul 22.45.

use Laravolt\Suitable\Columns\DateTime;

public function columns(): array
{
    return [
        DateTime::make('created_at'),   
    ];
}

Method format() dan timezone() juga tersedia, dengah behaviour yang sama dengan kolom Date.

Html

Kolom Html digunakan jika konten yang akan ditampilkan mengandung tag HTML, dan kita ingin menampilkannya sesuai format HTML.

public function data()
{
    return [
        ['bio' => '<b>Strong</b> <i>foo</i>'],
    ];
}

public function columns(): array
{
    return [
        Html::make('bio')
    ];
}

Pada contoh di atas, tabel akan menampilkan konten sebagai "Strong foo". Kolom ini biasa digunakan ketika data yang disimpan berasar dari WYSIWYG atau rich text editor seperti redactor dan tinymce.

Id

Kolom Id digunakan untuk menampilkan primary key dari sebuah model Eloquent. Di balik layar, kolom ini akan memanggil getKey(). Jadi pastikan hanya menggunakan kolom ini ketika sumber data berasal dari Eloquent.

use Laravolt\Suitable\Columns\Id;

public function columns(): array
{
    return [
      Id::make(),
    ];
}

Image

use Laravolt\Suitable\Columns\Image;

public function columns(): array
{
    return [
      Image::make('profile_picture')
    ];
}

Method tambahan yang tersedia: - height(int $sizeInPixel) untuk mengatur tinggi gambar. - width(int $sizeInPixel) untuk mengatur lebar gambar. - alt(string $text) untuk mengatur atribut alt (alternate text).

Label

Kolom Label digunakan untuk menampilkan sebuah nilai agar terlihat lebih mencolok. Contoh nilai yang cocok ditampilkan sebagai Label antara lain: status, kategori, jenis, tipe, dan sejenisnya.

use Laravolt\Suitable\Columns\Label;

public function columns(): array
{
    return [
        Label::make('status'),
    ];
}

Untuk menambahkan kelas CSS kepada Label, bisa memanfaatkan method addClass:

use Laravolt\Suitable\Columns\Label;

public function columns(): array
{
    return [
        Label::make('status')->addClass('green small'),
    ];
}

Ada beberapa kelas built in yang bisa dipakai untuk menentukan warna sebuah Label, yaitu red, orange, yellow, olive, green, teal, blue, violet, purple, pink, brown, grey, black.

Pada prakteknya, ada kebutuhan untuk memberikan warna yang berbeda untuk setiap value. Hal ini bisa dilakukan dengan memanfaatkan method map():

use Laravolt\Suitable\Columns\Label;

public function columns(): array
{
    return [
        Label::make('status')->map([
            'active' => 'green',
            'banned' => 'red',
        ]),
    ];
}

Number

Kolom Number digunakan untuk menampilkan sebuah value dalam format angka yang lebih manusiawi. Sebagai contoh, sebuah nilai 1000000 akan secara otomatis diubah menjadi 1.000.000 dan ditampilkan rata kanan.

use Laravolt\Suitable\Columns\Number;

public function columns(): array
{
    return [
        Number::make('gaji',)
    ];
}

Format angka yang saat ini digunakan adalah format yang umum dipakai di Indonesia.

Raw

Kolom Raw digunakan untuk menampilkan sebuah value apa adanya (unescaped), tanpa melewati fungsi htmlspecialchars. Hati-hati ketika menggunakan Raw untuk sebuah value yang berasal dari inputan user, karena ada peluang terjadinya Cross Site Scripting (XSS).

use Laravolt\Suitable\Columns\Raw;

public function data()
{
    return [
        ['bio' => '<b>Strong</b><script>alert("foo")</script>'],
    ];
}

public function columns(): array
{
    return [
        Raw::make('bio'),
    ];
}

Pada contoh di atas, kita akan mendapatkan sebuah tulisan Strong (dalam huruf tebal) dan kode Javascript untuk menampilkan alert akan dieksekusi oleh browser.

Jika membutuhkan custom logic yang lebih leluasa, maka jenis kolom Raw juga bisa menerima Closure:

// Menampilkan list of roles dari seorang user, dipisahkan dengan koman
Raw::make(
    function ($user) {
        return $user->roles->implode('name', ', ');
    },
    'Roles'
),

RestfulButton

Kolom RestfulButton digunakan untuk membuat tombol-tombol standard sebuah proses create-read-update-delete atau CRUD. Kita hanya perlu mendefinisikan resource name untuk kemudian dihasilkan tiga buat tombol show, edit, dan destroy.

use Laravolt\Suitable\Columns\RestfulButton;

public function columns(): array
{
    return [
        RestfulButton::make('users'),
    ];
}

Pada contoh kode di atas, ketiga tombol yang dihasilkan akan memiliki route:

  • show: route('users.show', <id>)
  • edit: route('users.edit', <id>)
  • destroy: route('users.destroy', <id>)

<id> otomatis diambil dari primary key object yang bersangkutan. Oleh sebab itu, RestfulButton hanya bisa dipakai jika sumber data berasal dari Eloquent.

Method tambahan yang tersedia:

  • only($action1, $action2) jika hanya ingin menampilkan aksi tertentu saja, misalnya only('show').
  • except($action1, $action2) jika ingin menghilangkan tombol tertentu, misalnya except('destroy').

Untuk memahami lebih lanjut tentang resource controller, silakan membaca dokumentasi resmi dari Laravel.

RowNumber

Kolom RowNumber digunakan untuk menampilkan nomor baris secara terurut, dimulai dari 1.

use Laravolt\Suitable\Columns\RowNumber;

public function columns(): array
{
    return [
        RowNumber::make(),
    ];
}

Text

Kolom Text digunakan untuk menampilkan value secara aman, terhindar dari Cross Site Scripting (XSS), memanfaatkan fungsi htmlspecialchars bawaan PHP.

use Laravolt\Suitable\Columns\Text;

public function data()
{
    return [
        ['bio' => '<b>Strong</b><script>alert("foo")</script>'],
    ];
}

public function columns(): array
{
    return [
        Text::make('bio'),      
    ];
}

Pada contoh di atas, "bio" akan ditampilkan apa adanya sesuai teks yang tertulis:

<b>Strong</b><script>alert("foo")</script>

Text vs Html vs Raw

Ada tiga kolom yang bisa digunakan untuk menampilkan konten yang berasal dari WYSIWYG dan mengandung tag HTML (dan Javascript). Berikut ini adalah perbedaan dari ketiganya:

Kolom Output Keterangan
Text <b>Strong</b> <script>alert("foo")</script> Paling aman, menampilkan teks apa adanya.
Html Strong <script>alert("foo")</script> Jika hanya ingin mengeksekusi tag HTML saja.
Raw Strong (dan muncul alert di browser) Jika ingin mengeksekusi sepenuhnya kode HTML dan Javascript.

Url

Kolom Url digunakan untuk menampilkan sebuah URL secara otomatis menjadi link yang bisa diklik.

use Laravolt\Suitable\Columns\Url;

public function columns(): array
{
    return [
        Url::make('website),
    ];
}

View

Ketika tampilan semakin kompleks, kita bisa memanfaatkan kolom View untuk memindahkan logic untuk merender isi sebuah kolom ke sebuah file blade terpisah.

use Laravolt\Suitable\Columns\View;

public function columns(): array
{
    return [
        View::make('profil')
    ];
}

Lalu buat sebuah file blade profil.blade.php:

resources/views/profil.blade.php
<dl>
    <dt>Name</dt>
    <dd>{{ $data->name }}</dd>
    <dt>Email</dt>
    <dd>{{ $data->email }}</dd>
</dl>

Custom Column

Jika kamu tidak menemukan jenis Column yang sesuai, maka Raw merupakan pilihan pertama untuk menampilkan data dengan custom logic buatan sendiri.

Jika custom logic tersebut dibutuhkan di beberapa tempat, dan kamu ingin menerapkan prinsip Don't Repeat Yourself (DRY), maka membuat sebuah Class khusus untuk custom column menjadi pilihan yang lebih bijak.

Berikut ini adalah kerangka dari sebuah custom column yang berfungsi untuk melakukan masking terhadap data yang ditampilkan:

<?php

namespace App\Tables\Columns;

use Laravolt\Suitable\Columns\Column;
use Laravolt\Suitable\Columns\ColumnInterface;

class MaskColumn extends Column implements ColumnInterface
{
    public function cell($cell, $collection, $loop)
    {
        return \Str::mask($cell->{$this->field});
    }
}

Yang perlu dibuat hanyalah sebuah class yang extends Column (base class), mengimplementasikan ColumnInterface, dan mendefinisikan logic untuk menampilkan data pada fungsi cell($cell, $collection, $loop).

$cell merupakan item dari Colllection (bisa Eloquent model atau obyek lain) yang saat ini akan ditampilkan di dalam cell dari sebuah tabel.

$collection adalah keseluruhan data (Collection).

$loop berisi informasi tentang looping, sesuai dokumentasi dari Laravel.

Pada contoh di atas, custom column disimpan di app/tables/columns. Ini bukan aturan baku. Silakan sesuaikan namespace sesuai struktur aplikasimu.

Searching

Search Query

Ketika pengguna mengetikkan sesuatu di kolom pencarian (searchbox), maka isian tersebut akan otomatis di-binding ke properti $search, sehingga kita bisa mengaksesnya dengan memanggil $this->search.

Berikut ini contoh kode untuk melakukan pencarian berdasar nama:

class UserTable extends TableView
{
    public function data()
    {
        return \App\Models\User::where('status', 'ACTIVE')
            ->whereLike(['name', 'email'], $this->search)
            ->paginate();
    }
}

whereLike merupakan method tambahan dari Laravolt untuk melakukan pencarian ke beberapa kolom sekaligus dengan mekanisme where <kolom> like "%<keyword>%". Untuk data yang banyak, query ini bisa jadi tidak optimal. Silakan buat sendiri logic pencarian sesuai kebutuhan.

Show/Hide Searchbox

class UserTable extends TableView
{
    public bool $showSearchbox = false; // default to "true"
}

Search Debounce

Setiap kali User mengetikkan sesuatu di searchbox, query pencarian akan otomatis dijalankan dengan mekanisme AJAX. Debounce adalah mekanisme menunggu beberapa saat hingga User dianggap benar-benar sudah berhenti mengetik, baru kemudian dilakukan query untuk mengupdate data yang ditampilkan.

class UserTable extends TableView
{
    public int $searchDebounce = 1000; // in miliseconds, default to 300
}

Sorting

Ketika mendefinisikan tabel, kita bisa menambahkan fungsi sortable() pada setiap kolom yang terdefinisi. Secara otomatis, kolom tersebut akan bisa diklik.

class UserTable extends TableView
{
    public function data()
    {
        return \App\Models\User::where('status', 'ACTIVE')->paginate();
    }

    public function columns(): array
    {
        return [
            Text::make('nama', 'Nama')->sortable(),
            Text::make('email', 'Email')->sortable(),            
            Raw::make(
                fn ($data) => DateHelper::formatDate($data->birth_date),
                'Tanggal Lahir'
            )->sortable('birth_date'),
        ];
    }
}

Tapi jangan lupa, kita juga perlu melakukan modifikasi query agar data yang dihasilkan juga ikut terurut. Laravolt sudah menyediakan Trait AutoSort untuk meng-handle kebutuhan tersebut.

Pertama-tama, pastikan Model yang menjadi sumber data sudah menggunakan AutoSort:

namespace App\Models;

use Laravolt\Suitable\AutoSort;

class User extends Model
{
    use AutoSort;
}

Selanjutnya, kita cukup memanggil fungsi autoSort() ketika melakukan query pengambilan data:

class UserTable extends TableView
{
    public function data()
    {
        return \App\Models\User::where('status', 'ACTIVE')
            ->autoSort($this->sortPayload())
            ->paginate();
    }
}

Filtering

Filtering berfungsi untuk melakukan pencarian dengan lebih exact. Ketika ingin menambahkan Filtering, usahakan sumber data tabel masih berupa Query Builder. Hal ini diperlukan karena logic Filtering akan diterapkan setelah pemanggilan data().

class UserTable extends TableView
{
    public function data()
    {
        // NOT OK
        return \App\Models\User::get();

        // NOT OK
        return \App\Models\User::paginate();

        // OK
        return \App\Models\User::query();

        // OK
        return \App\Models\User::where('status', 'ACTIVE');
    }
}

Untuk menambahkan Filter, buat sebuah class dengan skeleton seperti berikut:

<?php

namespace App\Tables\Filters;

use Laravolt\Ui\Filters\TextFilter;

class EmailFilter extends TextFilter
{
    protected string $label = 'Email';

    public function apply($data, $value)
    {
        if ($value) {
            $data->where('email', $value);
        }

        return $data;
    }
}

Kamu bisa mengubah logic query pada method apply sesuai kebutuhan. Pada contoh di atas, dilakukan filtering secara exact match.

Lalu tambahkan class tersebut pada method filters() dari Table:

class UserTable extends TableView
{
    public function data()
    {
        return \App\Models\User::where('status', 'ACTIVE');
    }

    public function filters(): array
    {
        return [
            new EmailFilter(),
        ];
    }
}

Kira-kira hasilnya seperti berikut ini:

image-20211003212947424

Built-in Filter

Ada beberapa jenis Filter yang sudah tersedia dan tinggal di-extends saja, yaitu:

  • TextFilter
  • DropdownFilter
  • CheckboxFilter
  • DateFilter

Text Filter

app/Tables/Filters/EmailFilter.php
<?php

namespace App\Tables\Filters;

use Laravolt\Ui\Filters\TextFilter;

class EmailFilter extends TextFilter
{
    protected string $label = 'Email';

    public function apply($data, $value)
    {
        if ($value) {
            $data->where('email', $value);
        }

        return $data;
    }
}

Dropdown Filter

image-20211003213815998

app/Tables/Filters/RoleFilter.php
<?php

namespace App\Tables\Filters;

use App\Models\Role;
use Laravolt\Ui\Filters\DropdownFilter;

class RoleFilter extends DropdownFilter
{
    protected string $label = 'Roles';

    public function apply($data, $value)
    {
        if ($value) {
            $data->whereHas('roles', fn ($query) => $query->where('id', $value));
        }

        return $data;
    }

    public function options(): array
    {
        return Role::query()->pluck('name', 'id')->prepend('All Roles', '0')->toArray();
    }
}

Checkbox Filter

image-20211003213844901

app/Tables/Filters/StatusFilter.php
<?php

namespace App\Tables\Filters;

use Laravolt\Ui\Filters\CheckboxFilter;

class StatusFilter extends CheckboxFilter
{
    protected string $label = 'Status';

    public function apply($data, $value)
    {
        $status = collect($value)->filter()->values()->toArray();
        if (! empty($status)) {
            $data->whereIn('status', $status);
        }

        return $data;
    }

    public function options(): array
    {
        return [
            'ACTIVE' => 'ACTIVE',
            'PENDING' => 'PENDING',
        ];
    }
}

Date Filter

image-20211003213928740

app/Tables/Filters/RegisteredFilter.php
<?php

namespace App\Tables\Filters;

use Laravolt\Ui\Filters\DateFilter;

class RegisteredFilter extends DateFilter
{
    protected string $label = 'Registered At';

    public function apply($data, $value)
    {
        if ($value) {
            $data->where('created_at', $value);
        }

        return $data;
    }
}

Pagination

Pagination akan ditampilkan secara otomatis sesuai sumber data yang dikembalikan pada method data(). Kustomisasi Pagination bisa dilakukan dengan mengubah beberapa properti:

class UserTable extends TableView
{
    // mengubah default jumlah data yang ditampilkan di tiap halaman
    private const DEFAULT_PER_PAGE = 15;    

    // mengubah opsi data yang ditampilkan di tiap halaman
    protected array $perPageOptions = [5, 15, 30, 50, 100, 250];

    // jika ingin mengubah view
    protected $paginationView = 'laravolt::pagination.livewire.simple';
}

Query String

Setiap kali User melakukan interaksi dengan Table, maka state saat ini akan disimpan sebagai query string. Sebagai contoh, setelah mencari data dengan keyword "foo", maka URL dari browser akan otomatis berubah menjadi:

/users?search=foo

Keuntungan yang didapat dengan menyimpan state secara eksplisit di URL antara lain:

  1. User bisa membagi URL saat ini dengan mudah.
  2. Ketika User me-refresh browser, data yang ditampilkan akan sesuai dengan state terakhir.

Berikut ini adalah state yang secara otomatis akan mengubah query string di browser:

protected $queryString = [
    'page' => ['except' => 1],
    'search' => ['except' => ''],
    'sort' => ['except' => null],
    'direction',
    'perPage' => ['except' => self::DEFAULT_PER_PAGE],
];

Silakan pelajari dokumentasi resmi dari Livewire terkait query string.

Namun, jika karena suatu hal kamu tidak menginginkan behaviour di atas (misalnya ketika harus menampilkan 2 tabel dalam satu halaman yang sama), maka cukup kosongkan properti $queryString :

class UserTable extends TableView
{
    protected $queryString = [];   
}

Selanjutnya, setiap kali User berinteraksi dengan tabel (searching, sorting, filtering, pagination), URL browser tidak akan ikut berubah.