Apa Bedanya Adobe Illustrator Dengan Photoshop?

Pada suatu hari, saya meminta mahasiswa untuk merancang gambar yang dipakai pada pemograman game. Saya menyarankan mereka untuk menggunakan Adobe Illustrator. Seorang mahasiswa pencinta Photoshop kontan protes dan segera membela tool kesayangannya. Mengapa memakai Illustrator? Berbeda dengan Photoshop, Illustrator menyimpan data dalam bentuk vector sehingga scalable. Bila pengguna Photoshop cenderung menggunakan brush untuk mempoles gambar, maka pengguna Illustrator lebih sering memanipulasi bentuk (shape dan anchor point). Hal ini membuat Illustrator sangat nyaman dipakai untuk merancang gambar seperti kartun dan icon.

Pada Illustrator, terdapat 2 tool pemilihan utama yang sering digunakan, yaitu Selection Tool (V) dan Direct Selection Tool (A) seperti yang terlihat pada gambar berikut ini:

Selection dan Direct Selection Tool

Selection dan Direct Selection Tool

Dengan Direct Selection Tool, saya bisa memilih setiap titik (disebut sebagai anchor point) pada sebuah bentuk. Dengan demikian, saya bisa memanipulasi setiap anchor point yang ada. Sebagai contoh, saya bisa mengubah sebuah persegi panjang menjadi trapesium:

Memodifikasi anchor point

Memodifikasi anchor point

Saya juga bisa melakukan hal di atas dengan Photoshop! Untuk menghapus atau menambah anchor point di Photoshop, saya dapat memilih tool yang terletak di kotak yang sama dengan Pen Tool (P). Selain itu, Direct Selection Tool (A) di Photoshop tersembunyi karena terdapat di kotak yang sama dengan Path Selection Tool (A).

Illustrator memiliki menu khusus untuk memanipulasi path yang dapat diakses di Object, Path. Sebagai contoh, tidak ada cara cepat untuk membuat shape segitiga di versi Photoshop yang saya pakai. Sebaliknya, membuat segitiga sangat mudah di Illustrator. Saya mulai dengan membuat sebuah persegi panjang dan memilih anchor point di sudut kiri atas dan kanan atas. Setelah itu, seusai memakai menu Object, Path, Average…, saya akan memperoleh sebuah segitiga seperti pada gambar berikut ini:

Membuat segitiga

Membuat segitiga

Sama seperti di Photoshop, saya juga dapat mengatur stroke dan mengisi sebuah bentuk dengan gradien di Illustrator. Sebagai contoh, saya akan menghapus stroke dan mengisi bentuk trapesium yang saya buat dengan gradien seperti pada gambar berikut ini:

Mengisi trapesium dengan gradien

Mengisi trapesium dengan gradien

Satu hal yang tidak bisa saya lakukan di Photoshop secara cepat adalah menghaluskan ujung (rounding) pada trapesium ini. Photoshop memang menyediakan pengaturan penghalusan ujung untuk persegi panjang, tapi setelah dimodifikasi menjadi trapesium, saya tidak menemukan pengaturan tersebut lagi. Di Illustrator, saya dapat melakukan hal ini cukup dengan memilih menu Effect, Stylize, Round Corners… seperti pada gambar berikut ini:

Menghaluskan ujung trapesium

Menghaluskan ujung trapesium

Saya kemudian membuat dua trapesium lain di samping kiri dan kanan sehingga gambar saya terlihat seperti pada gambar berikut ini:

Membuat trapesium baru di sisi kiri dan kanan

Membuat trapesium baru di sisi kiri dan kanan

Sekarang, saya ingin sebuah trapesium di bagian bawah untuk melengkapi bagian yang kosong. Pada Photoshop, saya harus menggambarnya secara manual. Illustrator menawarkan cara lain melalui penggunaan Shape Builder Tool (Shift+M). Dengan Shape Builder Tool, saya bisa menggabungkan atau menghapus (dengan menahan tombol Alt) bagian kombinasi dari beberapa shape. Sebagai contoh, saya membuat sebuah persegi panjang dan meletakkannya di belakang (dengan memilih Object, Arrange, Send To Back) seperti pada gambar berikut ini:

Menambahkan sebuah persegi panjang

Menambahkan sebuah persegi panjang

Setelah itu, saya memilih seluruh shape yang ada, kemudian membuang bagian yang tidak dibutuhkan dengan menggunakan Shape Builder Tool:

Memakai shape builder tool

Memakai shape builder tool

Setiap shape di Illustrator bisa memiliki lebih dari satu fill atau stroke dengan effect-nya masing-masing, seperti yang terlihat pada gambar berikut ini:

Lebih dari 1 fill pada shape yang sama

Lebih dari 1 fill pada shape yang sama

Hal serupa dapat dicapai di Photoshop dengan cara yang lebih rumit, misalnya dengan membuat beberapa fill layer yang dikombinasikan dengan layer mask sehingga hanya berlaku untuk shape yang dikehendaki (bukan bagian gambar lainnya).

Langkah berikutnya, saya menambahkan sebuah persegi panjang baru sehingga gambar saya terlihat seperti pada gambar berikut ini:

Membuat persegi panjang baru

Membuat persegi panjang baru

Untuk memberikan ujung yang baru pada bagian bawah persegi panjang, saya dapat menggunakan Direct Selection Tool untuk memilih kedua anchor point di ujung kiri bawah dan kanan bawah, kemudian mengisi nilai corners seperti pada gambar berikut ini:

Menghaluskan hanya ujung kiri bawah dan kanan bawah

Menghaluskan hanya ujung kiri bawah dan kanan bawah

Hal serupa juga dapat dilakukan di Photoshop dengan cara yang berbeda, yaitu dengan mengisi nilai rounding di window Properties.

Bila pengguna Photoshop seringkali memoles gambar dengan menggunakan brush, maka pengguna Illustrator memoles kartun dengan potongan shape melalui fasilitas Pathfinder. Sebagai contoh, saya bisa menduplikasi persegi panjang yang baru dibuat sebanyak dua kali. Saya kemudian menggeser salah satu persegi panjang ke kanan sebanyak 1 piksel. Setelah itu, saya memilih kedua persegi panjang dan memilih Minus Front dari window Pathfinder untuk memperoleh sebuah shape baru seperti pada gambar berikut ini:

