Mencoba ECMAScript 6 Harmony Di Firefox

Apa itu ECMAScript? Tidak lain adalah JavaScript! Lalu kenapa disebut ECMAScript? Berdasarkan riwayat di http://en.wikipedia.org/wiki/ECMAScript, Netscape adalah yang pertama kali memasarkan bahasa scripting bernama JavaScript pada browser Netscape Navigator (cikal bakal Firefox). Karena JavaScript sangat sukses, Microsoft kemudian mengembangkan bahasa serupa yang diberi nama JScript untuk dipakai di Internet Explorer. Microsoft kemudian melakukan standarisasi JScript melalui badan standarisasi Ecma International. Setelah berbagai perlawanan antar Netscape (kini menjadi Mozilla Organization) dan Microsoft, akhirnya bahasa yang telah di-standarisasi tersebut, ECMAScript, disepakati menjadi milik bersama. Jadi, JavaScript dan JScript disepakati sebagai implementasi dari bahasa ECMAScript.

Mungkin saja bagi pihak Mozilla, JScript seharusnya adalah implementasi dari JavaScript dan ECMAScript tidak perlu ada, karena mereka yang terlebih dahulu menciptakan JavaScript πŸ™‚ Browser Internet Explorer sebenarnya memakai JScript (http://en.wikipedia.org/wiki/JScript) bukan JavaScript. Microsoft juga membuat varian JScript yang disebut JScript .NET. Tapi bagi sebagian besar developer web, JScript di Internet Explorer dianggap sama seperti JavaScript. Dan yang lebih membingungkan lagi, Microsoft merombak implementasi ECMAScript 5 di Windows 8 dan kini menyebutnya sebagai JavaScript.

Versi paling baru ECMAScript adalah versi 6 yang disebut juga ES6 Harmony. ES6 saat ini belum final, tapi beberapa browser termasuk Firefox sudah mengimplementasikan beberapa bagian dari proposalnya. Seperti apa bahasa pemograman JavaScript generasi terbaru nanti? Saya akan mencoba beberapa fitur yang sudah diimplementasikan oleh Firefox.

ES6 mendukung spread operator. Pada Groovy, ini dilakukan dengan menggunakan operator bintang. Pada ES6, spreading dilakukan dengan menggunakan tiga titik (…) seperti pada contoh berikut ini:

function tambah(a,b) {
    return a + b;
}
var angka = [10, 20];
console.log(tambah(...angka)); // output: 30
// setera dengan:
console.log(tambah(angka[0], angka[1]));  // output: 30

Pada contoh di atas, setiap elemen dari array akan diterjemahkan menjadi argumen.

Saya dapat membuat sebuah variabel menjadi konstan (tidak dapat diubah nilainya) dengan menggunakan const seperti berikut ini:

const JUMLAH_KOLOM = 5;  // variabel ini tidak boleh diubah

ES6 juga memiliki arrow function untuk mendeklarasikan anonymous function secara lebih singkat lagi, seperti pada contoh berikut ini:

tombol.onclick = (e) => { alert('test'); }
// setara dengan:
tombol.onclick = function(e) { alert('test'); }

ES6 juga membolehkan definisi nilai default untuk parameter pada sebuah function seperti pada contoh berikut ini:

function cetak(pesan = '') {
    console.log(pesan + '<br>');
}
cetak(); // output: <br>

Bandingkan hasilnya dengan tanpa nilai default seperti:

function cetak(pesan) {
    console.log(pesan + '<br>');
}
cetak(); // output: undefined<br>

ES6 juga memiliki struktur data baru seperti Map, WeakMap dan Set. Sebagai contoh, kode program berikut ini memakai Map:

var daftarNilai = new Map();
daftarNilai.set('Solid Snake', 60);
daftarNilai.set('Liquid Snake', 70);
for (var[nama, nilai] of daftarNilai) {
    console.log(nama + ' memiliki nilai ' + nilai);
}

// Output:
// Solid Snake memiliki nilai 60
// Liquid Snake memiliki nilai 70

ES6 juga memiliki banyak fitur lainnya yang menarik namun belum di-implementasi-kan oleh Firefox. Salah satunya adalah adanya class. Dengan adanya class, maka OOP di JavaScript bisa lebih mudah lagi dibandingkan dengan saat ini yang berbasis prototype. Selain itu, ES6 juga akan mendukung module sehingga nantinya akan ada keyword import.

Memakai Indexed Database API Di HTML5

Pada web konvensional, satu-satunya metode formal untuk menyimpan data di browser adalah dengan menggunakan cookie. JavaScript dapat mengakses isi cookie melalui document.cookie. HTML5 menawarkan banyak cara baru untuk menyimpan data di sisi client. Saya bisa menyimpan data dengan mengasosiasikannya pada sebuah elemen HTML melalui attribute HTLM5 Custom Data Attributes (data-*). Selain itu, HTML5 juga menawarkan penyimpanan data melalui window.localStorage atau window.sessionStorage. Berbeda dengan penyimpanan melalui cookie, penggunaan HTML5 Web Storage sepenuhnya terjadi di sisi client dan tidak perlu di-inisialisasi melalui HTTP header dari server. Untuk penyimpanan data yang lebih banyak lagi, browser HTML5 memiliki database internal yang dapat diakses melalui Indexed Database API (http://www.w3.org/TR/IndexedDB/).

Pada artikel ini, saya akan mencoba memakai IndexedDB API dengan JavaScript. Sebagai latihan, saya akan membuat sebuah HTML sederhana dimana pengguna dapat mengisi data, menampilkannya pada tabel, dan menyimpannya pada database lokal di browser melalui IndexedDB API. Saya akan mulai dengan membuat HTML seperti berikut ini:

<!DOCTYPE html>
<html>
<head>
    <meta charset='utf-8'/>
    <title>Latihan IndexedDb</title>
    <style type='text/css'>     
        table {
            font: 17px/30px Verdana, Arial, Helvetica, sans-serif;
            border-collapse: collapse; width: 520px;
        }
        table th {
            padding: 0 0.5em; text-align: left; background-color: #FFE45C;
        }
        table tr {
            border-top: 1px solid #fb7a31; border-bottom: 1px solid #fb7a31;
            background: #ffc;
        }
        table td {
            border-bottom: 1px solid #ccc; padding: 0 0.5em;
        }
        form {
            margin-top: 30px;
        }
        ul {
            list-style-type: none; margin: 0px; padding: 0px;
        }
        li {
            padding: 12px; border-bottom: 1px solid #eee;
        }
        li:first-child, li:last-child {
            border-top: 1px solid #777;
        }
        label {
            width: 150px; float: left; margin-top: 3px; 
            display: inline-block;
        }
        #pesan {
            border: 1px dashed #eee; padding: 10px;
            color: #808080; font-family: monospace;
        }
    </style>
</head>
<body>    
    <table>
        <thead>
            <tr>
                <th>NIM</th>
                <th>Nama</th>
                <th>Tanggal Lahir</th>
                <th>IPK</th>
                <th></th>
            </tr>         
        </thead>
        <tbody id="tabel">
        </tbody>
    </table> 
    <form id='frmUtama'>
        <ul>
            <li>
                <label for="nim">NIM:</label>
                <input id="nim" name="nim" type="text" maxlength="20" required/>
            </li>     
            <li>
                <label for="nama">Nama:</label>
                <input id="nama" name="nama" type="text" maxlength="50" required/>
            </li>
            <li>
                <label for="tanggalLahir">Tanggal Lahir:</label>
                <input id="tanggalLahir" name="tanggalLahir" type="text" maxlength="20" pattern="[0-9]{2}/[0-9]{2}/[0-9]{4}" required/>
            </li>     
            <li>
                <label for="ipk">IPK:</label>
                <input id="ipk" name="ipk" type="number" maxlength="3" step="0.1" required />
            </li>
            <li>
                <input id='btnTambah' type="submit" value="Tambah" />
            </li>
        </ul>
    </form>
    <div id='pesan'>    
    </div>
    <script type="text/javascript">
        var tabel = document.getElementById('tabel'),
            nim = document.getElementById('nim'),
            nama = document.getElementById('nama'),
            tanggalLahir = document.getElementById('tanggalLahir'),
            btnTambah = document.getElementById('btnTambah'),
            form = document.getElementById('frmUtama'),
            pesan = document.getElementById('pesan'),
            ipk = document.getElementById('ipk'),
            db;         

        function tambahBaris(e) {
            // Periksa apakah NIM sudah ada
            if (tabel.rows.namedItem(nim.value)) {
                pesan.textContent = 'Error: Nim sudah terdaftar!';
                e.preventDefault();
                return;
            }

            // Membuat baris baru
            var baris = tabel.insertRow();
            baris.id = nim.value;
            baris.insertCell().appendChild(document.createTextNode(nim.value));
            baris.insertCell().appendChild(document.createTextNode(nama.value));
            baris.insertCell().appendChild(document.createTextNode(tanggalLahir.value));
            baris.insertCell().appendChild(document.createTextNode(ipk.value));

            // Membuat tombol hapus untuk setiap baris
            var btnHapus = document.createElement('input');
            btnHapus.type = 'button';
            btnHapus.value = 'Hapus';
            btnHapus.id = nim.value;            
            baris.insertCell().appendChild(btnHapus);

            e.preventDefault();
        }               

        function hapusBaris(e) {
            if (e.target.type=='button') {                
                tabel.deleteRow(tabel.rows.namedItem(e.target.id).sectionRowIndex);
            }
        }

        form.addEventListener('submit', tambahBaris, false);                  
        tabel.addEventListener('click', hapusBaris, true);            
    </script>

</body>
</html>

Pada HTML di atas, saya dapat menambah dan menghapus baris dari tabel seperti yang terlihat pada gambar berikut ini:

Tampilan awal HTML

Tampilan awal HTML

Pada gambar di atas, terlihat bahwa pengguna tidak boleh menambahkan mahasiswa dengan NIM yang sama karena saya akan memakai NIM sebagai key atau primary key yang bersifat unik.

Sekarang, saya akan mencoba menyimpan setiap mahasiswa yang ditambahkan oleh pengguna ke dalam database lokal. Tapi sebelumnya, langkah paling awal yang perlu saya lakukan adalah membuat database baru. Untuk itu, saya menambahkan kode program JavaScript seperti berikut ini:

function kesalahanHandler(e) {
    pesan.innerHTML += 'Kesalahan Database: ' + e.target.errorCode + '<br>';      
}

function buatDatabase() {
    var request = window.indexedDB.open('latihan', 1);
    request.onerror = kesalahanHandler;
    request.onupgradeneeded = function(e) {             
        var db = e.target.result;
        db.onerror = kesalahanHandler;                          
        var objectstore = db.createObjectStore('mahasiswa', { keyPath: 'nim' });
        pesan.innerHTML += 'Object store mahasiswa berhasil dibuat.<br>';
    }
    request.onsuccess = function(e) {           
        db = e.target.result;
        db.onerror = kesalahanHandler;                          
        pesan.innerHTML += 'Berhasil melakukan koneksi ke database!<br>';               
    }
}

buatDatabase();

Pemanggilan IndexedDB API selalu bersifat asynchronous sehingga saya harus memakai event handler. Sebagai contoh window.indexedDB.open() akan melakukan koneksi ke database. Tapi ia tidak melakukannya saat itu juga. Ia hanya mengembalikan sebuah IDBRequest. Lalu kapan saya tahu bahwa ‘request ‘ saya sudah selesai dikerjakan? Event success untuk IDBRequest menandakan bahwa ‘request’ tersebut sudah selesai dikerjakan.

Selain itu, bila ini adalah pertama kali database dibuat atau database memiliki versi yang lebih rendah dari yang hendak dipakai, maka event onupgradeneeded untuk IDBRequest akan terjadi. Ini adalah saat yang tepat untuk membuat object store baru. Perlu diperhatikan bahwa IndexedDB API tidak menyimpan data dalam bentuk tabel seperti pada database relasional. Data (tepatnya object) disimpan langsung ke object store berdasarkan sebuah key. Object store adalah sesuatu yang menyerupai tabel di database relasional.

Pada Firefox, data yang dimanipulasi melalui IndexedDB API akan disimpan melalui database SQLite (database relasional). Firefox juga menggunakan SQLite untuk menyimpan nilai penting lain miliknya. Browser yang berbeda bisa saja menggunakan back-end yang berbeda. Misalnya, pada Google Chrome, back-end untuk IndexedDB API adalah LevelDB (database NoSQL) buatan Google.

Pada Firefox terbaru, saya dapat menemukan file database SQLite yang menjadi back-end IndexedDB API di folder C:\Users[namauser]\AppData\Roaming\Mozilla\Firefox\Profiles[profile]\storage\persistent[namadomain]\idb. Untuk melihat isinya, bila sudah men-download SQLite client di http://www.sqlite.org/download.html, saya dapat menggunakan perintah berikut ini:

C:> sqlite3 C:\Users\[namauser]\AppData\Roaming\Mozzila\Firefox\...\namafile.sqlite

sqlite> .tables
database            index_data          object_store        unique_index_data
file                object_data         object_store_index
sqlite> .header on
sqlite> select * from database;
name        version
----------  ----------
latihan     1
sqlite> select * from object_store;
id          auto_increment  name        key_path
----------  --------------  ----------  ----------
1           0               mahasiswa   nim
sqlite> _

Programmer JavaScript yang memakai IndexedDB API tidak perlu (dan tidak seharusnya) mengetahui back-end database karena tujuan dari dari IndexedDB API adalah sebuah abstraksi yang mempermudah developer. Saya membuka isi database back-end hanya untuk menunjukkan bahwa object store yang dibuat dengan IndexedDB API berhasil disimpan oleh Firefox dalam bentuk sebuah database SQLite.

Berikutnya, saya akan menambahkan data pada object store bila pengguna membuat sebuah baris baru di tabel. Untuk itu, saya membuat JavaScript berikut ini:

...
function cetakPesanHandler(msg) {
    return function(e) {
        pesan.innerHTML += msg + '<br>';
    }
}

function buatTransaksi() {
    var transaction = db.transaction(['mahasiswa'], 'readwrite');
    transaction.onerror = kesalahanHandler;
    transaction.oncomplete = cetakPesanHandler('Transaksi baru saja diselesaikan.');                  
    return transaction;
}

function tambahKeDatabase(mahasiswa) {      
    var objectstore = buatTransaksi().objectStore('mahasiswa');
    var request = objectstore.add(mahasiswa);
    request.onerror = kesalahanHandler;
    request.onsuccess = cetakPesanHandler('Mahasiswa [' + mahasiswa.nim + '] telah ditambahkan ke database lokal.');            
}

...

function tambahBaris(e) {
    ...

    // Tambah ke database
    tambahKeDatabase({
        nim: nim.value,
        nama: nama.value,
        tanggalLahir: tanggalLahir.value,
        ipk: ipk.value
    });

    ...
}   

Pada kode program di atas, setiap kali menambah baris baru ke tabel, saya akan memanggil tambahKeDatabase() yang akan membuat sebuah IDBTransaction baru. Untuk menambah data ke object store, saya perlu memanggil method IDBTransaction.objectStore() yang akan mengambalikan object store dalam bentuk IDBObjectStore. Setelah itu, saya bisa memanipulasi object dalam object store dengan memanggil method IDBObjectStore seperti add(), clear(), delete(), put() dan sebagainya. Pada contoh di atas, karena saya ingin menambahkan data baru, maka saya memanggil IDBObjectStore.add().

Konsep transaction pada IndexedDB API cukup berbeda dibandingkan dengan database pada umumnya. Transaction disini bersifat asynchronous dan akan di-commit secara otomatis oleh browser. Developer hanya bisa membatalkannya dengan memanggil method abort(). Lalu kapan transaction di-commit? Selama browser melihat ada request yang diberikan pada transaction, maka transaction akan tetap aktif. Setelah tidak ada request untuk transaction tersebut, browser akan men-commit transaction tersebut secara otomatis. Perilaku ini disebut sebagai auto-commit. Bila user menutup browser sebelum browser sempat melakukan auto-commit, maka perubahan pada transaction tersebut tidak akan disimpan.

Sampai disini, setiap baris yang saya tambahkan ke tabel akan ikut tersimpan ke database seperti yang terlihat pada gambar berikut ini:

Menyimpan object melalui IndexedDB API

Menyimpan object melalui IndexedDB API

Setelah berhasil menyimpan data, saya akan kembali melakukan perubahan pada JavaScript agar membaca isi object store pada saat halaman pertama kali dibuka. Untuk itu, saya menambahkan kode program JavaScript berikut ini:

...
function buatDatabase() {
    ...
    request.onsuccess = function(e) {           
        ...
        bacaDariDatabase();                                                             
    }
}

function bacaDariDatabase() {
    var objectstore = buatTransaksi().objectStore('mahasiswa');
    objectstore.openCursor().onsuccess = function(e) {
        var result = e.target.result;
        if (result) {
            pesan.innerHTML += 'Membaca mahasiswa [' + result.value.nim + '] dari database.<br>';
            var baris = tabel.insertRow();                  
            baris.id = result.value.nim;
            baris.insertCell().appendChild(document.createTextNode(result.value.nim));
            baris.insertCell().appendChild(document.createTextNode(result.value.nama));
            baris.insertCell().appendChild(document.createTextNode(result.value.tanggalLahir));
            baris.insertCell().appendChild(document.createTextNode(result.value.ipk));
            var btnHapus = document.createElement('input');
            btnHapus.type = 'button';
            btnHapus.value = 'Hapus';
            btnHapus.id = result.value.nim;         
            baris.insertCell().appendChild(btnHapus);
            result.continue();
        }
    }   
}
...

Pada kode program di atas, untuk membaca seluruh isi object store, saya menggunakan cursor dengan memanggil openCursor(). Untuk melakukan iterasi ke seluruh object yang ada, saya harus memanggil method continue() dari IDBCursor yang dikembalikan. Pemanggilan continue() akan menyebabkan event handler untuk success kembali dipanggil. Bila masih ada object yang bisa dikembalikan, saya dapat membaca nilainya yang ditampung dalam property result. Bila sudah tidak ada object lagi, maka result akan bernilai undefined.

Sekarang, bila saya menutup browser lalu membuka browser lagi, baris yang sama seperti sebelumnya akan tetap muncul kembali, seperti yang terlihat pada gambar berikut ini:

Membaca object melalui IndexedDB API

Membaca object melalui IndexedDB API

Sebagai langkah terakhir, saya perlu menghapus nilai dari database lokal bila seandainya baris dihapus oleh pengguna. Oleh sebab itu, saya menambahkan kode program JavaScript berikut ini:

...
function hapusDariDatabase(nim) {
    var objectstore = buatTransaksi().objectStore('mahasiswa');
    var request = objectstore.delete(nim);
    request.onerror = kesalahanHandler;
    request.onsuccess = cetakPesanHandler('Mahasiswa [' + nim + '] berhasil dihapus dari database lokal.');
}
...
function hapusBaris(e) {
    if (e.target.type=='button') {                
        tabel.deleteRow(tabel.rows.namedItem(e.target.id).sectionRowIndex);
        hapusDariDatabase(e.target.id);
    }
}

Sekarang, bila saya menghapus baris dari tabel, maka object dari database akan dihapus berdasarkan nim yang diasosiasikan dengan baris tersebut, seperti yang terlihat pada gambar berikut ini:

Menghapus object melalui IndexedDB API

Menghapus object melalui IndexedDB API

Dengan IndexedDB API, saya telah membuat sebuah halaman web dimana seluruh datanya disimpan di sisi client. Tapi ada satu permasalahan yang saya jumpai disini. Walaupun ini adalah sebuah aplikasi yang hanya bekerja di sisi client, saya harus tetap terhubung secara online ke server untuk membaca halaman ini. Pada saat koneksi internet terputus atau saat saya tidak bisa menghubungi server, Firefox akan menampilkan pesan kesalahan Unable to connect: Firefox can’t establish a connection to the servert at … bila saya berusaha men-refresh halaman. Untuk mengatasi masalah ini, HTML5 memperkenalkan Application Cache (AppCache) yang dapat dipakai untuk menyimpan halaman secara offline sehingga pengguna tetap dapat membuka halaman pada saat tidak terkoneksi ke Internet.

Untuk memakai HTML5 AppCache, saya harus mendefinisikan sebuah file appcache. Sebagai contoh, saya akan membuat sebuah file bernama latihan.appcache yang isinya adalah:

CACHE MANIFEST
http://snake.domainku.com:8020/latihan_indexedb.html

Setelah itu, saya mengubah tag HTML dengan menambahkan atribut manifest seperti berikut ini:

<!DOCTYPE html>
<html manifest="latihan.appcache">
    ...
</html>

Sebagai tambahan, bila tidak memakai library seperti Offline.js, saya dapat memeriksa event online dan offline di Firefox untuk memberikan pesan informasi pada pengguna bila pengguna menjadi tidak terkoneksi atau menjadi terkoneksi ke Internet (sebagai catatan, kedua event ini tidak akurat dan hanya memeriksa status work offline di Firefox):

window.addEventListener('offline', function(e) {
    pesan.innerHTML += 'Anda sedang tidak terhubung ke Internet!<br>';
});         
window.addEventListener('online', function(e) {
    pesan.innerHTML += 'Anda sudah terhubung kembali ke Internet!<br>';
});

Sekarang, bila saya menjalankan browser dan menampilkan halaman pada kondisi online, ia akan men-download file yang terdaftar di cache manifest agar dapat dipakai secara offline. Setelah itu, saya dapat membuat halaman secara offline dan mengisi data seperti biasanya. Saya juga bisa me-refresh halaman tanpa menemukan pesan kesalahan lagi, seperti yang terlihat pada gambar berikut ini:

Web dapat direfresh walaupun tidak terhubung ke server

Web dapat direfresh walaupun tidak terhubung ke server

Halaman web tetap dapat bekerja walaupun server dimatikan atau tidak terkoneksi ke server karena Firefox menyimpan file untuk dipakai secara offline. Saya dapat melihat informasi application cache di Firefox dengan membuka menu Options, Network, Advanced. Pada bagian Offline Web Content and User Data, saya akan menemukan informasi seperti pada gambar berikut ini:

Pengaturan AppCache Di Firefox

Pengaturan AppCache Di Firefox

Saya juga dapat mengosongkan isi AppCache bila perlu dengan men-klik tombol Clear Now. Sebagai alternatif, saya juga dapat memilih salah satu situs yang terdaftar dan men-klik tombol Remove… untuk menghapus AppCache untuk situs tersebut.

Memakai Server-Sent Events API Di HTML5

Pada artikel Memakai Reverse Ajax Dengan Spring Web MVC 3.2, saya mengimplementasikan Reverse Ajax dengan menggunakan teknik long polling. Pada artikel tersebut, di sisi client, saya secara recursive terus memanggil jQuery.getJSON() untuk melakukan polling. Pada artikel ini, saya akan memakai cara yang lebih sederhana dengan menggunakan Server-Sent Events (SSE) API. Walaupun membuat semuanya menjadi sederhana, SSE hanya membolehkan komunikasi satu arah (yaitu dari server ke client). Selain itu, data tidak boleh dalam bentuk sembarangan melainkan harus mengikuti format text/event-stream. Setidaknya ini masih lebih mudah diterapkan bila dibandingkan menggunakan WebSocket yang mengharuskan server khusus.

Sebagai latihan, saya akan mulai dengan membuat sebuah proyek Laravel baru dengan memberikan perintah:

composer create-project laravel/laravel latihan_sse --prefer-dist

Setelah itu, saya akan membuat sebuah view sederhana yang melakukan proses login dengan nama login.php di folder view yang isinya seperti berikut ini:

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Login</title>
</head>
<body>
    <form method="post" action="<?php echo action('LoginController@login'); ?>">
        <div>
            <label for="nama">Nama</label>
            <input type="text" name="nama" maxlength="50" required />
        </div>
        <div>
            <label for="password">Password</label>
            <input type="password" name="password" required />
        </div>
        <div>
            <input type="submit" value="Login" />
        </div>
    </form>
</body>
</html>

View di atas akan memanggil LoginController.login() bila tombol submit di-klik.

Berikutnya, saya membuat sebuah view lagi dengan nama Logout.php di folder view yang isinya seperti berikut ini:

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Login</title>
</head>
<body>
    Selamat datang, <strong><?php echo $nama; ?></strong>.
    <?php 
        echo link_to_action('LoginController@logout', 
            'Klik disini untuk logout', array('nama'=>$nama));
    ?>
</body>
</html>

View di atas akan memanggil LoginController.logout() dengan menyertakan parametr nama user bila link untuk logout di-klik oleh pengguna.

Setelah view selesai dibuat, saya akan lanjut dengan membuat sebuah controller sederhana dengan nama LoginController.php di folder controller yang isinya seperti berikut ini:

<?php

class LoginController extends BaseController {

    public function login() {       
        if (Request::has('nama') && Request::has('password')) {
            $nama = Request::get('nama');
            $events = Cache::get('events', function() { return []; });
            $events[] = ['nama'=>$nama, 'aksi'=>'login'];
            Cache::forever('events', $events);            
            return Response::view('logout', array('nama' => $nama));
        } else {
            return Redirect::to('login');
        }
    }

    public function logout() {
        $nama = Request::get('nama');
        $events = Cache::get('events', function() { return []; });
        $events[] = ['nama'=>$nama, 'aksi'=>'logout'];
        Cache::forever('events', $events);        
        return Redirect::to('login');
    }

}

?>

Kode program sederhana di atas akan menyimpan log aktifitas login dan logout ke dalam sebuah array. Karena PHP tidak memungkinkan sebuah penyimpanan yang permanen untuk session berbeda, tidak seperti variabel static di Java EE yang dapat diakses kapan saja, maka saya akan menggunakan Cache dari Laravel. Secara default, Laravel akan menulis isi Cache ke sebuah file sehingga saya dapat memperoleh isi Cache yang sama kapan saja pada session apapun.

Langkah terakhir untuk membuat proses login dan logout bekerja adalah menambahkan kode program PHP berikut ini di routes.php:

Route::get('login', function() {
    return View::make('login');
});

Route::post('login', 'LoginController@login');

Route::get('logout', 'LoginController@logout');

Sekarang, bila saya membuka URL untuk /login, saya dapat login dengan nama pengguna apa saja, kemudian memilih logout. Di balik layar, PHP akan menyimpan aktifitas (event) login atau logout tersebut pada Cache milik Laravel.

Sekarang, saya akan mengimplementasikan bagian yang membaca event login atau logout dari Cache. Halaman tersebut akan memakai SSE: sebuah halaman yang menampilkan status user, apakah sedang online atau offline. Dengan Reverse Ajax, server akan menghubungi client bila ada user yang login atau logoff sehingga client hanya perlu bekerja memperbaharui tampilan jika ada event yang perlu diproses.

Saya akan mulai dengan membuat sebuah view bernama User.php di folder view yang isinya terlihat seperti berikut ini:

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Login</title>
    <style type="text/css">
        table {
            color: #666; font-size: 20px; text-shadow: 1px 1px 0px #fff;
            background: #eaebec; margin: 20px; border: #ccc 1px solid;
            border-radius: 3px; box-shadow: 0 1px 2px #d1d1d1;          
        }
        table td {
            padding-left: 20px; border-top: 1px solid #fff;
            border-bottom: 1px solid #e0e0e0;

        }
        .offline { color: red;  }       
        .online { color: green; }
    </style>  
</head>
<body>
    <table id='tabel'>
        <tbody>
            <tr id='solid snake'>
                <td>solid snake</td>
                <td class='offline'>OFFLINE</td>
            </tr>
            <tr id='liquid snake'>
                <td>liquid snake</td>
                <td class='offline'>OFFLINE</td>
            </tr>
            <tr id='big boss'>
                <td>big boss</td>
                <td class='offline'>OFFLINE</td>
            </tr>
            <tr id='mei ling'>
                <td>mei ling</td>
                <td class='offline'>OFFLINE</td>
            </tr>
        </tbody>
    </table>
    <script type="text/javascript">     
        var source = new EventSource("<?php echo action("EventController@proses"); ?>");      
        source.addEventListener('login', function(e) {
            var td = document.querySelector("table tr[id='" + e.data + "'] td:nth-child(2)");
            td.className = 'online';
            td.textContent = 'ONLINE';    
        }, false);
        source.addEventListener('logout', function(e) {
            var td = document.querySelector("table tr[id='" + e.data + "'] td:nth-child(2)");
            td.className = 'offline';
            td.textContent = 'OFFLINE';
        }, false);      
    </script>
</body>
</html>

Pada view di atas, penggunaan SSE membuat kode program JavaScript terlihat sederhana. Saya hanya perlu membuat sebuah object EventSource. Setelah itu, saya menggunakan addEventListener untuk menentukan apa yang akan dilakukan bila server mengirim event tertentu. Pada contoh di atas, bila event login (custom event) terjadi, maka JavaScript akan mengisi kolom kedua untuk user bersangkutan dengan ‘ONLINE’. Sebaliknya, bila event logout (custom event) dikirim oleh server, maka JavaScript akan mengisi kolom kedua untuk user bersangkutan menjadi ‘OFFLINE’.

Bagian yang lebih sulit adalah membuat kode program di sisi server-nya. Saya wajib mengembalikan respon dalam bentuk seperti:

event: login
data: solid snake

event: login
data: liquid snake

event: logout
data: liquid snake

event: logout
data: solid snake

...

Untuk itu, saya membuat sebuah controller baru dengan nama EventController.php di folder controllers yang isinya seperti berikut ini:

<?php

class EventController extends BaseController {

    public function proses() {
        header('Content-Type: text/event-stream');
        header('Cache-Control: no-cache');
        set_time_limit(0);
        while(1) {
            $events = Cache::get('events', function() { return []; });                    
            if (count($events) > 0) {
                $event = array_shift($events);
                echo "event: {$event['aksi']}n";
                echo "data: {$event['nama']}n";
                echo "n";
                ob_flush();
                flush();
            }           
            Cache::forever('events', $events);            
            sleep(1);           
        }
    }

}

?>

Pada kode program di atas, saya memberikan header Content-Type secara manual. Untuk mengembalikan respon untuk SSE, nilai Content-Type wajib berisi text/event-stream. Setelah itu, saya akan memeriksa isi Cache di Laravel untuk menentukan apakah event yang perlu dikirim ke browser. Perhatikan bahwa saya melakukan ini terus menerus dan function proses() tidak akan pernah selesai. Tidak semua server bisa bertahan bila dipaksa untuk seperti ini! Untuk keperluan produksi saat aplikasi diakses banyak orang secara bersamaan, sebaiknya gunakan server yang mendukung seperti Node.js atau server JEE yang mendukung Servlet 3.0 (asynchronous servlet).

Langkah terakhir adalah mendaftarkan routes di routes.php dengan menambahkan kode program berikut ini:

Route::get('user', function() {
    return View::make('user');
});

Route::get('event', 'EventController@proses');

Sekarang, saya bisa mencoba menjalankan aplikasi web sederhana ini dengan membuka URL http://localhost/latihan_sse/public/user. Semua user pada awalnya akan memiliki status offline. Setelah itu, pada tab browser yang berbeda, saya dapat membuka satu atau lebih URL http://localhost/latihan_sse/public/login untuk melakukan login sesuai dengan nama user yang diinginkan. Perhatikan bahwa status di halaman user akan langsung diperbaharui, seperti yang terlihat pada gambar berikut ini:

Animasi yang menunjukkan isi tabel diperbaharui secara otomatis

Animasi yang menunjukkan isi tabel diperbaharui secara otomatis

Memakai XMLHttpRequest Di JavaScript

Microsoft terkenal dengan strategi embrace, extend and extinguish (http://en.wikipedia.org/wiki/Embrace,_extend_and_extinguish). Contoh yang nyata adalah bagaimana pada awalnya Microsoft ingin mengimplementasikan Java dan menunjukkan bahwa mereka mendukung Java. Namun pada akhirnya, Microsoft membuat sebuah bahasa baru bernama C# dengan menambahkan banyak fitur yang tidak ada di Java (dari pendukung menjadi kompetitor). Begitu juga dengan browser. Pada awalnya, Microsoft Internet Explorer mendukung standar W3C. Lalu, Internet Explorer berkembang diluar jalur dan memperkenalkan banyak fitur baru yang menarik namun tidak compatible dengan browser merk lain. Penggunaan fitur di luar standar tersebut akan membuat developer terikat pada Internet Explorer. Salah satu fitur yang dulunya hanya ada di IE adalah XMLHttpRequest (XHR). Beruntungnya, XHR segera ditiru oleh browser lain dan kini jadi bagian dari standar W3C (http://www.w3.org/TR/XMLHttpRequest2/).

XHR merupakan API penting untuk mengimplementasikan AJAX. Sebagai contoh, saya memiliki HTML seperti berikut ini:

<!DOCTYPE html>
<html>
<head>
    <meta charset='utf-8'/>
    <title>Latihan</title>
    <style type='text/css'>
        body {
            background: linear-gradient(to bottom, #ffffff 0%, #feedb7 50%, #fed452 100%);
        }
        #login {
            border: solid 1px #527dff; margin: 10px; padding: 5px;
            background-color: #ccd9ff;  width: 250px; border-radius: 15px;
            box-shadow: 1px 1px 10px #b8c9ff;       
        }
        #frmLogin div {
            margin: 10px;
        }
        #content {
            margin-top: 30px; padding: 10px; height: 400px;         
        }
    </style>
</head>
<body>    
    <div id='login'>
        <form id='frmLogin'>
            <div>
                <label for='namaUser'>Nama:</label>
                <input type='text' name='namaUser' required />                
            </div>
            <div>
                <label for='password'>Password:</label>
                <input type='password' name='password' required />
            </div>
            <div>
                <input type='submit' value='Login' />
            </div>
        </form>
    </div>
    <div id='content'>
        <h1>Ini adalah halaman utama berisi berita!</h1>
    </div>
</body>
</html>

Halaman ini terdiri atas sebuah area untuk melakukan login dan area lain yang berisi berita, seperti yang terlihat pada gambar berikut ini:

Tampilan awal HTML

Tampilan awal HTML

Dengan XHR, saya dapat mengirimkan data login ke server tanpa harus memperbaharui seluruh layar yang ada. Sebagai contoh, saya akan membuat sebuah controller Laravel sederhana yang mengembalikan JSON dan didaftarkan pada public\login (method POST) yang isinya seperti berikut ini:

<?php

class UserController extends BaseController {

    public function login() {
        $hasil = false;     
        if (Input::has('namaUser') && Input::has('password')) {
            $nama = Input::get('namaUser');
            $password = Input::get('password');
            if ($nama=='solid' && $password=='snake') {
                $hasil = true;
            }                   
        }
        return json_encode(array('hasil' => $hasil, 'nama'=> $nama));
    }

}