Memakai pathfinder

Memakai pathfinder

Saya kemudian melakukan hal yang sama untuk membuat shape di ujung kanan. Setelah itu, saya bisa memberikan gradiasi berbeda pada shape baru yang terbentuk di ujung kiri dan kanan, seperti pada gambar berikut ini:

Gradiasi berbeda di ujung sisi  kiri dan kanan

Gradiasi berbeda di ujung sisi kiri dan kanan

Karena hasil akhir di Illustrator adalah vector, saya tidak perlu khawatir gambar menjadi rusak saat diperbesar atau diperkecil. Vector aman untuk di-resize. Sebagai contoh, pada hasil akhir berikut ini, gambar dapat ditampilkan dengan baik pada beberapa ukuran yang berbeda:

Gambar dengan ukuran 256 x 256 piksel

Gambar dengan ukuran 256 x 256 piksel

Gambar dengan ukuran 128 x 128  piksel

Gambar dengan ukuran 128 x 128 piksel

Gambar dengan ukuran 64 x 64  piksel

Gambar dengan ukuran 64 x 64 piksel

Gambar dengan ukuran 48 x 48 piksel

Gambar dengan ukuran 48 x 48 piksel

Gambar dengan ukuran 32 x 32 piksel

Gambar dengan ukuran 32 x 32 piksel

Melakukan Pengujian jQuery Ajax Di QUnit

Sesuai dengan namanya, unit testing adalah pengujian yang dilakukan pada satuan atau unit terkecil dari kode program. Unit lainnya sebisa mungkin dianggap konstan atau tidak berubah. Dengan demikian, bila terjadi kegagalan pengujian pada sebuah unit, developer bisa yakin bahwa penyebab kegagalan terjadi pada unit tersebut (bukan pada unit lain yang mungkin dipanggil). Untuk membuat unit lain menjadi konstan, developer dapat menerapkan teknik seperti mocking dan stubbing.

Sebagai contoh, anggap saja saya perlu menguji kode program JavaScript seperti berikut ini:

function simpan(data, callback) {      
  $.ajax({
    url: 'simpan',
    type: 'POST',
    data: JSON.stringify(data),
    contentType: 'application/json; charset=utf-8',
    success: function(hasil) {
      if (hasil.ok) {
        data.id = hasil.id;
      }
      callback();          
    }
  });      
}

Function di atas akan memakai $.ajax dari jQuery untuk mengirim sebuah object yang hendak disimpan dalam bentuk JSON ke server. Setelah nilai dikembalikan dari server secara asynchronous, maka function callback() akan dikerjakan dimana argumen hasil berisi informasi dari server. Bila proses penyimpanan sukses, maka nilai hasil.ok adalah true. Selain itu, atribut id dari object akan di-isi dengan sebuah nilai yang dihasilkan secara otomatis oleh server. Bila server gagal melakukan penyimpanan, maka nilai hasil.ok adalah false.

Untuk memastikan apakah kode program yang saya buat sudah memenuhi skenario yang ditetapkan, saya perlu membuat pengujian QUnit dalam bentuk sebuah file HTML, misalnya seperti berikut ini:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Latihan</title>  
  <script src="jquery.js"></script>  
  <link rel="stylesheet" href="qunit.css" media="screen">  
  <script src="qunit.js"></script>      
  <script>
    function simpan(data, callback) {      
      $.ajax({
        url: 'simpan',
        type: 'POST',
        data: JSON.stringify(data),
        contentType: 'application/json; charset=utf-8',
        success: function(hasil) {
          if (hasil.ok) {
            data.id = hasil.id;
          }
          callback();          
        }
      });      
    }

    QUnit.test('simpan(data) sukses', function(assert) {
      var mahasiswa = { nama: 'Solid Snake', usia: 27 };
      simpan(mahasiswa, function(hasil) {
        assert.equal(hasil.ok, true, 'Nilai hasil.ok harus true bila penyimpanan sukses')
        assert.ok(mahasiswa.id !== undefined, 'Nilai id harus terisi');  
      });            
    });

    QUnit.test('simpan(data) gagal', function(assert) {
      var mahasiswa = { nama: 'Solid Snake', usia: 27 };
      simpan(mahasiswa, function(hasil) {
        assert.equal(hasil.ok, false, 'Nilai hasil.ok harus false bila penyimpanan gagal');
        assert.ok(mahasiswa.id === undefined, 'Nilai id harus masih kosong');        
      });            
    });
  </script>    
</head>
<body>  
  <div id="qunit"></div>  
</body>
</html>

Akan tetapi, bila saya menjalankan pengujian dengan membuka file HTML tersebut di browser, saya akan menemukan pesan kegagalan seperti pada gambar berikut ini:

Pengujian yang gagal

Pengujian yang gagal

Hal ini terjadi karena request Ajax tidak dilakukan sehingga callback() tidak pernah dipanggil. Untuk membuat pengujian sukses, saya perlu memperoleh hasil kembalian dari server. Akan tetapi karena ini adalah unit test, saya tidak ingin ada pemanggilan ke server secara nyata pada saat pengujian dilakukan. Oleh sebab itu, saya perlu mensimulasikan kembalian dari server.

Beruntungnya, karena JavaScript adalah bahasa yang dinamis, melakukan stubbing secara mudah bukanlah hal yang mustahil. Salah satu cara yang paling mudah adalah mengganti method jQuery.ajax() dengan method yang langsung memanggil success. Sebagai contoh, saya bisa mengubah test case saya menjadi seperti berikut ini:

QUnit.test('simpan(data) sukses', function(assert) {      
  var mahasiswa = { nama: 'Solid Snake', usia: 27 };
  jQuery.ajax = function(args) {
    args.success({ok: true, id: 999});
  } 
  simpan(mahasiswa, function(hasil) {
    assert.equal(hasil.ok, true, 'Nilai hasil.ok harus true bila penyimpanan sukses')
    assert.ok(mahasiswa.id !== undefined, 'Nilai id harus terisi');  
  });            
});

QUnit.test('simpan(data) gagal', function(assert) {
  var mahasiswa = { nama: 'Solid Snake', usia: 27 };
  jQuery.ajax = function(args) {
    args.success({ok: false});
  }
  simpan(mahasiswa, function(hasil) {
    assert.equal(hasil.ok, false, 'Nilai hasil.ok harus false bila penyimpanan gagal');
    assert.ok(mahasiswa.id === undefined, 'Nilai id harus masih kosong');        
  });            
});