?>

Pada kode program PHP di atas, saya mengembalikan string dalam format JSON. Function json_encode() dari PHP akan menerjemahkan array menjadi JSON. Walaupun XmlHttpRequest mengandung nama XML, yang lebih sering digunakan sebagai format pertukaran data saat ini adalah JSON karena lebih mudah diolah oleh pembacanya (bukan sentimen pada Microsoft selaku pembuat XML πŸ™‚ ).

Untuk memanggil controller di atas melalui XHR, saya akan menambahkan JavaScript berikut ini pada HTML:

var form = document.getElementById('frmLogin');
var submit = form.querySelector("input[type='submit']");

submit.addEventListener('click', function(e) {
    var xhr = new XMLHttpRequest();
    xhr.open('POST', '/latihan_laravel/public/login', false);
    xhr.send(new FormData(form));
    if (xhr.status==200) {              
        var json = JSON.parse(xhr.responseText);
        if (json.hasil) {
            document.getElementById('login').textContent = 'Selamat datang, ' + json.nama;                  
        } else {
            alert('User atau password salah, ulangi lagi!');
        }
    }
}, false);

Pada JavaScript di atas, saya menggunakan XHR untuk mengirim data melalui method POST secara synchronous ke server. Mengirim data dalam bentuk POST tidak mudah, oleh sebab itu XMLHttpRequest Level 2 memperkenalkan FormData() yang dapat melakukan encoding <form> secara otomatis sehingga siap dikirim untuk POST.

Saya dapat membaca hasil yang dikembalikan oleh server melalui property responseText. Karena pada PHP, saya mengirim hasil dalam bentuk JSON, maka di JavaScript, saya dapat menggunakan JSON.parse() untuk menerjemahkan string dalam bentuk JSON menjadi object JavaScript. Hampir semua browser modern sudah dilengkapi dengan object JSON. Pada browser yang tidak memiliki object JSON, saya bisa langsung memanggil eval() untuk mengubah string yang dikembalikan dari server menjadi object JavaScript.

Setelah itu, bila login berhasil, maka saya akan menganti form login dengan tulisan ‘Selamat datang’ seperti yang terlihat pada gambar berikut ini:

Request ke server tanpa memperbaharui seluruh halaman

Request ke server tanpa memperbaharui seluruh halaman

Bila saya ingin pemanggilan dilakukan secara asynchronous dimana send() tidak menunggu hingga respon diterima melainkan langsung lanjut ke perintah berikutnya, saya dapat mengubah JavaScript di atas menjadi seperti berikut ini:

submit.addEventListener('click', function(e) {
    var xhr = new XMLHttpRequest();
    xhr.onload = function() {
        if (xhr.status==200) {              
            var json = JSON.parse(xhr.responseText);
            if (json.hasil) {
                document.getElementById('login').textContent = 'Selamat datang, ' + json.nama;
            } else {
                alert('User atau password salah, ulangi lagi!');
            }
        }   
    }
    xhr.open('post', '/latihan_laravel/public/login', true);
    xhr.send(new FormData(form));
    e.preventDefault();
}, false);

Event load adalah salah satu event yang diperkenalkan oleh XMLHttpRequest Level 2. Event ini akan terjadi bila respon dari server sudah diterima seluruhnya. Karena saya melakukan request secara asynchronous, maka saya saya perlu menambahkan event handler untuk load dimana ia akan dikerjakan hanya setelah respon diterima seluruhnya.

Perhatikan bahwa tidak terjadi perpindahan halaman selama saya memanggil server melalui XHR. Hal ini dapat terlihat dari tombol back yang tidak aktif. Bila seandainya ingin memberikan sebuah entry virtual di history sehingga user bisa men-klik tombol back, saya dapat menambahkan penggunaan HTML5 History API seperti berikut ini:

...
submit.addEventListener('click', function(e) {
    ...
    if (xhr.status==200) {              
        var json = JSON.parse(xhr.responseText);
        if (json.hasil) {
            document.getElementById('login').textContent = 'Selamat datang, ' + json.nama;
            history.pushState({login: json.nama}, "Login", '/latihan_laravel/public/latihan_xhr.html');                 
        } else {
            alert('User atau password salah, ulangi lagi!');
        }
    }
}, false);

window.addEventListener('popstate', function(e) {         
    if (e.state==null) {
        window.location.reload();
    } else if (e.state.login) {
        document.getElementById('login').textContent = 'Selamat datang, ' + e.state.login;
    }
}, false);

Sekarang, bila login berhasil, saya akan memperoleh sebuah entry di history seperti yang terlihat pada gambar berikut ini:

Membuat history semu dengan HTML5 History API

Membuat history semu dengan HTML5 History API

Dengan demikian, saya bisa berpindah antar-history walaupun saya tidak pernah memperbaharui layar secara keseluruhan.

Salah satu keterbatasan XHR adalah ia hanya bisa dipakai untuk memanggil URL pada domain yang sama. Bila saya berusaha memanggil URL yang di-host pada domain berbeda, Firefox akan menampilkan pesan kesalahan berikut ini di console:

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at [xyz]. This can be fixed by moving the resource to the same domain or enabling CORS.

Untuk mengatasi masalah ini, W3C memperkenalkan standar Cross-Origin Resource Sharing (CORS). Sebelum CORS, teknik yang sering digunakan adalah image ping (sama seperti yang dipakai untuk mengetahui apakah seseorang telah membuka email kita) dan JSONP. CORS adalah alternatif yang lebih modern dan standar karena browser modern memiliki XMLHttpRequest yang mendukung CORS. CORS bekerja dengan menggunakan HTTP header. Sebagai contoh, saat mengakses URL pada domain yang berbeda, browser dengan XMLHttpRequest yang mendukung CORS akan menambahkan header Origin seperti yang terlihat pada gambar berikut ini:

HTTP Header yang dibuat XMLHttpRequest bila memanggil URL pada domain berbeda

HTTP Header yang dibuat XMLHttpRequest bila memanggil URL pada domain berbeda

Tanggung jawab browser sampai disini, bagian berikutnya yang lebih penting adalah peran server dalam mendukung CORS. Server harus menjawab respon dengan menyertakan HTTP header bernama Access-Control-Allow-Origin yang minimal sesuai dengan isi Origin atau * bila server mengizinkan semua domain untuk mengaksesnya. Sebagai contoh, saya dapat mengubah kode program PHP (Laravel) di server menjadi seperti berikut ini:

<?php

class UserController extends BaseController {

    public function login() {
        $origin = Request::header('Origin');
        $hasil = false;     
        if (Input::has('namaUser') && Input::has('password')) {
            $nama = Input::get('namaUser');
            $password = Input::get('password');
            if ($nama=='solid' && $password=='snake') {
                $hasil = true;
            }                   
        }
        return Response::json(array('hasil' => $hasil, 'nama'=> $nama), 200, 
            array('Access-Control-Allow-Origin'=>$origin));
    }

}

?>

Bila sebelumnya saya membuat JSON secara manual dengan function PHP json_encode(), maka kali ini saya memakai Response::json() yang merupakan facade Laravel yang akan menghasilkan sebuah JsonResponse (turunan dari \Symfony\Component\HttpFoundation\JsonResponse). Pada facade Response::json(), argumen pertama adalah objek yang hendak dikonversikan menjadi JSON. Argumen kedua adalah HTTP status. Pada argumen ketiga, saya bisa mengisi HTTP header yang hendak dikembalikan ke browser.

Sekarang, bila saya mengerjakan kode program JavaScript yang melakukan request melalui XHR, semua akan baik2 saja walaupun URL yang dipanggil berada pada domain berbeda. Hal ini karena saya sudah mengikuti CORS melalui HTTP header yang terlihat seperti pada gambar berikut ini:

Request dari XHR dan response dari server yang mendukung CORS

Request dari XHR dan response dari server yang mendukung CORS

Memakai Fasilitas Drag and Drop Di HTML5

Pada artikel ini, saya akan menggunakan fasilitas drag and drop dari HTML5 untuk membuat sebuah permainan image puzzle sederhana.

Langkah pertama yang saya lakukan adalah membuat HTML yang memecah gambar menjadi 36 <div> berbeda. Nantinya setiap <div> ini harus bisa di-drag. Untuk itu, saya membuat HTML seperti berikut ini:

<!DOCTYPE html>
<html>
<head>
    <meta charset='utf-8'/>
    <style type="text/css">
        .kotak {
            background: url('Koala.jpg') no-repeat;
            position: absolute; border: 2px solid black;
        }
        #petunjuk {
            position: absolute; top: 0px; left: 0px;    
        }
    </style>
    <title>Latihan Drag And Drop</title>
</head>
<body>
    <div id='petunjuk'>
        <img id='gambar' src='Koala.jpg' style='opacity: 0.3;'/>
    </div>    
    <div id='drag'></div>
    <script type="text/javascript">
        var gambar=document.getElementById('gambar');
        var drag=document.getElementById('drag');
        var widthKotak=gambar.width / 6
        var heightKotak=gambar.height / 6;
        var x=0, y=0, posisi=0;
        for (var baris=0; baris<6; baris++) {
            for (var kolom=0; kolom<6; kolom++) {
                var kotak = document.createElement('div');
                kotak.id = posisi++;
                kotak.className = 'kotak';                
                kotak.style.width = widthKotak + 'px';
                kotak.style.height = heightKotak + 'px';                                                                                  
                kotak.style.top = y + 'px';
                kotak.style.left = x +'px';
                kotak.style.backgroundPosition = -x + 'px ' + -y + 'px';                
                kotak.draggable = 'true';             
                kotak.ondragstart = function(e) {
                    e.dataTransfer.setData('text', e.target.id);
                }                           
                drag.appendChild(kotak);
                x+=widthKotak;
            }
            x = 0;
            y+=heightKotak;
        }
    </script>
</body>
</html>

Pada HTML di atas, terdapat sebuah <div> bernama petunjuk yang didalamnya berisi gambar transparan. Saya kemudian membuat 36 <div> dengan class kotak di atas gambar transparan tersebut dimana masing-masing kotak mewakili potongan dari gambar. Untuk hanya menampilkan bagian tertentu dari gambar, saya menggunakan CSS background-position.

Pada Firefox, agar setiap kotak dapat di-drag, saya perlu mengatur atribut draggable menjadi "true". Selain itu, saya wajib memanggil DataTransfer.setData() pada event handler untuk dragstart. Tujuannya adalah untuk memberikan nilai yang diwakili oleh elemen yang sedang di-drag. Pada kode program di atas, nilai yang diwakili masing-masing kotak adalah id-nya yang berupa nomor posisi (dari 0 hingga 35).

Bila saya menampilkan HTML di atas pada browser, setiap kotak sudah dapat di-drag seperti yang terlihat pada gambar berikut ini:

Membuat kotak (<div>) secara dinamis

Membuat kotak (<div>) secara dinamis

Agar lebih menarik, saya perlu membuat posisi masing-masing kotak menjadi acak dengan mengubah kode program yang mengatur atribut CSS top dan left menjadi seperti berikut ini:

kotak.style.top = Math.random()*(window.innerHeight-heightKotak) + 'px';
kotak.style.left = Math.random()*(window.innerWidth-widthKotak) +'px';

Sekarang, bila saya menampilkan halaman HTML di browser, saya akan memperoleh tampilan yang acak seperti berikut ini:

Mengacak posisi kotak (<div>) yang telah dibuat

Mengacak posisi kotak (<div>) yang telah dibuat

Sebuah permasalahan yang muncul adalah ketika saya men-drag kotak ke sebuah posisi berbeda, ia tidak akan berubah dan tetap diam di tempat. Akan lebih baik bila kotak yang di-drag akan berpindah ke posisi terakhir dimana pengguna melepas mouse-nya (baik itu di tujuan maupun di tempat kosong). Oleh sebab itu, saya akan mengubah HTML di atas menjadi seperti berikut ini:

<!DOCTYPE html>
<html>
<head>
    <meta charset='utf-8'/>
    <style type="text/css">
        .kotak {
            background: url('Koala.jpg') no-repeat;
            position: absolute; border: 2px solid black;
        }
        #petunjuk {
            position: absolute; top: 0px; left: 0px;    
        }
    </style>
    <title>Latihan Drag And Drop</title>
</head>
<body>
    <div id='petunjuk'>
        <img id='gambar' src='Koala.jpg' style='opacity: 0.3;'/>
    </div>    
    <div id='drag' draggable='false'></div>
    <script type="text/javascript">
        var gambar=document.getElementById('gambar');
        var drag=document.getElementById('drag');
        var widthKotak=gambar.width / 6
        var heightKotak=gambar.height / 6;
        var x=0, y=0, posisi=0;         
        var startScreenX=0,startScreenY=0;      

        drag.addEventListener('dragstart', function(e) {
            if (e.target.className=='kotak') {
                e.dataTransfer.setData('text', e.target.id);
                startScreenX = e.screenX;
                startScreenY = e.screenY;               
            }       
        }, true);
        drag.addEventListener('dragend', function(e) {
            var target = e.target;
            if (target.className=='kotak') {
                target.style.top = parseInt(target.style.top) + (e.screenY-startScreenY) + "px";
                target.style.left = parseInt(target.style.left) + (e.screenX-startScreenX) + "px";
            }
        }, true);

        for (var baris=0; baris<6; baris++) {
            for (var kolom=0; kolom<6; kolom++) {
                var kotak = document.createElement('div');
                kotak.id = posisi++;
                kotak.className = 'kotak';                
                kotak.style.width = widthKotak + 'px';
                kotak.style.height = heightKotak + 'px';                                                                                  
                kotak.style.top = Math.random()*(window.innerHeight-heightKotak) + 'px';
                kotak.style.left = Math.random()*(window.innerWidth-widthKotak) +'px';
                kotak.style.backgroundPosition = -x + 'px ' + -y + 'px';                
                kotak.draggable = 'true';                             
                drag.appendChild(kotak);
                x+=widthKotak;
            }
            x = 0;
            y+=heightKotak;
        }
    </script>
</body>
</html>