Pada kasus sukses, saya membuat seolah-olah server mengirimkan respon berupa {ok: true, id: 999}. Pada kasus gagal, saya menganggap server mengirimkan respon berupa {ok: false}. Sekarang, bila saya menjalankan pengujian, semuanya akan sukses seperti pada gambar berikut ini:

Pengujian sukses setelah men-stub method jQuery.ajax()

Pengujian sukses setelah men-stub method jQuery.ajax()

Melakukan Front-End Testing Dengan Codeception

Pada artikel Melakukan Front-End Testing Dengan Selenium Tanpa “Mengotori” Database, saya mencoba melakukan pengujian halaman web secara otomatis. Pengujian dilakukan dengan mengisi database dengan data yang konsisten lalu mensimulasikan akses pada browser dan memastikan hasilnya sesuai harapan. Pada artikel ini, saya akan melakukan hal yang sama tetapi dengan menggunakan Codeception. Codeception adalah sebuah ‘framework’ pengujian yang mempermudah menulis kode program pengujian. Dukungan yang dimiliki oleh Codeception ditawarkan dalam bentuk module seperti Selenium2, Webdriver, PhpBrowser, dan lainnya.

Sebagai latihan, saya akan menguji sebuah web sederhana dimana terdapat sebuah halaman yang hanya bisa ditampilkan setelah pengguna login. Proyek web tersebut dibuat dengan menggunakan fasilitas Auth yang disediakan oleh Laravel.

Untuk men-install Codeception pada proyek Laravel, saya akan menggunakan perintah Composer berikut ini:

C:\proyek> composer require "codeception/codeception:*"

Composer akan men-download Codeception pada folder vendor/codeception.

Langkah pertama untuk memakai Codeception adalah menghasilkan file konfigurasi dengan memberikan perintah seperti berikut ini:

C:\proyek> vendor\bin\codecept bootstrap app

Perintah di atas akan menyebabkan Codeception membuat beberapa file baru yang dibutuhkan oleh Codeception di folder app\tests. Folder acceptance, functional dan unit masing-masing mewakili jenis pengujian. Istilah yang dipakai untuk front-end testing disini adalah acceptance testing. Ini yang akan saya pakai nantinya.

Sebelumnya, saya perlu mencari file file acceptance.suite.yml dan mengubah nilai url agar sesuai dengan lokasi URL aplikasi yang hendak diuji. Ini adalah salah satu konfigurasi yang dibutuhkan oleh PHPBrowser. Secara default, Codeception akan memakai module PHPBrowser untuk front-end testing. Karena aplikasi yang saya uji tidak mengandung JavaScript, maka ini adalah pilihan yang masuk akal. Bila hendak menguji interaksi melalui JavaScript pada aplikasi, saya harus memakai module Selenium2.

Untuk membuat sebuah file pengujian baru, saya akan memberikan perintah berikut ini:

C:\proyek> vendor\bin\codecept generate:cept acceptance Login -c app

Perintah di atas akan membuat sebuah file baru bernama LoginCept.php di folder app\tests\acceptance. Saya dapat membuat kode program yang mewakili pengujian di file ini. Sebagai contoh, saya dapat mengubah isinya menjadi seperti berikut ini:

<?php
$I = new WebGuy($scenario);

$I->wantTo('display home page');
$I->amOnPage('home/welcome');
$I->dontSee('Selamat datang');
$I->see('Login');

$I->wantTo('login');
$I->fillField('nama', 'snake');
$I->fillField('password', '12345');
$I->click('Login');
$I->amOnPage('home/welcome');
$I->see('Selamat datang, snake');
$I->seeLink('logout');

Kode program di atas sangat mudah dipahami, bukan? Untuk menjalankannya, saya akan memberikan perintah berikut ini:

C:\proyek> vendor\bin\codecept run

Saya akan memperoleh hasil seperti berikut ini:

Codeception PHP Testing Framework v1.8.5
Powered by PHPUnit 3.7.37 by Sebastian Bergmann.

Acceptance Tests (1) ------------------------
Trying to login (LoginCept.php)         Ok
---------------------------------------------

Functional Tests (0) ------------------------
---------------------------------------------

Unit Tests (0) ------------------------------
---------------------------------------------


Time: 858 ms, Memory: 6.50Mb

OK (1 test, 4 assertions)

Salah masalah dengan pendekatan skenario seperti di atas adalah saya harus memiliki 1 file PHP per skenario. Sebagai contoh, walaupun pada kode program pengujian, saya memiliki 2 buah wantTo(), hasil pengujian hanya menampilkan yang terakhir kali saja (Trying to login). Untuk mengatasi hal seperti ini, saya dapat mendeklarasi pegujian dalam bentuk class. Untuk itu, saya membuat skenario baru dengan memberikan perintah seperti berikut ini:

C:\proyek> vendor\bin\codecept generate:cest acceptance Auth -c app

Perhatikan bahwa pada perintah di atas, saya memakai generate:cest sementara pada perintah sebelumnya, saya memakai generate:cept. Perintah tersebut akan menghasilkan sebuah file baru bernama AuthCest.php di folder acceptance. File tersebut berisi class AuthenticationCest yang akan saya ubah menjadi seperti berikut ini:

<?php
use WebGuy;

class AuthCest
{

    public function visit(WebGuy $I) {
        $I->wantTo('display home page as guest');
        $I->amOnPage('home/welcome');
        $I->dontSee('Selamat datang');
        $I->see('Login');
    }

    /**     
     * @before login
     */
    public function wrongLogin(WebGuy $I) {
        $I->wantTo('login with wrong password');
        $I->lookForwardTo('see error when entering wrong password');
        $I->amOnPage('login/show');
        $I->fillField('nama', 'snake');
        $I->fillField('password', 'wrong password');
        $I->click('Login');                
        $I->seeInField('Nama', 'snake');
        $I->see('Terjadi kegagalan');      
    }

    public function login(WebGuy $I) {
        $I->wantTo('login');
        $I->amOnPage('login/show');
        $I->fillField('nama', 'snake');
        $I->fillField('password', '12345');
        $I->click('Login');
        $I->amOnPage('home/welcome');
        $I->see('Selamat datang, snake');
        $I->seeLink('logout');
    }