Salah satu perubahan yang saya lakukan di atas adalah saya tidak lagi memberikan event handler pada masing-masing kotak, melainkan hanya memberikan satu event handler pada drag. Tujuannya adalah untuk meningkatkan kinerja (berbeda dengan Java atau C# dimana method adalah sebuah operasi yang sama per class, pada JavaScript, masing-masing function adalah nilai yang perlu ditampung di memori!) Kode program masih dapat bekerja karena event pada JavaScript memiliki event bubling dimana event untuk sebuah elemen akan diteruskan ke parent-nya, grandparent-nya dan seterusnya. Karena seluruh kotak berada dalam drag, maka saya cukup memberikan satu event handler pada drag untuk mewakili seluruh kotak yang ada.

Saya menambahkan event handler untuk dragend yang akan dikerjakan pada saat proses drag sudah selesai. Pada event handler ini, saya menghitung selisih piksel (berdasarkan nilai Event.screenX dan Event.screenY lalu mengatur posisi kotak yang baru (melalui atribut CSS top dan left) berdasarkan selisih tersebut.

Berikutnya, saya perlu membuat target untuk di-drop. Untuk itu, saya membuat <div> baru untuk mewakili target drop sehingga isi HTML saya terlihat seperti berikut ini:

...
<body>
    <div id='petunjuk'>
        <img id='gambar' src='Koala.jpg' style='opacity: 0.3;'/>
    </div>    
    <div id='target'></div>   
    <div id='drag' draggable='false'></div>
</body>
...

Kemudian, saya membuat <div> baru untuk setiap kotak yang saya isi pada target dengan menggunakan JavaScript seperti berikut ini:

...
for (var baris=0; baris<3; baris++) {
    for (var kolom=0; kolom<3; kolom++) {
        // Kotak berisi gambar
        var kotak = document.createElement('div');
        kotak.id = posisi;
        ...

        // Kotak sebagai target
        var kotakTarget = document.createElement('div');
        kotakTarget.id = posisi;
        kotakTarget.className = 'target';
        kotakTarget.style.width = widthKotak + 'px';
        kotakTarget.style.height = heightKotak + 'px';
        kotakTarget.style.top = y + 'px';
        kotakTarget.style.left = x + 'px';
        target.appendChild(kotakTarget);

        x+=widthKotak;
        posisi++;
    }
    x = 0;
    y+=heightKotak;
}
...

Berikutnya, saya perlu menambahkan event handler pada dragenter dan dragover sehingga ia mengerjakan e.preventDefault(). Dengan demikian event handler default pada browser akan dibatalkan (karena pada defaultnya, <div> tidak dapat menjadi target drop). Oleh sebab itu, saya menambahkan JavaScript berikut ini:

var preventer = function(e) { e.preventDefault() };
target.addEventListener('dragenter', preventer, true);  
target.addEventListener('dragover', preventer, true);  

Bila pengguna men-drop pada salah satu kotak pada target, maka akan terjadi event drop di target. Saya perlu menambahkan event handler tersebut seperti berikut ini:

target.addEventListener('drop', function(e) {         
    var diterima = e.dataTransfer.getData('text');
    var diharapkan = e.originalTarget.id;
    if (diterima == diharapkan) {                               
        // Tambahkan nanti!                     
    }
    e.preventDefault();
}, true);       

Untuk menampilkan visualisasi bahwa target dapat menjadi target untuk drop, saya akan menggunakan CSS pseudoclass -moz-drag-over yang hanya berjalan di Firefox. Pada CSS Selectors Level 4 yang saat ini belum stabil (bagian dari CSS4 nanti), ini mungkin setara dengan pseudoclass :active-drop-target. Dengan demikian, deklarasi CSS akan terlihat seperti pada gambar berikut ini:

.target {
    position: absolute; 
}
.target:-moz-drag-over {
    background-color: green; opacity: 0.5;
}

Sekarang, bila saya menampilkan HTML di browser, saya akan memperoleh tampilan seperti berikut ini saat men-drag ke salah satu target:

Memberikan clue pada target drop (<div>) untuk kotak

Memberikan clue pada target drop (<div>) untuk kotak

Berikutnya, saya perlu menentukan apa yang akan terjadi bila pengguna men-drop kotak ke target. Untuk itu saya perlu mengubah kode program di event handler untuk drop. Bukankah saya sudah pernah memberikan handler pada event dragend, lalu apa bedanya dengan drag? Event dragend akan tetap dikerjakan bila drag dibatalkan oleh pengguna. Selain itu, event dragend berlaku untuk elemen yang sedang di-drag. Saya menggunakannya untuk memindahkan kotak tidak peduli apapun tujuannya. Sebaliknya, event drop berlaku pada element yang menjadi tujuan drop (dalam hal ini adalah target).

Perubahan yang saya lakukan menyebabkan HTML saya menjadi seperti berikut ini:

<!DOCTYPE html>
<html>
<head>
    <meta charset='utf-8'/>
    <style type="text/css">
        .kotak {
            background: url('Koala.jpg') no-repeat;
            position: absolute; border: 2px solid black;
        }
        .target {
            position: absolute; 
        }
        .target:-moz-drag-over {
            background-color: green; opacity: 0.5;
        }
        .benar {
            border: 2px solid green;
        }
        #petunjuk {
            position: absolute; top: 0px; left: 0px;    
        }
    </style>
    <title>Latihan Drag And Drop</title>
</head>
<body>
    <div id='petunjuk'>
        <img id='gambar' src='Koala.jpg' style='opacity: 0.3;'/>
    </div>    
    <div id='target'></div>   
    <div id='drag' draggable='false'></div>
    <script type="text/javascript">
        var gambar=document.getElementById('gambar');
        var drag=document.getElementById('drag');
        var target=document.getElementById('target');
        var widthKotak=gambar.width / 6;
        var heightKotak=gambar.height / 6;
        var x=0, y=0, posisi=0;         
        var startScreenX=0,startScreenY=0;      
        var lastHit = false;

        drag.addEventListener('dragstart', function(e) {
            if (e.target.className=='kotak') {
                e.dataTransfer.setData('text', e.target.id);
                startScreenX = e.screenX;
                startScreenY = e.screenY;               
            }       
        }, true);
        drag.addEventListener('dragend', function(e) {
            var target = e.target;
            if (target.className=='kotak' && !lastHit) {
                target.style.top = parseInt(target.style.top) + (e.screenY-startScreenY) + "px";
                target.style.left = parseInt(target.style.left) + (e.screenX-startScreenX) + "px";                
            }
            lastHit = false;
        }, true);


        target.addEventListener('drop', function(e) {         
            var diterima = e.dataTransfer.getData('text');
            var diharapkan = e.originalTarget.id;
            if (diterima == diharapkan) {                               
                var kotak = document.querySelector("div[id='" + diterima + "'].kotak");
                kotak.draggable = false;
                kotak.style.left = e.originalTarget.style.left;
                kotak.style.top = e.originalTarget.style.top;
                kotak.style.zIndex = -1;
                kotak.classList.add('benar');                                                                                                                     
                target.removeChild(e.originalTarget);
                lastHit = true;                     
            }
            e.preventDefault();
        }, true);       

        var preventer = function(e) { e.preventDefault() };
        target.addEventListener('dragenter', preventer, true);  
        target.addEventListener('dragover', preventer, true);                 

        for (var baris=0; baris<6; baris++) {
            for (var kolom=0; kolom<6; kolom++) {
                // Kotak berisi gambar
                var kotak = document.createElement('div');
                kotak.id = posisi;
                kotak.className = 'kotak';                
                kotak.style.width = widthKotak + 'px';
                kotak.style.height = heightKotak + 'px';                                                                                  
                kotak.style.top = Math.random()*(window.innerHeight-heightKotak) + 'px';
                kotak.style.left = Math.random()*(window.innerWidth-widthKotak) +'px';
                kotak.style.backgroundPosition = -x + 'px ' + -y + 'px';                
                kotak.draggable = 'true';                                         
                drag.appendChild(kotak);                

                // Kotak sebagai target
                var kotakTarget = document.createElement('div');
                kotakTarget.id = posisi;
                kotakTarget.className = 'target';
                kotakTarget.style.width = widthKotak + 'px';
                kotakTarget.style.height = heightKotak + 'px';
                kotakTarget.style.top = y + 'px';
                kotakTarget.style.left = x + 'px';
                target.appendChild(kotakTarget);

                x+=widthKotak;
                posisi++;
            }
            x = 0;
            y+=heightKotak;
        }
    </script>
</body>
</html>

Pada JavaScript di atas, saya memakai query selector seperti "div[id='" + diterima + "].kotak'" karena id masing-masing kotak dalam bentuk angka. HTML5 memungkinkan id berupa angka, tapi syntax CSS tidak memungkinkan id berupa angka seperti #07.kotak. Bila sebuah kotak di-drop ke target yang benar, maka saya membuatnya menjadi tidak dapat di-drag lagi dengan memberikan nilai atribut draggable=false. Selain itu, saya menambahkan sebuah CSS class baru padanya dengan perintah kotak.classList.add('benar') yang membuatnya memiliki border berwarna hijau. Bagi yang terbiasa dengan jQuery, ini mirip seperti $(kotak).addClass('benar').

Bila saya menjalankan HTML di atas pada browser, image puzzle sekarang dapat dimainkan, seperti yang terlihat pada gambar berikut ini:

Contoh hasil di browser

Contoh hasil di browser

Memahami Cara Kerja WebGL API

HTML5 melakukan standarisasi pada tag <canvas> dan mempopulerkan Canvas API untuk melakukan manipulasi gambar 2D. Selain Canvas API, <canvas> juga dapat dipakai untuk menampilkan grafis 3D dengan menggunakan WebGL API. Karena WebGL cukup rumit dan berbeda jauh dari Canvas API, pada artikel ini saya akan berusaha menuliskan cara kerja WebGL dengan hanya memakai JavaScript biasa tanpa library eksternal sama sekali.

WebGL adalah salah satu implementasi OpenGL untuk dipakai pada browser tanpa driver atau plugin tambahan. OpenGL adalah API yang digunakan menghasilkan game 3D yang bersaing dengan Microsoft DirectX. Hampir semua game 3D desktop akan memakai salah satu dari OpenGL atau DirectX (yang hanya jalan di Windows). Yup! Game 3D akan memakainya secara langsung maupun tidak langsung (misalnya melalui engine seperti Unity). Seluruh graphic cards modern (sering juga disebut VGA card di pasaran) sudah mendukung OpenGL dan DirectX.

Dulu saat belajar computer graphics, saya menggunakan Visual C++ untuk memanggil OpenGL API secara langsung. Saya masih ingat betapa rumitnya membuat matrix hanya untuk sebuah efek sederhana. Walaupun memeras keringat, tapi ada rasa bangga saat berhasil menyelesaikan quiz-quiz yang ada (karena saya melakukannya tanpa Photoshop πŸ™‚ ). Selain itu setidaknya saya menyadari bahwa mata kuliah aljabar linear bukan mata kuliah yang tak jelas manfaatnya karena ia sering kali diterapkan disini!

Salah satu subset dari OpenGL adalah Open GL ES (Embedded Systems) yang ditujukan untuk perangkat portable. Saya dapat menggunakan OpenGL ES API di Java ME melalui Java Binding for the OpenGL ES API (JSR 239). WebGL adalah API dibuat berdasarkan OpenGL ES API. Bahasa yang dipakai untuk mempogram WebGL adalah JavaScript.

JavaScript??? Sulit membayangkan membuat sebuah game 3D dengan JavaScript, tapi saat ini banyak game engine WebGL yang beredar untuk mempermudah penggunaan WebGL. Masalah lain adalah seluruh kode program JavaScript dapat dilihat dan dimodifikasi oleh pengguna. Ini tidak begitu diharapkan oleh developer game πŸ™‚

Selain itu, walaupun browser mendukung WebGL API, belum tentu hardware di platform pengguna mendukung. Hal ini karena browser perlu mengakses langsung GPU (Graphics Processing Unit) di graphics card. Semua komputer pasti memiliki CPU (contoh merk: Intel atau AMD) untuk mengerjakan instruksi program sehari-hari. Tapi tidak semuanya dilengkapi dengan GPU (contoh merk: NVIDIA) yang mendukung OpenGL terbaru. GPU memiliki tugas mengerjakan instruksi khusus untuk grafis 3D yang membutuhkan presisi tinggi; dengan demikian CPU tidak akan begitu sibuk. Beberapa platform juga yang dilengkapi dengan PPU (Physics Processing Unit) yang akan mengerjakan instruksi untuk perhitungan fisika seperti fraktur, simulasi rambut, dan sebagainya sehingga CPU bisa lebih leluasa mengerjakan hal lainnya. Dan bagi seorang gamer seperti saya, impian utama adalah memiliki GPU terbaru yang dilengkapi PPU plus sebuah monitor HD resolusi tinggi πŸ™‚

Bagaimana contoh penggunaan WebGL? Sebagai latihan, saya akan membuat sebuah halaman HTML sederhana yang melakukan inisialisasi WebGL seperti berikut ini:

<!DOCTYPE html>
<html>
<head>
    <meta charset='utf-8'/>
    <title>Latihan WebGL</title>
</head>
<body>
    <canvas id='canvas' width='640' height='480'>
    </canvas>
    <script type="text/javascript">
        var canvas = document.getElementById('canvas');
        var context = canvas.getContext('webgl');
        if (context) {
            context.clearColor(0.0, 0.0, 0.0, 1.0);         
            context.clear(context.COLOR_BUFFER_BIT);
        }
    </script>
</body>
</html>

Bila saya menjalankan HTML di atas, saya hanya akan memperoleh kotak kosong. Tujuannya adalah memastikan WebGL dapat bekerja di browser saya.

Berikutnya, saya perlu menggambar sesuatu. Tapi ini tidak mudah! WebGL hanya bisa menggambar titik, baris, dan segitiga! Segitiga adalah yang paling sering dipakai pada grafis 3D. Saya kemudian bisa membuat berbagai bentuk lainnya berdasarkan segitiga,titik atau garis yang ada. Btw, ini adalah letak kesulitan utama dalam mempelajari atau memakai WebGL secara langsung: bingung cara menggambar sebuah bentuk dasar. Hal ini karena untuk menggambar bentuk yang paling dasar sekalipun, saya tidak sekedar memanggil satu atau dua function, tapi ada beberapa proses yang harus dilalui.

Apa yang saya gambar di layar akan diterjemahkan oleh shader menjadi titik dan warna. Dengan demikian, saya bisa memprogram shader untuk melakukan transformasi pada segitiga yang saya berikan. Btw, shader akan dikerjakan oleh GPU (bukan CPU). Pada WebGL, shader didefinisikan dengan menggunakan sebuah bahasa khusus yang disebut OpenGL ES Shading Language (GLSL). Tidak seluruh yang ada di GLSL resmi berlaku di WebGL, misalnya ftransform() yang ada di spesifikasi GLSL tidak dijumpai di WebGL.

WebGL memiliki 2 jenis definisi shader yaitu fragment shader yang mewakili informasi warna dan vertex shader yang mewakili posisi hasil pemograman. Koordinat yang dapat digambar pada WebGL memiliki nilai minimal -1.0 hingga maksimal 1.0. Jadi, tidak peduli seberapa besar ukuran canvas, -1 adalah nilai minimal, 0 adalah nilai tengah, dan 1 adalah nilai maksimal. Koordinat di WebGL terdiri atas 3 sumbu: X, Y, dan Z.

Agar lebih jelas, saya akan membuat sebuah kode program sederhana seperti berikut ini:

<!DOCTYPE html>
<html>
<head>
    <meta charset='utf-8'/>
    <title>Latihan WebGL</title>    
</head>
<body>
    <canvas id='canvas' width='640' height='480'>
    </canvas>
    <script type="text/javascript">
        // Inisialisasi WebGL
        var canvas = document.getElementById('canvas');
        var gl = canvas.getContext('webgl');
        gl.clearColor(0.0, 0.0, 0.0, 1.0);          
        gl.clear(gl.COLOR_BUFFER_BIT);

        // Inisialisasi shader      
        var vertexShaderText = 'attribute vec2 aVertexPosition; void main() { gl_Position = vec4(aVertexPosition, 0.0, 1.0); }';
        var fragmentShaderText = 'void main() { gl_FragColor = vec4(0.0, 1.0, 0.0, 1.0); }';              
        var vertexShader = gl.createShader(gl.VERTEX_SHADER);
        gl.shaderSource(vertexShader, vertexShaderText);
        gl.compileShader(vertexShader);     
        var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
        gl.shaderSource(fragmentShader, fragmentShaderText);
        gl.compileShader(fragmentShader);

        // Inisialisasi program
        var program = gl.createProgram();
        gl.attachShader(program, vertexShader);
        gl.attachShader(program, fragmentShader);
        gl.linkProgram(program);
        gl.useProgram(program);

        // Gambar segitiga 
        var buffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0,0, 0,1, 1,1]), gl.STATIC_DRAW);      
        var aVertexPosition = gl.getAttribLocation(program, "aVertexPosition");
        gl.enableVertexAttribArray(aVertexPosition);
        gl.vertexAttribPointer(aVertexPosition, 2, gl.FLOAT, false, 0, 0);      
        gl.drawArrays(gl.TRIANGLES, 0, 3);      
    </script>
</body>
</html>

Pada HTML diatas, saya mendeklarasikan vertex shader dengan GLSL seperti berikut ini:

attribute vec2 aVertexPosition; 
void main() { 
    gl_Position = vec4(aVertexPosition, 0.0, 1.0); 
}

Kode program di atas menerima input berupa posisi vertex dalam koordinat 2D (vec2), lalu mengubahnya menjadi koordinat 3D (vec4) yang ditampung pada gl_Position. Nilai gl_Position merupakan nilai hasil transformasi yang akan dipakai. Ia terdiri atas 4 nilai, yaitu koordinat X, Y, Z, dan nilai proyeksi W. Nilai W adalah pembagi untuk seluruh X, Y, Z yang ada. Pada GLSL di atas, posisi hasil transformasi adalah posisi X dan Y yang sama seperti pada yang diberikan, dengan posisi Z berupa 0.0 dan nilai W berupa 1.0 (tidak ada perubahan). Ini akan menghasilkan posisi persis seperti pada koordinat 2D.

Isi dari fragment shader pada HTML di atas adalah:

void main() { 
    gl_FragColor = vec4(0.0, 1.0, 0.0, 1.0); 
}

Fragment shader di atas akan selalu mengembalikan nilai hijau (Red=0.0, Green=1.0, Blue=0.0, dan Alpha=1.0).

Saya menggambar segitiga berdasarkan deretan array [0,0, 0,1, 1,1] yang merupakan koordinat 2D. Koordinat ini mewakili titik (0,0), (0,1) dan (1,1). Karena vertex shader tidak melakukan apa-apa dengan koordinat tersebut dan memberikan nilai koordinat Z=0, maka ia akan ditampilkan apa adanya tanpa kedalaman, seperti yang terlihat pada hasil berikut ini:

Tampilan WebGL dengan z=0

Tampilan WebGL dengan z=0

Sederhana, bukan? Tapi itu hanya segitiga. Bagaimana bila saya menginginkan bentuk lain? Segiempat, misalnya? Saya dapat menambah sebuah segitiga lagi seperti pada kode program berikut ini:

var buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0,0, 0,1, 1,1, 1,1, 1,0, 0,0]), gl.STATIC_DRAW);       
var aVertexPosition = gl.getAttribLocation(program, "aVertexPosition");
gl.enableVertexAttribArray(aVertexPosition);
gl.vertexAttribPointer(aVertexPosition, 2, gl.FLOAT, false, 0, 0);      
gl.drawArrays(gl.TRIANGLES, 0, 6);

Bila saya menampilkan HTML ini di browser, saya akan memperoleh sebuah segiempat yang terdiri atas 2 segitiga seperti yang terlihat pada gambar berikut ini:

Membuat kotak berdasarkan dua segitiga

Membuat kotak berdasarkan dua segitiga

Berikutnya, saya akan memodifikasi shader akan memberikan warna bervariasi yang merupakan hasil interpolasi. Tapi sebelumnya, kode program GLSL yang diletakkan dalam bentuk string di kode program JavaScript akan sangat sulit dibaca dan ditulis. Oleh sebab itu, saya dapat meletakkan kode program GLSL pada <script> dengan memakai atribut type yang unik sehingga browser tidak akan mengerjakannya sebagai JavaScript. Setelah itu, saya dapat membaca isi teks dari <script> melalui DOM API seperti biasa (memanggil atribut text).

Sebagai latihan, saya menambahkan pewarnaan sehingga kode program saya yang baru akan terlihat seperti berikut ini:

<!DOCTYPE html>
<html>
<head>
    <meta charset='utf-8'/>
    <title>Latihan WebGL</title>
    <script id='latihan-vertex' type='x-shader/x-vertex'>
        attribute vec2 aVertexPosition;
        attribute vec4 aVertexColor;
        varying lowp vec4 vColor;
        void main() { 
            gl_Position = vec4(aVertexPosition, 0.0, 1);
            vColor = aVertexColor;
        }
    </script>
    <script id='latihan-fragment' type='x-shader/x-fragment'>
        varying lowp vec4 vColor;
        void main() { 
            gl_FragColor = vColor; 
        }
    </script> 
</head>
<body>
    <canvas id='canvas' width='640' height='480'>
    </canvas>
    <script type="text/javascript">
        // Inisialisasi WebGL
        var canvas = document.getElementById('canvas');
        var gl = canvas.getContext('webgl');
        gl.clearColor(0.0, 0.0, 0.0, 1.0);          
        gl.clear(gl.COLOR_BUFFER_BIT);

        // Inisialisasi shader      
        var vertexShaderText = document.getElementById('latihan-vertex').text;
        var fragmentShaderText = document.getElementById('latihan-fragment').text;                
        var vertexShader = gl.createShader(gl.VERTEX_SHADER);
        gl.shaderSource(vertexShader, vertexShaderText);
        gl.compileShader(vertexShader);     
        var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
        gl.shaderSource(fragmentShader, fragmentShaderText);
        gl.compileShader(fragmentShader);

        // Inisialisasi program
        var program = gl.createProgram();
        gl.attachShader(program, vertexShader);
        gl.attachShader(program, fragmentShader);
        gl.linkProgram(program);
        gl.useProgram(program);

        // Atur warna
        var warna = [1.0, 1.0, 1.0, 1.0,  
                     1.0, 0.0, 0.0, 1.0,  
                     0.0, 1.0, 0.0, 1.0, 
                     0.0, 0.0, 1.0, 1.0,
                     1.0, 1.0, 1.0, 1.0,
                     0.0, 1.0, 0.0, 1.0];
        var bufferWarna = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, bufferWarna);
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(warna), gl.STATIC_DRAW);        
        var aVertexColor = gl.getAttribLocation(program, "aVertexColor");
        gl.enableVertexAttribArray(aVertexColor);           
        gl.vertexAttribPointer(aVertexColor, 4, gl.FLOAT, false, 0, 0);

        // Gambar Dua Segitiga 
        var buffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0,0, 0,1, 1,1, 1,1, 1,0, 0,0]), gl.STATIC_DRAW);                                   var aVertexPosition = gl.getAttribLocation(program, "aVertexPosition");
        gl.enableVertexAttribArray(aVertexPosition);        
        gl.vertexAttribPointer(aVertexPosition, 2, gl.FLOAT, false, 0, 0);      
        gl.drawArrays(gl.TRIANGLES, 0, 6);      
    </script>
</body>
</html>

Bila saya menampilkan HTML tersebut, saya akan memperoleh hasil seperti pada gambar berikut ini:

Memberikan warna pada setiap vertex

Memberikan warna pada setiap vertex

Pada GLSL shader di atas, saya mendefinisikan variabel vColor dengan qualifier varying. Variabel varying hanya bisa di-isi di vertex shader dan bersifat read-only di fragment shader dimana ia merupakan hasil interpolasi relatif terhadap vertex.

Tipe vColor adalah vec4 yang menunjukkan bahwa ia adalah sebuah vector yang terdiri atas 4 nilai. Vertex shader akan mengisi nilai vColor berdasarkan urutan yang di array warna. Atau dengan kata lain, setiap 4 elemen di array warna adalah sebuah warna bagi salah satu titik segitiga (yang disimpan di buffer).

Selanjutnya, saya akan mengubah kode program di atas untuk membuat sebuah benda 3D yang terdiri atas dua sisi, menjadi seperti berikut ini:

<!DOCTYPE html>
<html>
<head>
    <meta charset='utf-8'/>
    <title>Latihan WebGL</title>
    <script id='latihan-vertex' type='x-shader/x-vertex'>
        attribute vec3 aVertexPosition; 
        attribute vec4 aVertexColor;    
        varying lowp vec4 vColor;       

        void main() { 
            gl_Position = vec4(aVertexPosition, 1.0);
            vColor = aVertexColor;          
        }
    </script>
    <script id='latihan-fragment' type='x-shader/x-fragment'>
        varying lowp vec4 vColor;       
        void main() { 
            gl_FragColor = vColor; 
        }
    </script> 
</head>
<body>
    <canvas id='canvas' width='640' height='480'>
    </canvas>
    <script type="text/javascript">
        // Inisialisasi WebGL
        var canvas = document.getElementById('canvas');
        var gl = canvas.getContext('webgl', {antialias: true});
        gl.clearColor(0.0, 0.0, 0.0, 1.0);          
        gl.clear(gl.COLOR_BUFFER_BIT);

        // Inisialisasi shader      
        var vertexShaderText = document.getElementById('latihan-vertex').text;
        var fragmentShaderText = document.getElementById('latihan-fragment').text;                
        var vertexShader = gl.createShader(gl.VERTEX_SHADER);
        gl.shaderSource(vertexShader, vertexShaderText);
        gl.compileShader(vertexShader);     
        var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
        gl.shaderSource(fragmentShader, fragmentShaderText);
        gl.compileShader(fragmentShader);

        // Inisialisasi program
        var program = gl.createProgram();
        gl.attachShader(program, vertexShader);
        gl.attachShader(program, fragmentShader);
        gl.linkProgram(program);
        gl.useProgram(program);

        // Atur warna
        var warna = [            
            0.5, 0.0, 0.0, 1.0,  
            0.5, 0.0, 0.0, 1.0,
            1.0, 0.0, 0.0, 1.0,
            1.0, 0.0, 0.0, 1.0,
            1.0, 0.0, 0.0, 1.0,
            0.5, 0.0, 0.0, 1.0,

            0.0, 1.0, 0.0, 1.0,
            0.0, 1.0, 0.0, 1.0,
            0.0, 0.5, 0.0, 1.0,
            0.0, 0.5, 0.0, 1.0,
            0.0, 0.5, 0.0, 1.0,
            0.0, 1.0, 0.0, 1.0,                     
        ];
        var bufferWarna = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, bufferWarna);
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(warna), gl.STATIC_DRAW);        
        var aVertexColor = gl.getAttribLocation(program, "aVertexColor");
        gl.enableVertexAttribArray(aVertexColor);           
        gl.vertexAttribPointer(aVertexColor, 4, gl.FLOAT, false, 0, 0);

        // Gambar kubus 
        var buffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
            // sisi depan               
            -0.5, -0.5,  1.0,
            -0.5,  0.5,  1.0,
             0.0,  0.5,  1.0,
             0.0,  0.5,  1.0,
             0.0, -0.5,  1.0,
            -0.5, -0.5,  1.0,

            // sisi samping
              0.0,   0.5,  1.0,
              0.0, - 0.5,  1.0,
             0.25, -0.25, -1.0,
             0.25, -0.25, -1.0,
             0.25,  0.75, -1.0,
              0.0,   0.5,  1.0 
        ]), gl.STATIC_DRAW);                                    
        var aVertexPosition = gl.getAttribLocation(program, "aVertexPosition");
        gl.enableVertexAttribArray(aVertexPosition);        
        gl.vertexAttribPointer(aVertexPosition, 3, gl.FLOAT, false, 0, 0);      
        gl.drawArrays(gl.TRIANGLES, 0, 12);     
    </script>
</body>
</html>

Bila saya menjalankan program di atas, saya akan memperoleh hasil seperti pada gambar berikut ini:

Membuat bentuk 3D

Membuat bentuk 3D

Kali ini pada vertex shader, saya menerima aVertexPosition dalam bentuk vec3 dalam koordinat XYZ sehingga saya bisa membuat bentuk 3D secara leluasa. Saya mendefinisikan 12 vertex yang ditampung oleh buffer. Masing-masing vertex ini memiliki warna sesuatu dengan yang didefinisikan di warna. Kali ini saya tidak acak memberikan warna sehingga hasilnya lebih rapi.

WebGL API termasuk API low-level tanpa banyak pernak-pernik tambahan seperti kamera dan pencahayaan (lightning). Saya perlu menghitung semua yang butuhkan bila hanya WebGL API. Oleh sebab itu, pada proyek yang lebih serius, saya perlu memakai library atau game engine WebGL yang lebih mudah dipakai dibandingkan dengan mengimplementasikan semuanya sendiri.

Sebagai informasi tambahan, Firefox dilengkapi dengan sebuah shader editor yang memudahkan pengguna untuk mengamati shader yang ada pada HTML yang sedang dibuka. Untuk itu, saya perlu memilih menu Developer, Web Console. Shader editor tidak aktif secara default sehingga saya perlu mengaktifkannya secara manual dengan men-klik Toolbox Options dan memberi tanda centang pada Shader Editor seperti pada gambar berikut ini:

Mengaktifkan shader editor

Mengaktifkan shader editor

Setelah itu, saya dapat memantau dan memodifikasi shader secara dinamis untuk halaman yang sedang dibuka seperti yang terlihat pada gambar berikut ini:

Shader editor di Firefox

Shader editor di Firefox

Saya menemukan bahwa perubahan pada GLSL tidak memiliki efek, walaupun pada dokumentasi resminya, Firefox menyebutkan bahwa fitur shader editor dapat memperbaharui perubahan secara langsung. Mungkin ini tidak berlaku untuk semua kondisi.

Memakai HTML5 Editing API

Hampir semua browser modern sudah mendukung atribut contenteditable. Apa fungsinya? Penggunaan contenteditable=true pada sebuah <div> menyebabkan isi dari <div> tersebut dapat di-edit oleh pengguna. Lalu apa bedanya dengan memakai <textarea>? Dengan menggunakan contenteditable, saya dapat men-format isi dari sebuah <div> dengan menggunakan execCommand() untuk membuat huruf tebal, mengubah jenis huruf, warna, menyisipkan gambar, membuat link, dan sebagainya. Function execCommand() adalah bagian dari Editing API. Selain berhubungan dengan format tulisan, Editing API juga menawarkan function yang berhubungan dengan selection seperti getSelection() yang akan mengembalikan sebuah objek Selection.

Sebagai latihan, saya dapat membuat halaman HTML seperti berikut ini:

<!DOCTYPE html>
<html>
    <head>
        <meta charset='utf-8'/>
        <title>Event Tester</title>
        <style type="text/css">     
            #editor {
                float: left; border: 3px solid gray;
                padding: 15px; height: 400px;
                width: 400px; background-color: black;                             
                color: #eeee00; white-space: pre-wrap;
            }
            #outputHTML {               
                width: auto; margin-left: 450px;
                border: 3px solid gray; background-color: #eeeeee;
                height: 400px; padding: 15px; font: 15px monospace;
            }
        </style>
    </head>
    <body>
        <div id='editor' contenteditable="true">Silahkan menulis disini!</div>
        <div id='outputHTML'>Silahkan menulis disini!</div>
        <script type="text/javascript">
            var editor = document.getElementById('editor');
            var outputHTML = document.getElementById('outputHTML');
            editor.addEventListener('input', function(e) {                
                outputHTML.textContent = editor.innerHTML;                          
            }, false);
        </script>
    </body>
</html>

Bila saya menampilkan HTML di atas, saya akan memperoleh hasil seperti pada gambar berikut ini:

<div> yang dapat di-edit

<div> yang dapat di-edit

Saya dapat mengubah isi dari <div> yang ada dibagian kiri sama seperti ketika memakai <textarea>.

Untuk menunjukkan penggunaan HTML Editing API, saya perlu memanggil execCommand(). Function ini membutuhkan tiga nilai berupa aCommandName, aShowDefaultUI, dan aValueArgument. Nilai untuk aCommandName adalah sebuah string yang mewakili aksi yang akan dikerjakan pada teks terpilih seperti backColor, bold, copy, createLink, delete dan sebagainya. Beberapa dari aksi tersebut membutuhkan nilai parameter yang diberikan melalui aValueArgument. Pada Firefox, nilai aShowDefaultUI tidak dipakai sehingga saya membiarkannya bernilai false.

HTML yang telah saya ubah akan terlihat seperti pada contoh berikut ini:

<!DOCTYPE html>
<html>
    <head>
        <meta charset='utf-8'/>
        <title>Event Tester</title>
        <style type="text/css">     
            #editor {
                float: left; border: 3px solid gray;
                padding: 15px; height: 300px;
                width: 400px; background-color: black;                             
                color: #eeee00; white-space: pre-wrap;
            }
            #editor::-moz-selection {
                color: black;
                background-color: #eeee00;
            }
            #outputHTML {               
                width: auto; margin-left: 450px;
                border: 3px solid gray; background-color: #eeeeee;
                height: 300px; padding: 15px; font: 15px monospace;
            }
            #commandPanel {
                margin: 10px;
            }
        </style>
    </head>
    <body>
        <div id='editor' contenteditable="true">Silahkan menulis disini!</div>
        <div id='outputHTML'>Silahkan menulis disini!</div>
        <div id='commandPanel'>
            Command:            
            <select id='command'>
                <option value="backColor">backColor(color)</option>
                <option value="bold">bold</option>
                <option value="contentReadOnly">contentReadOnly(boolean)</option>
                <option value="copy">copy</option>
                <option value="createLink">createLink(link)</option>
                <option value="cut">cut</option>
                <option value="decreaseFontSize">decreaseFontSize</option>
                <option value="delete">delete</option>
                <option value="fontName">fontName(font)</option>
                <option value="fontSize">fontSize(size)</option>
                <option value="foreColor">foreColor(color)</option>
                <option value="formatBlock">formatBlock(tag)</option>
                <option value="indent">indent</option>
                <option value="inserthorizontalrule">inserthorizontalrule</option>
                <option value="insertimage">insertimage(url)</option>
                <option value="insertorderedlist">insertorderedlist</option>
                <option value="insertparagraph">insertparagraph</option>
                <option value="insertunorderedlist">insertunorderedlist</option>
                <option value="italic">italic</option>
                <option value="justifycenter">justifycenter</option>
                <option value="justifyleft">justifyleft</option>
                <option value="outdent">outdent</option>
                <option value="paste">paste</option>
                <option value="removeformat">removeformat</option>
                <option value="selectall">selectall</option>
                <option value="underline">underline</option>
                <option value="unlink">unlink</option>
            </select>
            Value:
            <input id='txtValue' type='text' size='50'/>
            <input id='btnExecute' type='button' value='executeCommand()'/>
        </div>
        <script type="text/javascript">
            var editor = document.getElementById('editor');
            var outputHTML = document.getElementById('outputHTML');
            var selCommand = document.getElementById('command');
            var txtValue = document.getElementById('txtValue');           

            editor.addEventListener('input', function(e) {                
                outputHTML.textContent = editor.innerHTML;              
            }, false);
            document.getElementById('btnExecute').addEventListener('click', function(e) {               
                var optCommand = selCommand.options[selCommand.selectedIndex];
                var strCommand = optCommand.value;              
                document.execCommand(strCommand, false, txtValue.value);                
            }, false);          
        </script>
    </body>