    /**     
     * @after login
     */
    public function logout(WebGuy $I) {
        $I->wantTo('logout');
        $I->seeLink('logout');
        $I->click('logout');
        $I->dontSee('Selamat datang');
        $I->see('Nama');
        $I->see('Password');
        $I->see('Login');
    }

}

Bila saya menjalankan pengujian, kali ini saya akan memperoleh hasil seperti:

Codeception PHP Testing Framework v1.8.5
Powered by PHPUnit 3.7.37 by Sebastian Bergmann.

Acceptance Tests (4) ------------------------------------------------
Trying to display home page as guest (AuthCest.visit)           Ok
Trying to login with wrong password (AuthCest.wrongLogin)       Ok
Trying to login (AuthCest.login)                                Ok
Trying to logout (AuthCest.logout)                              Ok
---------------------------------------------------------------------

Functional Tests (0) ------------------------
---------------------------------------------

Unit Tests (0) ------------------------------
---------------------------------------------


Time: 1.59 seconds, Memory: 6.75Mb

OK (4 tests, 15 assertions)

Selain menjalankan pengujian, saya juga bisa mengubah kode program pengujian menjadi sesuatu yang lebih mudah dibaca (oleh mereka yang bukan developer) dengan memberikan perintah berikut ini:

C:\proyek> vendor\bin\codecept generate:scenarios acceptance -c app

Hasilnya dapat dilihat pada folder app/tests/_data/scenarios/acceptance. Saya akan menemukan file seperti Auth_Cest.login.txt, Auth_Cest.logout.txt, dan sebagainya. Sebagai contoh, bila saya melihat isi file Auth_Cest.login.txt, saya akan menemukan hasil seperti berikut ini:

I WANT TO LOGIN

I am on page "login/show"
I fill field "nama","snake"
I fill field "password","12345"
I click "Login"
I am on page "home/welcome"
I see "Selamat datang, snake"
I see link "logout"

Unit Testing Di JavaScript Dengan QUnit

Pada artikel ini, saya akan mencoba menggunakan QUnit untuk melakukan unit testing pada kode program JavaScript. Berbeda dengan Java dimana unit adalah sebuah class, pada JavaScript, unit adalah sebuah function. Salah satu tool yang dapat dipakai untuk melakukan unit testing pada JavaScript adalah QUnit. Tool ini juga dipakai untuk melakukan pengujian pada proyek jQuery dan jQuery UI. Untuk memakai QUnit, saya dapat men-download-nya di http://qunitjs.com.

Sebagai latihan, saya akan membuat sebuah library sederhana yang mengimplementasikan fungsi require() seperti pada spesifikasi module oleh CommonJS. JavaScript (sebelum ES6) tidak memiliki konsep module. Oleh sebab itu, developer JavaScript seringkali mengalami masalah berkaitan dengan penggunaan banyak library berbeda dan kesalahan penamaan yang sama. Pada Java, hal ini diatasi melalui konsep package dan keyword import. ECMAScript 6 nanti juga akan memperkenalkan konsep module pada JavaScript. Saat ini, untuk sementara ada beberapa spesifikasi yang dapat dipakai dalam mengatur kode program JavaScript agar lebih standar dalam mengelola module. Salah satunya adalah yang dibuat oleh CommonJS (http://www.commonjs.org/specs/modules/1.0/). Yang ditawarkan adalah koleksi design pattern yang bisa diterapkan oleh setiap developer JavaScript. Karena ini hanya latihan, saya tidak akan membuat sesuatu yang mengikuti seluruh yang ada di spesifikasi (selain itu, implementasi CommonJS sudah banyak, salah satunya adalah Node.js).

Kali ini saya akan membuat unit test dengan mengikuti filosofi test driven development (TDD). Dengan TDD, saya mulai dengan membuat test case terlebih dahulu baru kemudian membuat implementasi kode programnya. Yup! Membuat unit test dulu baru membuat kode program! Saya pernah mendengar cerit bahwa seorang dosen S2 yang sedang mendidik dosen S1 guna sertifikasi mengatakan bahwa harus ada GUI (tampilan atau output) terlebih dahulu baru bisa menguji kode program. Tentu saja ia salah. Lalu, bagaimana cara mengujinya?

Saya akan mulai dengan membuat test case yang mengikuti spesifikasi CommonJS dengan membuat 2 file JavaScript yang mewakili module, yaitu moduleA.js dan moduleB.js. Isi dari moduleA.js adalah:

exports.foo = require("moduleB").foo;

Sementara itu, isi dari moduleB.js adalah:

exports.foo = function() {};

Pada spesifikasi CommonJS, fungsi require() dipakai untuk menyertakan modul lain. Sebuah modul yang ingin men-export function atau nilai agar dapat dipakai oleh modul lainnya harus mengisinya pada exports yang selalu tersedia. Pada contoh di atas, moduleA akan memakai moduleB dimana function foo() dari moduleA persis sama seperti function foo() dari moduleB.

Berikutnya, saya akan membuat sebuah halaman HTML sederhana yang mewakili sebuah test case dengan nama test.html yang isinya seperti berikut ini:

<!doctype html>
<html>
<head>
    <title>Latihan QUnit</title>
    <link rel="stylesheet" href="qunit-1.14.0.css" />
    <script src="qunit-1.14.0.js"></script>
    <script src="myrequire.js"></script>
    <script>
        test("Menguji module", function() {
            var moduleA = require("moduleA");
            var moduleB = require("moduleB");
            ok(moduleA!=undefined, "moduleA harus punya nilai");
            ok(moduleB!=undefined, "moduleB harus punya nilai");
        });
        test("Menguji hasil export di module", function() {
            var moduleA = require("moduleA");
            var moduleB = require("moduleB");
            ok(moduleA.foo!=undefined, "moduleA.foo harus ada");          
            ok(moduleB.foo!=undefined, "moduleB.foo harus ada");
        });
        test("Menguji function yang di-export", function() {
            var moduleA = require("moduleA");
            var moduleB = require("moduleB");     
            equal(moduleA.foo, moduleB.foo);
        });
    </script>
</head>
<body>
    <div id="qunit"></div>
</body>
</html>

Bila saya menjalankan HTML di atas, saya akan menemui tampilan seperti pada gambar berikut ini:

Hasil awal pengujian menunjukkan semua test gagal

Hasil awal pengujian menunjukkan semua test gagal

Jangan kaget! Di TDD, pada awalnya seluruh test akan gagal. Tujuan dari TDD adalah bagaimana membuat test yang gagal tersebut menjadi benar dan sukses (ini adalah arti kata test-driven dimana pemicu pembuatan kode program adalah test case). Progress atau perkembangan proyek juga bisa dilihat berdasarkan jumlah test yang sukses atau gagal.

Sebagai langkah berikutnya, saya akan membuat sebuah file JavaScript baru bernama myrequire.js yang mewakili kode program yang hendak diuji dengan isi seperti berikut ini:

(function(global) {
    function require(moduleId) {            
        var module = {};
        return module;
    }       

    global.require = require;
}(this));

Saya kemudian menambahkan baris berikut ini pada test.html:

<script src="myrequire.js"></script>

Sekarang, bila saya menampilkan file tersebut pada browser, akan ada 1 test case yang sukses, namun yang lainnya masih gagal, seperti yang terlihat pada gambar berikut ini:

Hasil pengujian menunjukkan test case pertama sudah sukses

Hasil pengujian menunjukkan test case pertama sudah sukses

Saya kembali melakukan perubahan di myrequire.js agar test case berikutnya menjadi sukses. Sebagai contoh, saya mengubahnya menjadi seperti berikut ini:

(function(global) {
    function require(moduleId) {            
        var module = {};
        module.foo = function() {};
        return module;
    }       

    global.require = require;
}(this));

Sekarang, bila saya menjalankan pengujian QUnit, saya akan memperoleh hasil seperti pada gambar berikut ini:

Hasil pengujian menunjukkan test case pertama dan kedua sudah sukses

Hasil pengujian menunjukkan test case pertama dan kedua sudah sukses

Sudah semakin baik bukan? Hanya saja, agar semua test sukses, kode program saya perlu membaca file JavaScript yang mewakili module. Untuk itu, saya perlu memperkenalkan sebuah function baru yang membaca file JavaScript dengan menggunakan XHR. Seperti biasa, pada TDD, sebelum membuat kode program, saya terlebih dahulu membuat test case-nya. Untuk itu, saya membuat file testBacaFile.html yang isinya seperti berikut ini:

<!doctype html>
<html>
<head>
    <title>Latihan QUnit</title>
    <link rel="stylesheet" href="qunit-1.14.0.css" />
    <script src="qunit-1.14.0.js"></script>
    <script src="myrequire.js"></script>
    <script>
        test("Membaca module dalam direktori yang sama", function() {         
            equal('exports.foo = require("moduleB").foo;', bacaFile("moduleA.js"));
            equal('exports.foo = function() {};', bacaFile("moduleB.js"));
        });             
        test("Membaca module yang tidak ada", function() {
            throws(function() {
                bacaFile("entahDimana.js");
            });
        });
    </script>
</head>
<body>
    <div id="qunit"></div>
</body>
</html>

Pada pengujian di atas, saya memastikan bahwa bacaFile() akan mengembalikan string yang sesuai dengan isi file JavaScript. Bila saya berusaha membaca file yang tidak ada, function tersebut harus mengembalikan sebuah exception. Pada QUnit, saya menggunakan throws() untuk memeriksa apakah ada exception yang dikembalikan (dan saya juga bisa memeriksa jenis exception yang dikembalikan bila perlu).

Seperti biasa, bila saya menjalankan pengujian di atas, saya akan menemukan kegagalan karena function bacaFile() belum ada. Oleh sebab itu, saya perlu membuatnya dengan mengubah kode program myrequire.js menjadi seperti berikut ini:

(function(global) {

    function require(moduleId) {            
        var module = {};
        module.foo = function() {};
        return module;
    }       

    function bacaFile(namaFile) {
        var xhr = new XMLHttpRequest();
        xhr.open("GET", namaFile, false); 
        return xhr.responseText;        
    };

    global.require = require;
}(this));

Saya kemudian kembali menjalankan unit test di testBacaFile.html, tapi saya kembali mendapatkan pesan kesalahan yang sama bahwa bacaFile is not defined. Mengapa demikian? Hal ini karena function bacaFile() berada dalam closure sehingga tidak dapat diakses dari luar (termasuk oleh test case). Saya tidak bisa menguji sesuatu yang tidak bisa saya akses. Untuk itu, saya akan mengganti penggunakan closure menjadi sesuatu yang lebih OOP dengan mengubah kode program di myrequire.js menjadi seperti berikut ini:

function MyRequire(exports) {
    this.exports = exports;
};

MyRequire.prototype = {

    require: function require(moduleId) {           
        var module = {};
        module.foo = function() {};
        return module;
    },

    bacaFile: function bacaFile(namaFile) {
        var xhr = new XMLHttpRequest();
        xhr.open("GET", namaFile, false); 
        return xhr.responseText;        
    },

}

this.myRequire = new MyRequire(typeof exports === 'object' && exports || this);
this.require = function(s) { return this.myRequire.require(s); };

Walaupun cara diatas menyebabkan ada variabel global myRequire, setidaknya ini akan mempermudah pengujian yang merupakan kriteria penting dalam TDD. Sekarang, saya bisa mengubah kode program di testBacaFile.html menjadi seperti berikut ini:

<!doctype html>
<html>
<head>
    <title>Latihan QUnit</title>
    <link rel="stylesheet" href="qunit-1.14.0.css" />
    <script src="qunit-1.14.0.js"></script>
    <script src="myrequire.js"></script>
    <script>
        test("Membaca module dalam direktori yang sama", function() {         
            equal('exports.foo = require("moduleB").foo;', myRequire.bacaFile("moduleA.js"));
            equal('exports.foo = function() {};', myRequire.bacaFile("moduleB.js"));
        }); 
        test("Membaca module yang tidak ada", function() {
            throws(function() {
                myRequire.bacaFile("entahDimana.js")
            });
        });         
    </script>
</head>
<body>
    <div id="qunit"></div>
</body>
</html>

Tapi bila saya menampilkan HTML tersebut, kode program saya masih salah. File masih belum dibaca dengan benar. Mengapa demikian? Frustasi adalah hal biasa bagi developer. Setidaknya TDD membantu saya mengetahui bahwa ada yang salah dengan kode program sebelum library ini di-integrasi-kan pada proyek web yang lebih kompleks lagi.

Saya akhir menemukan bahwa saya lupa memanggil xhr.send()! Pantas saja gagal, saya segera menambahkan xhr.send() setelah xhr.open() di function bacaFile(). Sekarang, bila saya menampilkan testBacaFile.html, maka test case pertama akan sukses seperti pada gambar berikut ini:

Hasil pengujian menunjukkan test pertama sudah sukses

Hasil pengujian menunjukkan test pertama sudah sukses

Berikutnya, bagaimana caranya agar test case kedua tidak gagal? Saya harus men-throw sesuatu bila terjadi kegagalan saat membaca file. Sebagai contoh, saya mengubah kode program bacaFile() di myrequire.js menjadi seperti berikut ini:

...
bacaFile: function bacaFile(namaFile) {
    var xhr = new XMLHttpRequest();     
    xhr.open("GET", namaFile, false);
    xhr.send();
    if (xhr.status===200) {
        return xhr.responseText;
    } else {
        throw "Can't read file: " + namaFile;
    }       
},
...

Sekarang, seluruh pengujian untuk method bacaFile() akan sukses seperti yang terlihat pada gambar berikut ini:

Hasil pengujian menunjukkan seluruh test case sudah sukses

Hasil pengujian menunjukkan seluruh test case sudah sukses

Dampak dari unit test adalah membuat saya lebih percaya diri. Saya setidaknya bisa yakin bahwa bacaFile() sudah bekerja sesuai dengan yang diharapkan.

Perkembangannya terasa dengan jelas, bukan? Sampai disini saya bisa istirahat sejenak, menikmati segelas teh hangat atau jalan-jalan sebentar. Setelah saya kembali, saya tahu apa yang harus dilakukan. Masih ada test case yang gagal 🙂

Agar semua test case di test.html sukses, saya mengubah kode program pada myrequire.js menjadi seperti berikut ini:

function MyRequire(exports) {
    this.exports = exports;
    this.modules = new Map();
};

MyRequire.prototype = {

    require: function require(moduleId) {           
        var module = {id: moduleId, exports: {}};           

        // TIdak melakukan loading bila sudah pernah di-load    
        if (this.modules.has(moduleId)) return this.modules.get(moduleId).exports;

        // Membaca file JS yang mewakili module
        new Function("module", "exports", this.bacaFile(moduleId + ".js"))
            .call(this.exports, module, module.exports);
        this.modules.set(moduleId, module);                     

        return module.exports;
    },

    bacaFile: function bacaFile(namaFile) {
        var xhr = new XMLHttpRequest();     
        xhr.open("GET", namaFile, false);
        xhr.send();
        if (xhr.status===200) {
            return xhr.responseText;
        } else {
            throw "Can't read file: " + namaFile;
        }       
    },

}

this.myRequire = new MyRequire(typeof exports === 'object' && exports || this);
this.require = function(s) { return this.myRequire.require(s); };

Sekarang, bila saya menjalankan kode program, seluruh test yang ada akan sukses seperti pada yang terlihat pada gambar berikut ini:

Hasil pengujian menunjukkan seluruh test case sudah sukses

Hasil pengujian menunjukkan seluruh test case sudah sukses

Sebagai informasi, QUnit juga mendukung pola module di CommonJS sehingga saya dapat memanggilnya melalui require(). Sebagai contoh, saya akan membuat unit test lagi yang diambil dari halaman dokumentasi CommonJS di http://www.commonjs.org/specs/modules/1.0/. Saya membuat file math.js yang isinya seperti berikut ini (sesuai dengan example di website tersebut):

exports.add = function() {
    var sum = 0, i = 0, args = arguments, l = args.length;
    while (i < l) {
        sum += args[i++];
    }
    return sum;
}

Kemudian, saya membuat file increment.js yang isinya seperti berikut ini (sesuai dengan example di website CommonJS):

var add = require('math').add;
exports.increment = function(val) {
    return add(val, 1);
};

Setelah itu, saya akan membuat unit test di file testSample.html yang isinya seperti berikut ini:

<!doctype html>
<html>
<head>
    <title>Latihan QUnit</title>
    <link rel="stylesheet" href="qunit-1.14.0.css" /> 
    <script src="myrequire.js"></script>
    <script>
        var QUnit = require('qunit-1.14.0');

        test('Sample Common JS', function() {
            var inc = require('increment').increment;
            var a = 1;
            equal(2, inc(a));
            var b = 5;
            equal(6, inc(b));
        });             
    </script>
</head>
<body>
    <div id="qunit"></div>
</body>
</html>

Bila saya menampilkan halaman di atas pada browser, saya akan memperoleh tampilan yang menunjukkan bahwa kode program saya bekerja sesuai harapan seperti yang terlihat pada gambar berikut ini:

Hasil pengujian menunjukkan seluruh test case sudah sukses

Hasil pengujian menunjukkan seluruh test case sudah sukses

Cara di atas, dimana saya membuat unit test setelah selesai membuat kode program, adalah sesuatu yang melanggar prinsip test driven development (TDD). Pada TDD, saya harus membuat atau mengembangkan unit test terlebih dahulu sebelum membuat kode program. Tapi tentu saja menambah unit test belakangan bukan sesuatu yang salah. Semakin banyak test case yang ada akan membuat sebuah proyek menjadi semakin teruji dan handal.

Memakai Grunt Untuk Mengelola Proyek JavaScript

Saya sudah biasa memakai Gradle atau Maven untuk mengelola proyek Java. Lalu bagaimana dengan proyek JavaScript? Tentu saja tool serupa tidak begitu berguna bila dipakai pada web yang hanya sekedar memakai JavaScript di dalam HTML. Tapi untuk sebuah proyek library JavaScript (seperti jQuery), penggunaan tool otomatis untuk mengelola proyek akan sangat berguna. Grunt adalah salah satu tool yang dirancang untuk keperluan tersebut. Contoh proyek open-source JavaScript yang memakai Grunt adalah jQuery. Pada artikel ini, saya akan mencoba memakai Grunt untuk mengelola proyek JavaScript sederhana.

Untuk men-install Grunt, saya perlu memakai npm. npm adalah package manager yang berjalan pada platform Node.js. Ini adalah sesuatu yang memiliki fungsi mirip seperti Composer di PHP. Pada platform Linux, saya memberikan perintah berikut ini untuk men-install npm:

$ sudo apt-get install nodejs
$ sudo apt-get install npm

Setelah npm ter-install, saya dapat men-install Grunt CLI dengan menggunakan perintah berikut ini:

$ sudo npm install -g grunt-cli

Perintah di atas akan men-install perintah grunt-cli pada lokasi /usr/local (dapat diatur dengan mengubah nilai konfigurasi prefix). Dengan demikian, saya dapat memanggil perintah grunt dari mana saja.

Untuk menghasilkan template proyek JavaScript secara cepat, saya akan menggunakan grunt-init. Tapi sebelumnya, saya perlu men-install-nya terlebih dahulu dengan memberikan perintah berikut ini:

$ sudo npm install -g grunt-init

Untuk memakai grunt-init, saya perlu men-download minimal sebuah template yang akan dijadikan sebagai patokan struktur direktori awal. Lokasi template secara default terletak di folder ~/.grunt-init/. Sebagai latihan, saya akan memakai template grunt-init-commonjs dengan memberikan perintah berikut ini:

$ git clone https://github.com/gruntjs/grunt-init-commonjs.git ~/.grunt-init/commonjs

Bila git belum ter-install, saya perlu memberikan perintah sudo apt-get install git. Perintah di atas akan menyalin sebuah template untuk proyek JavaScript yang bersifat umum.

Saya siap untuk membuat proyek baru. Tapi sebelumnya, saya perlu mengatasi sebuah permasalahan kecil terlebih dahulu. Pada distro Linux yang saya pakai, perintah seperti grunt atau grunt-init tidak akan bisa dijalankan, malah muncul kesalahan seperti berikut ini:

$ grunt --version
/usr/bin/env: node: No such file or directory

Hal ini terjadi karena konflik nama antara package untuk Node.js dengan package node (Amateur Packet Radio Node Program) sehingga binary Node.js yang seharusnya adalah node terpaksa mengalah dan diganti nama menjadi nodejs. Karena banyak script yang mengharapkan nama binary Node.js berupa node, saya perlu me-rename nodejs menjadi node, atau agar aman, saya dapat membuat symbolic link seperti berikut ini:

$ sudo ln -s /usr/bin/nodejs /usr/bin/node

Sekarang, saya siap untuk membuat sebuah proyek JavaScript baru dengan memberikan perintah berikut ini:

$ mkdir myUtils
$ cd myUtils
$ grunt-init commonjs
Running "init:commonjs" (init) task
This task will create one or more files in the current directory, based on the
environment and the answers to a few questions. Note that answering "?" to any
question will show question-specific help and answering "none" to most questions
will leave its value blank.

Please answer the following:
[?] Project name (myUtils) 
[?] Description (The best project ever.) My reusable JS APIs.
[?] Version (0.1.0) 
[?] Project git repository (git://github.com/snake/myUtils.git) 
[?] Project homepage (https://github.com/snake/myUtils) 
[?] Project issues tracker (https://github.com/snake/myUtils/issues) 
[?] Licenses (MIT) Apache
[?] Author name (none) Solid Snake
[?] Author email (none) solid@snake.com
[?] Author url (none) https://thesolidsnake.wordpress.com
[?] What versions of node does it run on? (>= 0.10.0) 
[?] Main module/entry point (lib/myUtils) 
[?] Npm test command (grunt nodeunit) 
[?] Do you need to make any changes to the above before continuing? (y/N) N

Writing .gitignore...OK
Writing .jshintrc...OK
Writing Gruntfile.js...OK
Writing README.md...OK
Writing lib/.jshintrc...OK
Writing lib/myUtils.js...OK
Writing test/myUtils_test.js...OK
Writing package.json...OK

Initialized from template "commonjs".
You should now install project dependencies with npm install. After that, you
may execute project tasks with grunt. For more information about installing
and configuring Grunt, please see the Getting Started guide:

http://gruntjs.com/getting-started

Done, without errors.

grunt-init akan menanyakan beberapa pertanyaan. Saya bisa mengisinya atau menerima nilai default dengan menekan tombol Enter. Setelah pertanyaan selesai dijawab, grunt-init akan membuat sebuah proyek baru dengan struktur seperti berikut ini:

$ tree -a
.
|--- .gitignore
|--- Gruntfile.js
|--- .jshintrc
|--- lib
|    |--- .jshintrc
|    |--- myUtils.js
|--- package.json
|--- README.md
|--- test
     |--- myUtils_test.js

2 directories, 8 files

File Gruntfile.js adalah file wajib yang dibutuhkan untuk bekerja dengan Grunt. File package.json dipakai oleh npm untuk men-download modul lain yang dibutuhkan. grunt-init juga sudah membuat file lib\myUtils.js yang nantinya akan berisi kode program saya. Selain itu, juga ada file lib\myUtils_test.js yang berisi unit test untuk menguji program JavaScript yang ada.

Bila saya membuka file package.json, saya akan memperoleh isi "devDependencies" yang terlihat seperti berikut ini:

...
"devDependencies": {
    "grunt-contrib-concat": "~0.3.0",
    "grunt-contrib-uglify": "~0.2.0",
    "grunt-contrib-jshint": "~0.6.0",
    "grunt-contrib-nodeunit": "~0.2.0",
    "grunt-contrib-watch": "~0.4.0",
    "grunt": "~0.4.5"
},
...

Konfigurasi di atas menunjukkan ketergantungan pada beberapa plugin Grunt. Salah satu kelebihan Grunt adalah ia memiliki banyak plugin seperti yang terdaftar di http://gruntjs.com/plugins. Sebagai contoh, grunt-contrib-concat adalah plugin Grunt untuk menggabungkan beberapa file JavaScript yang berbeda menjadi satu. Developer selalu lebih nyaman memakai beberapa file berbeda (misalnya satu file untuk sebuah object JavaScript) sementara kinerja download akan lebih baik bila <script> merujuk pada satu file tunggal. Itu sebabnya file perlu digabungkan menjadi satu pada saat distribusi. Plugin grunt-contrib-uglify akan memakai UglifyJS untuk menghasilkan versi minified dari kode program JavaScript yang memiliki ukuran lebih kecil. Plugin grunt-contrib-jshint akan memakai JSHint untuk melakukan analisa kode program kode program JavaScript (JSHint adalah fork dari JSLint). Plugin grunt-contrib-nodeunit dibutuhkan untuk melakukan unit test dengan menggunakan nodeunit (ini adalah sesuatu yang memiliki fungsi mirip seperti JUnit di Java). Dan terakhir, plugin grunt-contrib-watch memiliki kemampuan untuk mengerjakan task Grunt tertentu secara otomatis bila ada file yang berubah.

Saat ini, semua plugin yang dibutuhkan belum ter-install pada lokasi proyek. Oleh sebab itu, saya perlu meminta npm untuk men-download semua yang ada di package.json tersebut dengan memberikan perintah:

$ npm install

Perintah di atas akan menyebabkan npm men-download dan meletakkan file yang dibutuhkan pada folder node_modules.

Berikutnya, saya akan melihat is file Gruntfile.js. Secara garis besar, saya dapat melihat konfigurasi untuk masing-masing task yang ada seperti berikut ini:

'use strict';

module.exports = function(grunt) {


  grunt.initConfig({

    pkg: ...,

    banner: ...,

    concat: ...,

    uglify: ...,

    nodeunit: ...,

    jshint: ...,

    watch: ...,

  });

  ...

};

Selain konfigurasi, saya juga menjumpai pemanggilan grunt.loadNpmTasks() yang akan me-load plugin. Setiap plugin menawarkan task masing-masing. Bila menginginkan task yang tidak disediakan oleh plugin Grunt, saya dapat membuat kode programnya sendiri dengan memanggil grunt.registerTask() yang melewatkan function yang berisi apa yang akan dikerjakan oleh task baru tersebut.

Apa itu task? Task adalah sebuah proses yang dikerjakan secara otomatis. Pada era konvensional, developer membuat batchfile (*.bat) atau shell script (*.sh) untuk mengerjakan proses pengelolaan proyek secara otomatis. Tapi file-file tersebut sering kali menjadi bertambah banyak, tidak terorganisir, dan sulit dipakai oleh developer lain (perbedaan platform, bahasa, dsb). Apache Ant mempopulerkan cara baru yang lebih seragam dan lebih standar dimana task yang umum dijumpai pada pengelolaan proyek Java didefinisikan dalam bentuk XML. Kesuksesan Ant pun dilanjutkan oleh tool lain seperti Maven dan Gradle (yang kini sedang ‘naik daun’ dan dipakai oleh Android Studio). Grunt adalah salah satu tool serupa tetapi ditujukan untuk mengelola proyek JavaScript.

Untuk melihat task apa saja yang bisa dikerjakan, saya dapat memberikan perintah berikut ini:

$ grunt --help
...
Available tasks
        concat  Concatenate files. *                                           
        uglify  Minify files with UglifyJS. *                                  
      nodeunit  Run Nodeunit unit tests. *                                     
        jshint  Validate files with JSHint. *                                  
         watch  Run predefined tasks whenever watched files change.            
       default  Alias for "jshint", "nodeunit", "concat", "uglify" tasks. 
...

Pada bagian Available tasks, saya dapat menjumpai task apa saja yang dapat saya panggil.

Sebagai contoh, bila saya ingin menjalankan unit test, saya dapat memberikan perintah seperti berikut ini:

$ grunt nodeunit
Running "nodeunit:files" (nodeunit) task
Testing myUtils_test.js.OK
>> 1 assertions passed (10ms)

Done, without errors.

Terlihat bahwa hasil pengujian sukses tanpa kesalahan.

Untuk menghasilkan file distribusi dimana seluruh file JavaScript terpisah akan digabungkan menjadi satu, saya dapat memberikan perintah:

$ grunt concat
Running "concat:dist" (concat) task
File "dist/myUtils.js" created.

Done, without errors.

Task di atas akan membuat sebuah file baru di folder dist dengan nama myUtils.js. Ini adalah file yang dapat didistribusikan kepada pengguna. Saya bisa membuat versi minified dari file yang dihasilkan oleh task concat tersebut dengan memberikan perintah:

$ grunt uglify
Running "uglify:dist" (uglify) task
File "dist/myUtils.min.js" created.

Done, without errors.

File myUtils.min.js adalah file berukuran kecil dari file myUtils.js yang dihasilkan oleh UglifyJS.

Penggunaan template grunt-init membuat struktur proyek JavaScript menjadi jelas dan rapi. Sebagai contoh, pada template yang saya pakai, kode program JavaScript yang dibuat developer terletak di direktori lib. Hasil akhir yang merupakan penggabungan file di lib dan versi minified-nya dapat dijumpai di folder dist. Kedua file tersebut merupakan output yang dapat didistribusikan langsung ke pengguna.

Task watch adalah sebuah task yang agak unik yang tidak saya jumpai di dunia Java. Bila saya menjalankan task ini, saya akan memperoleh hasil seperti:

$ grunt watch
Running "watch" task
Waiting...

Saya harus tetap membiarkan console ini tetap aktif. Lalu, saya membuka console lain dan melakukan perubahan pada file lib/myUtils.js. Begitu saya selesai menyimpan perubahan pada file tersebut, console yang menjalankan task watch akan menampilkan informasi seperti berikut ini:

Running "watch" task
Waiting...OK
>> File "lib/myUtils.js" changed.

Running "jshint:lib" (jshint) task
>> 1 file lint free.

Running "nodeunit:files" (nodeunit) task
Testing myUtils_test.jsF
>> awesome - no args
>> Message: should be awesome.
>> Error: 'not awesome?' == 'awesome'
>> at Object.exports.awesome.no args (test/myUtils_test.js:33:10)
>> at Object.exports.awesome.setUp (test/myUtils_test.js:28:5)

Warning: 1/1 assertions failed (17ms) Use --force to continue.

Waiting...

Terlihat bahwa begitu ada file JavaScript yang dimodifikasi oleh developer, maka task jshint dan nodeunit akan dikerjakan secara otomatis dan saya bisa langsung melihat laporannya. Bila proses seperti ini dilakukan di server tempat dimana seluruh developer men-commit perubahan kode program mereka, maka saya sudah memiliki sebuah sistem continous integration sederhana yang menguji kode program dari developer secara otomatis setiap kali ada perubahan.

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