</html>
Contoh hasil execCommand()

Contoh hasil execCommand()

Pada HTML di atas, saya dapat mencoba bereksperimen dengan command yang terdaftar. Pada kasus nyata, biasanya setiap eksekusi command diwakili oleh sebuah button pada toolbar atau sebuah shortcut. Saya tidak akan membuat sebuah editor lengkap seperti itu karena sudah ada banyak script rich text editor untuk HTML seperti TinyMCE dan CKEditor. Fasilitas inline editing dari CKEditor termasuk salah satu fitur yang memanfaatkan atribut contenteditable dari HTML5 (sesungguhnya fasilitas contenteditable sudah lama didukung browser modern sebelum HTML5, hanya saja kini menjadi lebih terstandarisasi).

Satu permasalahan yang cukup menganggu pada HTML di atas adalah teks yang terpilih di editor akan hilang dan tidak di-ingat bila saya memindahkan kursor ke textbox untuk mengisi nilai. Untuk memanipulasi teks yang sedang dipilih, Editing API menyediakan object Selection. Untuk memperoleh object Selection tersebut, saya dapat memanggil document.getSelection() atau window.getSelection(). Saya juga dapat memodifikasi teks yang terpilih dengan memanggil addRange() atau removeRange() dari Selection.

Sebagai contoh, saya dapat mengubah JavaScript di atas agar mengingat teks yang sebelumnya dipilih dengan mengubahnya menjadi seperti berikut ini:

var editor = document.getElementById('editor');
var outputHTML = document.getElementById('outputHTML');
var selCommand = document.getElementById('command');
var txtValue = document.getElementById('txtValue');           
var range;

editor.addEventListener('input', function(e) {                
    outputHTML.textContent = editor.innerHTML;              
}, false);

editor.addEventListener('mouseup', function(e) {
    // menyimpan teks yang terpilih
    var selection = document.getSelection();
    if (selection.rangeCount > 0) range = selection.getRangeAt(0);
}, false);

document.getElementById('btnExecute').addEventListener('click', function(e) {
    // set pilihan kembali ke seperti terakhir kali (yang tersimpan di range)
    var selection = document.getSelection();
    selection.removeAllRanges();
    selection.addRange(range);

    // kerjakan execCommand()               
    var optCommand = selCommand.options[selCommand.selectedIndex];
    var strCommand = optCommand.value;              
    document.execCommand(strCommand, false, txtValue.value);                
}, false);  

Memakai HTML5 Constraints Validation API

Validasi adalah sesuatu yang selalu dibutuhkan dalam setiap kode program. Validasi dapat dilakukan dengan membuat kode program yang memeriksa setiap nilai satu per satu. Tapi cara yang lebih elegan adalah melakukan validasi dengan memberikan constraint secara deklaratif. Cara ini tidak melibatkan kode program sehingga lebih ringkat, sederhana, dan mudah dipahami. Sebagai contoh, saya sering menggunakan Hibernate Validator (salah satu implementasi dari JSR 349 – Bean Validation Specification). Dengan Hibernate Validator, saya cukup memberikan annotation seperti @NotNull, @Size, @NotBlank, @Past dan sebagainya pada domain class. Hibernate ORM akan menghasilkan tabel dengan constraints seperti pada domain class. Teknologi seperti Spring juga akan memeriksa apakah object yang dikirim dari browser memiliki nilai yang melanggar validasi atau tidak. Demikian juga, saat domain object dikirim dari satu layer ke layer lainnya, teknologi yang bersangkutan dapat memeriksa dan memastikan tidak ada validasi yang dilanggar. Proses validasi jadi sangat sederhana, bukan? Cukup dengan annotation dan semuanya berlangsung otomatis.

Bila Hibernate Validator adalah andalan saya untuk sisi server, lalu bagaimana dengan sisi client/browser? Apakah saya masih harus melakukan validasi secara kode program melalui JavaScript? Beruntungnya, HTML5 memiliki Constraints Validation API yang memungkinkan validasi secara deklaratif. Saya dapat memberikan constraint langsung pada HTML seperti pada contoh berikut ini:

<!DOCTYPE html>
<html>
    <head>
        <meta charset='utf-8'/>
        <title>Latihan</title>  
        <style type='text/css'>
            .baris { margin-bottom: 15px; }
        </style>
    </head>
    <body>
        <form id='frmUtama'>
        <div class='baris'>
            <label>Nama:</label>
            <input id='txtNama' name='nama' type='text' maxlength='50' required/>         
        </div>
        <div class='baris'>
            <label>Email:</label>
            <input id='txtEmail' name='email' type='email' maxlength='100' required/>
        </div>
        <div class='baris'>
            <label>Usia:</label>
            <input id='txtUsia' name='usia' type='number' min='0' max='100'/>
        </div>
        <div class='baris'>
            <label>Berlanggan Sampai:</label>
            <input id='txtTanggal' name='tanggal' type='date' required/>
        </div>        
        <div class='baris'>
            <input type='submit' value='proses'/>
        </div>
    </form>
    </body>
</html>

Pada HTML di atas, saya menggunakan constraint seperti required, maxlength, type='email', type='number' dan type='date'. Browser yang mendukung akan menampilkan pesan kesalahan secara otomatis saat saya men-klik tombol submit, seperti yang terlihat pada gambar berikut ini:

Validasi melalui HTML5 Constraints Validation API

Validasi melalui HTML5 Constraints Validation API

Validasi melalui HTML5 Constraints Validation API

Validasi melalui HTML5 Constraints Validation API

Tidak semua browser mendukung constraint di HTML5. Sebagai contoh, Firefox tidak melakukan validasi untuk type='date'. Sebagai gantinya, saya dapat menggunakan validasi melalui regex dengan menggunakan atribut pattern seperti yang terlihat di HTML berikut ini:

<div class='baris'>
    <label>Berlanggan Sampai:</label>
    <input id='txtTanggal' name='tanggal' type='text' pattern='[0-9]{2}-[0-9]{2}-[0-9]{4}' required/>
</div>        
Memakai regex pada validasi

Memakai regex pada validasi

Bagaimana bila saya ingin mengubah pesan kesalahan yang akan ditampilkan oleh browser? Firefox menyediakan cara cepat dengan menggunakan atribut x-moz-errormessage, seperti pada HTML berikut ini:

<!DOCTYPE html>
<html>
    <head>
        <meta charset='utf-8'/>
        <title>Latihan</title>  
        <style type='text/css'>
            .baris { margin-bottom: 15px; }
        </style>
    </head>
    <body>
        <form id='frmUtama'>
        <div class='baris'>
            <label>Nama:</label>
            <input id='txtNama' name='nama' type='text' maxlength='50' required 
                x-moz-errormessage='Nama harus diisi dengan maksimal 50 karakter'/>            
        </div>
        <div class='baris'>
            <label>Email:</label>
            <input id='txtEmail' name='email' type='email' maxlength='100' required
                x-moz-errormessage='Email harus diisi dengan benar'/>
        </div>
        <div class='baris'>
            <label>Usia:</label>
            <input id='txtUsia' name='usia' type='number' min='0' max='100'
                x-moz-errormessage='Usia harus diisi dengan angka dari 0 hingga maksimal 100'/>
        </div>
        <div class='baris'>
            <label>Berlanggan Sampai:</label>
            <input id='txtTanggal' name='tanggal' type='text' pattern='[0-9]{2}-[0-9]{2}-[0-9]{4}' required
                x-moz-errormessage='Tanggal harus dalam format dd-mm-yyyy seperti 31-12-2013'/>
        </div>        
        <div class='baris'>
            <input type='submit' value='proses'/>
        </div>
    </form>
    </body>
</html>
Memakai pesan kesalahan custom pada validasi

Memakai pesan kesalahan custom pada validasi

Karena x-moz-errormessage adalah atribut khusus Firefox, maka ia tidak berlaku untuk browser lain. Berdasarkan informasi di https://www.w3.org/Bugs/Public/show_bug.cgi?id=10923, proposal untuk menambahkan atribut seperti ini ditolak oleh editor W3C (Status: Rejected) dengan alasan terlalu berlebihan (Rationale: Requested feature seems redundant with existing features). Sepertinya membuat standar yang memenuhi keinginan dan kebutuhan semua orang memang bukanlah sebuah hal mudah.

Lalu, kalau begitu apa cara yang disarankan untuk memberikan pesan kesalahan custom? HTML5 Constraints API memiliki method setCustomValidity() pada setiap elemen di dalam form untuk mengatur pesan kesalahan custom. Untuk menggunakanya, saya perlu memakai JavaScript seperti yang terlihat pada contoh berikut ini:

<!DOCTYPE html>
<html>
    <head>
        <meta charset='utf-8'/>
        <title>Latihan</title>  
        <style type='text/css'>
            .baris { margin-bottom: 15px; }
        </style>
    </head>
    <body>
        <form id='frmUtama'>
            <div class='baris'>
                <label>Nama:</label>
                <input id='txtNama' name='nama' type='text' maxlength='50' required/>         
            </div>
            <div class='baris'>
                <label>Email:</label>
                <input id='txtEmail' name='email' type='email' maxlength='100' required/>
            </div>
            <div class='baris'>
                <label>Usia:</label>
                <input id='txtUsia' name='usia' type='number' min='0' max='100'/>
            </div>
            <div class='baris'>
                <label>Berlanggan Sampai:</label>
                <input id='txtTanggal' name='tanggal' type='text' pattern='[0-9]{2}-[0-9]{2}-[0-9]{4}' required/>
            </div>        
            <div class='baris'>
                <input type='submit' value='proses'/>
            </div>
        </form>
        <script type="application/javascript">
            var pesanKesalahan = {
                'txtNama': 'Nama harus diisi dengan maksimal 50 karakter',
                'txtEmail': 'Email harus diisi dengan benar',
                'txtUsia': 'Usia harus diisi dengan angka dari 0 hingga maksimal 100',
                'txtTanggal': 'Tanggal harus dalam format dd-mm-yyyy seperti 31-12-2013',
            }                   
            function customMessage(e) {
                var state = e.target.validity;
                var invalid = state.patternMismatch || state.rangeOverflow || state.rangeUnderflow || state.stepMismatch ||
                              state.tooLong || state.typeMismatch || state.valueMissing;                                                
                e.target.setCustomValidity(invalid? pesanKesalahan[e.target.id]: '');                                             
            }
            var form = document.getElementById('frmUtama');
            form.addEventListener('input', customMessage, true);
            form.addEventListener('invalid', customMessage, true);                                    
        </script>
    </body>
</html>

Pada kode program JavaScript di atas, saya menampung pesan kesalahan custom pada sebuah JSON agar mudah dimodifikasi dikemudian hari. Saya dapat memeriksa jenis kesalahan yang ada dengan memeriksa nilai property validity dari Constraints Validation API. Nilai customError akan selalu bernilai true bila saya sudah memanggil setCustomValidity() sebelumnya sehingga saya harus mengabaikan nilai tersebut. Bila ada yang tidak valid, saya akan menggunakan pesan kesalahan baru dengan memanggil function setCustomValidity(). Event handler ini akan dikerjakan baik pada event input maupun invalid. Event invalid adalah event yang muncul bila ada input yang tidak valid. Tapi itu tidak cukup, saya perlu mengerjakannya pada event input agar pesan kesalahan dibersihkan (dengan memanggil setCustomValidity('')) bila input sudah valid.

Memakai Event Handler Di JavaScript

Cara yang paling umum dipakai dalam menggunakan event handler di JavaScript adalah dengan menggunakan kode program seperti berikut ini:

<!DOCTYPE html>
<html>
<head>
    <meta charset='utf-8'/>
    <title>Latihan</title>
</head>
<body>
    <h1>Latihan</h1>    
    <form id='frmUtama'>
        <div class='baris'>
            <label>Nama:</label>
            <input id='txtNama' name='nama' type='text' class='komponen'/>            
        </div>
        <div class='baris'>
            <label>Jenis:</label>
            <input id='jenisManusia' type='radio' name='jenis' value='manusia' class='komponen'/>Manusia
            <input id='jenisMutant' type='radio' name='jenis' value='mutant' class='komponen'/>Mutant
        </div>
        <div class='baris'>
            <label>Kemampuan:</label>
            <select id='kemampuan' name='kemampuan' multiple=true size='3' class='komponen'>
                <option value='telepati'>Telepati</option>
                <option value='magnetisme'>Magnetisme</option>
                <option value='penyembuhan'>Penyembuhan</option>
                <option value='machinary'>Machinary</option>
            </select>
        </div>
        <div class='baris'>
            <input id='proses' type='submit' value='Proses' />
        </div>
    </form>
    <script type="text/javascript">
        var proses = document.getElementById('proses');
        proses.onclick = function() {
            with(this.form) {
                console.log('Nama = ' + nama.value);              
                for(var item of jenis) { 
                    if (item.checked) { console.log('Jenis = ' + item.value); } 
                }               
                console.log('Kemampuan: ');
                for (var nilai of kemampuan.selectedOptions) {
                    console.log(nilai.value);
                }   
            }
        };
    </script>
</body>
</html>

Pada contoh di atas, saya memberikan anonymous function pada proses.onclick sehingga anonymous function tersebut dikerjakan bila tombol submit di-klik.

Sebagai alternatif, saya juga dapat memberikan event handler dengan DOM Level 2 Events seperti pada kode program berikut ini:

var proses = document.getElementById('proses');
proses.addEventListener('click', function() {
    with(this.form) {
        console.log('Nama = ' + nama.value);              
        for(var item of jenis) { 
            if (item.checked) { console.log('Jenis = ' + item.value); } 
        }               
        console.log('Kemampuan: ');
        for (var nilai of kemampuan.selectedOptions) {
            console.log(nilai.value);
        }   
    }
}, false);

Saya merasa cara ini lebih mirip seperti di Java Swing dimana saya bisa mendaftarkan beberapa event handler berbeda pada sebuah event yang sama.

Event handler yang ada dapat menerima sebuah argument berupa sebuah object Event. Sebagai contoh, saya dapat memanggil method preventDefault() untuk membatalkan aksi dari event. Bila perintah ini diberikan pada tombol submit, maka proses submit (berpindah halaman) akan dibatalkan, seperti yang terlihat pada contoh kode program berikut ini:

var proses = document.getElementById('proses');
proses.addEventListener('click', function(e) {
    with(this.form) {
        if (!nama.value) {
            e.preventDefault();
            return;
        }
        console.log('Nama = ' + nama.value);                              
    }
}, false);

Berikut ini adalah daftar nama event yang sering dipakai:

  • UI events
    1. load
    2. unload
    3. abort
    4. error
    5. select
    6. resize
    7. scroll
  • Focus events
    1. blur
    2. focus
    3. focusin (tidak didukung Firefox)
    4. focusout (tidak didukung FIrefox)
  • Mouse events
    1. click
    2. dblclick
    3. mousedown
    4. mouseenter
    5. mouseleave
    6. mouseout
    7. mouseover
    8. mouseup
    9. mousewheel (pada Firefox bernama DOMMouseScroll)
  • Keyboard events
    1. keydown
    2. keypress
    3. keyup

Sebagai latihan, saya akan membuat sebuah halaman HTML untuk menguji event di atas yang isinya seperti berikut ini:

<!DOCTYPE html>
<html>
    <head>
        <meta charset='utf-8'/>
        <title>Event Tester</title>
        <style type="text/css">     
            #target {
                float: left;
                border: 3px solid black;
                text-align: center;
                height: 400px;
                width: 400px;
                background-color: green;                               
                color: white;
            }
            #daftarEvent {              
                width: auto;
                margin-left: 450px;
            }
            input {
                margin-top: 10px;
                font-size: large;
            }
        </style>
    </head>
    <body>
        <div id='target'>
            <p>Target Event Ada Disini!</p>
            <input id='txtInput' type='text' maxlength="50" />          
        </div>
        <div id='daftarEvent'></div>
        <script type="text/javascript">
            // Event handler yang akan dikerjakan bila checkbox diklik
            function checkboxEventHandler(e) {
                var target = document.getElementById('target');
                // khusus untuk focus, target adalah textbox
                if (['blur', 'focus', 'focusin', 'focusout'].indexOf(this.value) >= 0) {
                    target = document.getElementById('txtInput');
                }
                if (this.checked) {
                    console.log('Event handler untuk ' + this.value + ' didaftarkan!');
                    target.addEventListener(this.value, generalEventHandler, true);                                 
                } else {
                    console.log('Event handler untuk ' + this.value + ' dihapus!');             
                    target.removeEventListener(this.value, generalEventHandler, true);
                }
            }   

            // Event handler generic
            function generalEventHandler(e) {               
                console.log(e);
            }

            var events = ['blur', 'focus', 'focusin','focusout', 'click', 'dblclick', 
                          'mousedown', 'mouseenter', 'mouseleave', 'mouseout', 'mouseover', 
                          'mouseup', 'mousewheel', 'DOMMouseScroll', 'keydown', 'keypress', 'keyup'];
            var container = document.getElementById('daftarEvent');
            events.forEach(function(namaEvent) {
                // Membuat checkbox secara dinamis
                var checkBox = document.createElement('input');
                checkBox.id = namaEvent;
                checkBox.type = 'checkbox';
                checkBox.value = namaEvent;
                var baris = document.createElement('div');
                baris.appendChild(checkBox);
                baris.appendChild(document.createTextNode(namaEvent));
                container.appendChild(baris);
                checkBox.addEventListener('click', checkboxEventHandler, false);                              
            });                             
        </script>     
    </body>
</html>

Pada HTML di atas, saya bisa mendaftarkan sebuah event handler yang sama secara dinamis untuk seluruh event yang dipilih. Event handler tersebut akan menampilkan argumen Event ke console Firefox sehingga saya dapat memeriksa nilainya dengan mudah, seperti yang terlihat pada gambar berikut ini:

Menguji event di JavaScript

Menguji event di JavaScript

Pada HTML5, terdapat beberapa event lain, salah satunya adalah contextmenu. Event contextmenu akan dikerjakan bila pengguna browser akan menampilkan context menu (bisa melalui klik kanan atau melalui shortcut di keyboard). Untuk membatalkan context menu sehingga pengguna tidak akan memperoleh tampilan apa-apa saat klik kanan, saya dapat memanggil preventDefault() milik Event tersebut seperti pada kode program berikut ini:

document.addEventListener('contextmenu', function(e) {
    e.preventDefault();
}, false); 

Event contextmenu sering dipakai untuk membuat context menu berdasarkan CSS. Akan tetapi, bila memakai browser yang mendukung HTML5 Context Menu, sebaiknya menggunakan atribut contextmenu untuk menghasilkan menu bila pengguna men-klik kanan, seperti pada contoh berikut ini:

<!DOCTYPE html>
<html>
    <head>
        <meta charset='utf-8'/>
        <title>Context Menu</title> 
    </head>
    <body>
        <div style="width: 100%; height: 500px;" contextmenu='menuSingkat'>
        </div>
        <menu type='context' id='menuSingkat'>
            <menuitem label='Laporan' icon='menu_laporan.png'></menuitem>
            <menuitem label='Produk' icon='menu_produk.png'></menuitem>
            <menuitem label='Maintenance' icon='menu_maintenance.png'></menuitem>           
        </menu>           
    </body>
</html>
Membuat context menu dengan HTML5

Membuat context menu dengan HTML5

Event menarik lainnya pada HTML5 adalah beforeunload yang dapat dipakai untuk menampilkan sebuah pesan bila pengguna akan meninggalkan halaman. Event tersebut tidak dapat dipakai untuk membatalkan aksi pengguna yang ingin meninggalkan halaman karena keputusan tetap ada ditangan pengguna. Sebagai contoh, saya dapat menampilkan sebuah pesan pada saat pengguna menutup browser atau beralih ke situs lain, dengan menggunakan kode program berikut ini:

<!DOCTYPE html>
<html>
    <head>
        <meta charset='utf-8'/>
        <title>Halaman Saya</title> 
    </head>
    <body>
        <script type="text/javascript">
            window.addEventListener('beforeunload', function(event) {
                event.returnValue="Terima kasih sudah mengunjungi saya!";
            });
        </script>
    </body>
</html>

Pada FireFox, pesan yang saya berikan tidak akan ditampilkan seperti yang terlihat pada gambar berikut ini:

beforeunload pada Firefox

beforeunload pada Firefox

Sementara itu, pada Internet Explorer, pesan akan ditampilkan seperti yang terlihat pada gambar berikut ini:

beforeUnload pada Internet Explorer

beforeUnload pada Internet Explorer

Selain memakai event bawaan, beberapa browser termasuk Firefox juga mendukung CustomEvent. Untuk men-raise CustomEvent, saya menggunakan method dispatchEvent(). Untuk mendaftarkan event handler, saya tetap bisa menggunakan addEventListener() seperti biasa. Sebagai contoh, berikut ini adalah kode program JavaScript yang memakai CustomEvent:

<!DOCTYPE html>
<html>
    <head>
        <meta charset='utf-8'/>
        <title>Latihan</title>  
    </head>
    <body>
        <input id='selesai' type='button' value='Makanan Selesai Dimasak!' />
        <div id='output' style='font-size: xx-large; color: red;'></div>
        <script type="text/javascript">
            var tombol = document.getElementById('selesai');
            var output = document.getElementById('output');           

            /* menghasilkan event / raise event */          
            tombol.addEventListener('click', function() {
                var selesaiDimasakEvent = new CustomEvent('selesaiDimasak', 
                    {'detail': {'namaMenu': 'Ayam Goreng', 'waktu': '3 menit'}});
                output.dispatchEvent(selesaiDimasakEvent);              
            }, false);

            /* mendengarkan event / event listener */                           
            output.addEventListener('selesaiDimasak', function(e) {
                console.log(e);                                         
                output.innerHTML = e.detail.namaMenu + " sudah siap! (waktu masak: " + e.detail.waktu + ")";
            }, false);
        </script>
    </body>
</html>

Memakai Selectors API Di JavaScript

Salah satu library yang mempopulerkan CSS selector adalah jQuery. Dengan CSS selector seperti ".selected" atau "#buttonSimpan", developer dapat melakukan navigasi DOM dengan mudah. Tapi tidak selamanya butuh library tambahan untuk melakukan hal tersebut. Misalnya, saya bisa memakai Selectors API secara langsung dari JavaScript. Selectors API (http://www.w3.org/TR/selectors-api/) adalah spesifikasi yang memiliki tujuan untuk menciptakan dukungan CSS selector secara native (terintegrasi dalam browser). Karena tidak dibutuhkan library eksternal, maka kinerjanya akan lebih baik.

Hampir semua browser modern sudah memiliki fasilitas ini melalui method querySelector() dan querySelectorAll(). Perbedaan antara kedua method tersebut adalah method querySelector() hanya akan mengembalikan sebuah node sementara itu method querySelectorAll() akan mengembalikan seluruh node yang dicari dalam bentuk NodeList.

Sebagai latihan, anggap saja saya memiliki HTML dengan isi seperti berikut ini:

<!DOCTYPE html>
<html>
<head>
    <meta charset='utf-8'/>
    <title>Latihan</title>
</head>
<body>
    <h1>Latihan</h1>    
    <form id='frmUtama'>
        <div class='baris'>
            <label>Nama:</label>
            <input id='txtNama' type='text' class='komponen'/>          
        </div>
        <div class='baris'>
            <label>Jenis:</label>
            <input id='jenisManusia' type='radio' name='jenis' value='manusia' class='komponen'/>Manusia
            <input id='jenisMutant' type='radio' name='jenis' value='mutant' class='komponen'/>Mutant
        </div>
        <div class='baris'>
            <label>Kemampuan:</label>
            <select id='kemampuan' multiple=true size='3' class='komponen'>
                <option value='telepati'>Telepati</option>
                <option value='magnetisme'>Magnetisme</option>
                <option value='penyembuhan'>Penyembuhan</option>
                <option value='machinary'>Machinary</option>
            </select>
        </div>
    </form>
</body>
</html>

Untuk mendapatkan masing-masing komponen yang ada melalui id, saya dapat menggunakan JavaScript yang memakai CSS selector seperti pada gambar berikut ini:

Contoh penggunaan CSS selector

Contoh penggunaan CSS selector

Selain itu, saya juga bisa mencari setiap komponen tersebut berdasarkan CSS class (pada HTML5, ini setara document.getElementsByClassName()) seperti yang terlihat pada gambar berikut ini:

Contoh penggunaan CSS selector

Contoh penggunaan CSS selector

Terlihat bahwa bila menggunakan querySelector(), yang dikembalikan hanya sebuah Element saja. Sementara itu, querySelectorAll() akan mengembalikan sebuah NodeList yang berisi seluruh Element yang memiliki CSS class .komponen. Perhatikan bahwa NodeList bukan sebuah array sehingga ia tidak memiliki method seperti forEach() atau map(). Tapi saya dapat melakukan looping untuk seluruh elemen yang ada dengan menggunakan for...of seperti pada contoh berikut ini:

for (var item of document.querySelectorAll('.komponen')) {
    console.log(item.id);
}

// Output:
// "txtNama"
// "jenisManusia"
// "jenisMutant"
// "kemampuan"

Karena NodeList adalah sebuah object, maka saya tidak dapat memakai for...in yang akan melakukan enumerasi untuk seluruh property milik object. Hasil yang saya peroleh akan berbeda, seperti yang terlihat pada contoh berikut ini:

for (var item in document.querySelectorAll('.komponen')) {    
    console.log(item);
}

// Output:
// "0"
// "1"
// "2"
// "3"
// "item"
// "@@iterator"
// "length"

Saya dapat mencari Element berdasarkan atributnya, misalnya untuk mencari <input> yang memiliki atribut type='radio', saya dapat menggunakan JavaScript seperti berikut ini:

document.querySelectorAll("input[type='radio']");

Untuk mencari berbedasarkan beberapa atribut yang berbeda, saya dapat menggunakan CSS selector seperti berikut ini:

document.querySelectorAll("input[type='radio'][value='manusia']");

Untuk mendapatkan radio button yang sedang dipilih, saya dapat menggunakan CSS selector seperti berikut ini:

document.querySelector("input[name='jenis']:checked");

Untuk mendapatkan nilai yang dipilih dari <select>, saya dapat menggunakan CSS selector seperti:

document.querySelectorAll("#kemampuan > option:checked");
// Output: NodeList [<option>, <option>]

Bila ingin mengolah NodeList yang dihasilkan menjadi dalam bentuk string, saya dapat membuat JavaScript seperti berikut ini:

Array.prototype.slice.call(document.querySelectorAll("#kemampuan > option:checked"))
    .map(function(i) { return i.label; })
    .join(', ');

Satu hal yang menarik adalah bila saya mencoba seluruh kode program di atas pada Firefox Web Console, maka saya sebenarnya dapat menggunakan $() sebagai sinonim dari document.querySelector() dan $$() sebagai sinonim dari document.querySelectorAll(), seperti yang terlihat pada gambar berikut ini:

$ dan $$ di Firefox Web Console

$ dan $$ di Firefox Web Console

Selain Selector API Level 1, juga ada Selector API Level 2 (http://www.w3.org/TR/selectors-api2) yang menambahkan find() dan findAll() pada Element dan NodeList, serta matches() pada Element. Method find() dan findAll() akan mencari berdasarkan relative selector. Sayangnya, spesifikasi tersebut belum stabil dan belum di-implementasi-kan pada browser modern.