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()

Iklan

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.

Memakai Reverse Ajax “Comet” Dengan Spring Web MVC 3.2

Hujan turun tanpa henti di malam yang ceriah ini.   Karena hari ini adalah hari terakhir di tahun 2012.  Dan tulisan ini akan menjadi tulisan terakhir di tahun 2012.  Selamat merayakan tahun baru dan selamat datang, 2013!  Kode program terakhir yang saya ubah di tahun 2012 berkaitan dengan sebuah aplikasi web yang saya buat dimana tampilannya seperti berikut ini:

Permintaan Perubahan

Permintaan Perubahan

Halaman tersebut menampilkan data termasuk status data yang ada. Perubahan status data, seperti “Belum Disajikan” menjadi “Sudah Disajikan” dilakukan oleh pengguna lain di komputer lain. Perubahan ini perlu diperbaharui oleh halaman tersebut.

Tetapi yang terjadi adalah bila pengguna membuka halaman ini, maka sampai selama-lamanya halaman akan terlihat sama, walaupun pengguna lain telah men-update status data.   Informasi yang ditampilkan menjadi tidak up-to-date lagi.

Lalu bagaimana cara memperbaharui tampilan? Pengguna bisa men-klik tombol Refresh atau menekan tombol F5!! ¬† Bayangkan jika pengguna harus memantau halaman ini terus menerus selama jam kerja (misalnya seorang kasir), maka tombol F5 bisa jadi tombol yang paling cepat pudar di keyboard ūüėČ

Lalu apakah ada solusi untuk membuat halaman ini terlihat lebih profesional dimana perubahan data oleh user lain bisa langsung diperbaharui?  Yup!  Saya bisa menggunakan apa yang disebut sebagai teknik Reverse Ajax.   Bila biasanya client yang duluan menghubungi server, maka pada Reverse Ajax, seolah-olah server yang akan menghubungi client.   Solusi lain adalah dengan memakai WebSocket (bagian dari HTML5) dengan syarat browser pengguna sudah mendukung.

Teknik Reverse Ajax terdiri atas polling, piggyback, dan Comet (long polling).

Polling adalah cara yang paling sederhana, yaitu dengan membuat sebuah timer JavaScript yang memeriksa perubahan data di server secara periodik. Cara ini akan sangat membebani server karena selalu ada data yang dikirim dari server biarpun tidak ada perubahan.

Piggyback akan mengembalikan status perubahan bersamaan dengan sebuah request normal. Misalnya, pada saat user men-klik sesuatu di halaman, sekalian ikut kembalian event perubahan.   Cara ini lebih hemat bandwidth dibandingkan dengan teknik polling. Kelemahannya adalah harus ada sebuah request yang normal terlebih dahulu, baru event perubahan yang terakumulasi di server dikirim balik bersamaan dengan reponse.   Saya tidak bisa menggunakan piggyback karena saya ingin halaman tetap diperbaharui biarpun pengguna hanya duduk diam menatap monitor.

Teknik Reverse Ajax yang lebih efisien adalah Comet atau sering disebut juga dengan long polling. Pada Comet, client akan melakukan request, tetapi server tidak akan langsung mengembalikan nilai.   Request ini akan terbuka untuk waktu yang lama.   Selama selang waktu tersebut, server dapat mengirim data ke client kapan saja.   Teknik ini adalah teknik yang paling efisien karena bandwidth hanya akan dipakai bila server perlu mengembalikan data ke client.

Saya akan memakai teknik Comet. ¬† Untuk itu, saya harus memenuhi persyaratan, yaitu memiliki server yang mendukung asynchronous reponse. ¬†Selain itu saya juga perlu memakai bahasa pemograman yang mendukung multithreading. Sebagai contoh, bila saya memakai Apache + PHP ‘murni’ dan berusaha menghentikan request dengan sleep(), maka selama thread di-sleep(), seluruh client lain tidak akan bisa mengakses web!! ¬† Kinerja web malah akan jauh lebih buruk dibanding memakai teknik polling. ¬†Server apa yang sudah mendukung asynchronous response? ¬† Tomcat 6 ke atas, Jetty 6 ke atas, Glassfish dengan Grizzly, dan sebagainya. ¬† Saya akan memakai Tomcat 7.

Apa contoh teknologi bahasa pemograman yang mendukung asynchronous response?   Servlet 3 di Java EE!   Ada juga framework yang mempermudah seperti CometD, DWR (Direct Web Remoting), dan Atmosphere.   Selain itu,  Spring Web MVC sejak versi 3.2 telah mendukung asynchronous response.   Karnea aplikasi saya dari awal dikembangkan dengan Spring Web MVC, maka yang perlu saya lakukan adalah men-upgrade versi Spring yang dipakai ke versi 3.2.   Dengan memakai Apache Maven, yang perlu saya lakukan hanya mengganti nilai property org.springframework-version menjadi 3.2.0.RELEASE, dan Maven akan mendownload JAR yang dibutuhkan.

Setelah men-upgrade versi Spring ke 3.2.0.RELEASE, saya perlu mengubah file web.xml. Perubahan yang saya lakukan adalah:

<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" version="3.0">

Selain itu, saya perlu menambahkan <async-supported> di definisi org.springframework.web.servlet.DispatcherServlet milik Spring Web MVC seperti yang terlihat berikut ini:

<servlet>
    <servlet-name>appServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>    
    <init-param>      
      <param-name>contextConfigLocation</param-name>
      <param-value>/WEB-INF/spring/appServlet/servlet-context.xml</param-value>
    </init-param>    
    <load-on-startup>1</load-on-startup>
    <async-supported>true</async-supported>
</servlet>

Karena saya memakai Spring Security yang mendefinisikan sebuah filter di web.xml yaitu org.springframework.web.filter.DelegatingFilterProxy, maka saya juga perlu menambahkan <async-supported> pada definisi filter tersebut. Berikut ini adalah perubahan yang saya lakukan:

<filter>
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    <async-supported>true</async-supported>    
  </filter>
  <filter-mapping>
    <filter-name>springSecurityFilterChain</filter-name>
    <url-pattern>/*</url-pattern>
    <dispatcher>REQUEST</dispatcher>
    <dispatcher>ASYNC</dispatcher>
  </filter-mapping>
</filter>

Setelah ini, saya bisa mulai membuat kode program.  Saya akan mulai dari service layer.  Saya sedikit frustasi karena tidak menemukan pendekatan yang lebih rapi yang tidak mengotori service layer dengan objek DeferredResult milik presentation layer.  Saya selalu berusaha menghindari coupling seperti ini, tapi sepertinya tidak ada pilihan yang lebih gampang.   Pada service layer, saya mendefisinikan sebuah  struktur untuk menampung DeferredResult yang mewakili asynchronous request seperti berikut ini:

private List<DeferredResult<Boolean>> listDeferredResult = new Vector<DeferredResult<Boolean>>();

Saya memakai Vector karena class tersebut thread safe.  Pada asynchronous request yang diberikan halaman kasir, saya hanya mengembalikan sebuah nilai true bila halaman perlu diperbaharui atau false bila tidak.  Hal ini karena tabel dibuat dengan jqGrid dan saya tidak ingin repot mengelola data.

Pada satu atau lebih service yang menyebabkan perubahan di halaman kasir, saya memberikan kode program seperti berikut ini:

public Pemesanan prosesTambahPemesanan(Pemesanan pemesanan) {
   ...
   prosesNotifikasiPembayaran();
}

public Pemesanan prosesUpdatePemesanan(Pemesanan pemesanan) {
   ...
   prosesNotifikasiPembayaran();
}

private void prosesNotifikasiPembayaran() {
   for (DeferredResult<Boolean> deferredResult: listDeferredResult) {
      deferredResult.setResult(Boolean.TRUE);
   }
}

Method prosesNotifikasiPembayaran() akan menyebabkan asynchronous response selesai dengan nilai kembali berupa true.

Tidak lupa saya juga menyediakan method untuk mendaftar dan menghapus setiap DeferredResult yang ada di Vector, seperti berikut ini:

public void daftarNotifikasiPembayaran(DeferredResult<Boolean> deferredResult) {
  listDeferredResult.add(deferredResult);
}

public void hapusNotifikasiPembayaran(DeferredResult<Boolean> deferredResult) {
  listDeferredResult.remove(deferredResult);
}

Ok, service layer sudah selesai, sekarang saya akan ke presentation layer.  Saya menambahkan method berikut ini di controller:

@RequestMapping(value="pembaharuan")
@ResponseBody
public DeferredResult<Boolean> notifikasiPembayaran() {
  final DeferredResult<Boolean> deferredResult = new DeferredResult<Boolean>(30000l, Boolean.FALSE);
  pemesananService.daftarNotifikasiPembayaran(deferredResult);
  deferredResult.onCompletion(new Runnable() {
    @Override
    public void run() {
      pemesananService.hapusNotifikasiPembayaran(deferredResult);
    }
  });
  return deferredResult;
}

Pada kode program controller tersebut, saya memberikan waktu tunggu 30.000 ms atau 30 detik.  Bila setelah 30 detik, belum ada yang memanggil method DeferredResult.setResult(), maka nilai false akan dikembalikan.

Langkah terakhir, menambahkan kode program ini di view JSP:

...
<spring:url value="/pembayaran/pembaharuan" var="urlPembaharuan" />
...
function comet() {
  $.getJSON("${urlPembaharuan}", function(data) {
     if (data==true) {
        gridUtama.trigger('reloadGrid');
     }
     comet();
  });
}
...

Pada kode program di atas, bila hasil asynchronous request adalah true, maka dialog jqGrid akan di-refresh.   Setelah itu, tidak peduli nilai kembali adalah true atau false, fungsi tersebut akan kembali memanggil dirinya sendiri.

Untuk mencobanya, saya membuka tiga browser yang berbeda, dimana pada 2 browser, saya membuka halaman untuk kasir.  Satu browser-nya lagi untuk perubahan yang harus di-update oleh halaman kasir, seperti pada gambar berikut ini:

Pengujian

Pengujian

Pada gambar di atas, pada saat tombol “Iya” di-klik, maka tabel di browser Kasir A maupun browser Kasir B akan di-reload secara otomatis.

Teknik Comet (long polling) memang agak mirip polling, dimana client harus melakukan request secara periodik.  Tetapi penggunaan asynchronous request membuat Comet jauh lebih efisien dibanding polling:

  1. Pada  polling,  bila interval adalah 30 detik, maka client benar-benar harus menunggu selama 30 detik baru ada hasil yang diperoleh.  Pada Comet, bila interval adalah 30 detik, lalu pada detik ke-15 sudah ada event, maka hasil akan segera dikembalikan pada saat itu juga.
  2. Karena alasan di atas, untuk memperoleh hasil yang real-time, maka polling biasanya memiliki interval yang singkat, misalnya 10ms.   Hal ini menyebabkan semakin banyak request yang dilakukan ke server secara periodik dan akan membebani server.

Memakai Autocomplete Widget Di Spring Web MVC

Saya memiliki sebuah halaman pencarian dimana pengguna bisa mengetik keyword pencarian, lalu isi halaman akan diperbaharui berdasarkan keyword tersebut secara AJAX.   Perubahan isi konten secara otomatis mungkin akan memberatkan browser pengguna; selain itu, saya harus men-query seluruh data setiap kali pengguna melakukan perubahan keyword yang di-ketik.  Bayangkan seorang pengguna yang menekan backspace, mengetik satu huruf, menekan backspace, mengetik dua huruf, menekan backspace dua kali, dan seterusnya.  Berapa kali harus request AJAX yang dilakukan ke server?  Hal ini dapat diatasi dengan AutoComplete widget dari jQuery UI yang memiliki option delay yang akan menunggu sesuai dengan milliseconds yang diberikan sebelum menghubungi server.   Dengan memakai Autocomplete widget, maka isi konten tidak lagi diperbaharui secara otomatis, tetapi seiring pengguna mengetik, akan muncul panduan keyword yang bisa dipilih oleh pengguna.

Berikut ini adalah kode HTML untuk bagian pencarian:

<div class="search custom_general_menu">
  <form name="formSearch" id="formSearch" action="#" method="post">
    <p>
      <input name="keyword" id="keyword" placeholder="Cari menu makanan..." type="text" />
      <input value="" id="cariMenu" class="btnsearch" type="submit" />
    </p>
  </form>
</div>

Untuk memakai Autocomplete widget, maka saya menambahkan baris berikut ini di halaman jspx tersebut:

...
  <spring:url value="/menumakanan/autocomplete" var="urlAutocomplete" />
  ...
  <script type="text/javascript">
     ...
     $("input#keyword").autocomplete({
        delay: 500,
        minLength: 3,
        source: "${urlAutocomplete}"
     });
     ...
  </script>
...

Pada konfigurasi di atas, setelah 500 ms  sejak user mengetik sebuah huruf  baru akan dilakukan request ke URL /menumakanan/autocomplete, dimana akan dilewatkan parameter term berupa keyword yang diketik oleh user.

Okay, bagian sisi web front-end sudah selesai, sekarang saya perlu melakukan perubahan di sisi back-end.

Saya akan mulai dengan menambahkan sebuah query baru yang hanya mengembalikan String di MenuMakananRepository.java.  Karena saya memakai Spring Data JPA, saya hanya perlu menambah sebuah method baru dengan isi seperti berikut ini:

...
@Query("SELECT menu.namaMenu FROM MenuMakanan menu WHERE menu.namaMenu LIKE :namaMenu")
List<String> findNamaMenuMakanan(@Param("namaMenu") String namaMenuMakanan);
...

Setelah itu, saya menambahkan sebuah service baru di MenuMakananService.java dengan isi seperti berikut ini:

...
public List<String> getNamaMenuMakanan(String namaMenuYangDicari) {
  return menuMakananRepository.findNamaMenuMakanan(
    String.format("%%%s%%", namaMenuYangDicari));
}
...

Sampai disini, saya akan membuat test case pada unit test MenuMakananService tersebut untuk memastikan bahwa tidak ada yang salah pada kode program.  Mm, kadang-kadang saya suka memakai test driven development (TDD) dimana saya membuat unit test terlebih dahulu baru membuat kode program di atas.  Biasanya, saya mengikuti  prinsip TDD hanya jika saya masih belum memahami sepenuhnya apa yang harus saya buat.

Setelah memastikan tidak ada yang salah di service layer, saya akan lanjut ke controller.   Saya akan membuat sebuah class Java yang mewakili JSON yang akan dikembalikan nanti.  Isi class tersebut akan terlihat seperti:

public class Autocomplete {

  public String value;

  public Autocomplete(String value) {
     this.value = value;
  }

  public String getValue() {
     return value;
  }

  public void setValue(String value) {
     this.value = value;
  }
}

Class ini nantinya akan diterjemahkan menjadi JSON oleh Jackson.  Format JSON ini yang diharapkan oleh Autocomplete widget adalah seperti berikut ini:

[
  {label: 'Item 1', value: 'KODE1'},
  {label: 'Item 2', value: 'KODE2'},
  {label: 'Item 3', value: 'KODE3'}
]

Nilai label akan ditampilkan dalam popup nanti, sementara nilai value akan dimasukkan ke dalam text box. Karena umumnya, kedua nilai tersebut sama, maka Autocomplete widget juga menerima JSON seperti berikut ini:

[
  {value: 'Item 1'},
  {value: 'Item 2'},
  {value: 'Item 3'}
]

Dengan demikian, untuk menghasilkan format JSON di atas, saya hanya perlu meletakkan setiap object Autocomplete ke dalam sebuah List.

Sekarang, saya tinggal menambahkan sebuah method baru di controller MenuMakananController.java dengan isi seperti berikut ini:

...
@RequestMapping(value="autocomplete")
@ResponseBody
public List<Autocomplete> autocomplete(@RequestParam("term") String term) {
   return (List<Autocomplete>) CollectionUtils.collect(menuMakananService.getNamaMenuMakanan(term),
     new Transformer() {
       @Override
       public Object transform(Object input) {
          return new Autocomplete((String) input);
       }
     }
   });
}
...

Pada kode program controller di atas, saya memakai class CollectionUtils dari Apache Commons Collections untuk menyederhanakan kode program.  Sesungguhnya, perbedaan jumlah baris tidak terlalu beda jauh dibanding membuat kode looping for biasa, tetapi saya selalu membiasakan diri untuk tidak melakukan reinvent the wheel (kecuali untuk kasus-kasus tertentu, misalnya tweaking kinerja kode program).  Well,  seorang computer scientist akan tertarik mempelajari kode program internal dan seluk-beluknya (bahkan membuat sesuatu yang baru yang lebih baik!), tetapi seorang software developer harus berusaha menyelesaikan sebuah tugas secara efisien dan secepat mungkin.

Sekarang, bila saya menjalankan halaman web tersebut,  Autocompletion widget akan bekerja seperti yang terlihat di gambar berikut ini:

Tampilan Autocomplete widget

Tampilan Autocomplete widget

Memahami Event Bubbling dan Method .on() di jQuery

Pada jlSimpleTableEditor (yang ada di artikel Menghasilkan Tabel Editor Sederhana Dengan Widget jQuery), setiap kali sebuah baris ditambahkan, maka method _buatTombolEdit() dan _buatTombolHapus akan dipanggil.  Didalam kedua method tersebut, akan dibuat event handler baru untuk kedua tombol tersebut.  Hal ini berarti bila ada 10 baris, maka akan terdapat 20 event handler (untuk 2 tombol di setiap baris).  Bagaimana bila ada 100 baris?  Akan ada 200 event handler!  Walaupun overhead ini mungkin tidak akan terasa pada pengguna yang memiliki PC canggih, tapi saya bisa melakukan optimalisasi disini.

Di JavaScript, sebuah event untuk sebuah elemen, akan disampaikan juga kepada elemen yang mengandungnya atau element parent.  Lalu dari parent ke parent-nya parent.  Begitu seterusnya, hingga akhirnya sampai ke yang paling atas, yaitu document.  Perilaku ini biasanya disebut sebagai event bubbling atau event propagation.

Mari lakukan pembuktian sederhana mengenai event bubbling.  Saya membuat sebuah HTML seperti berikut ini:

<html>
  <head>
  </head>
  <body>
    <div id="div1">
      <div id="div2">
        <p id="paragraph">
          <input type="button" value="Klik Disini" id="tombol" />
        </p>
      </div>
    </div>
    <script>
      function pesan(e) {
        alert('Event target adalah ' + e.target.id);
      }
      document.getElementById("tombol").onclick = pesan;
      document.getElementById("paragraph").onclick = pesan;
      document.getElementById("div2").onclick = pesan;
      document.getElementById("div1").onclick = pesan;
    </script>
  </body> 
</html>

Jika tombol di-klik, maka akan muncul kotak dialog 4 kali! ¬†Yup, 4 kali, tetapi isinya selalu sama, yaitu “Event target adalah tombol”. ¬†Padahal, ada 4 elemen yang berbeda yang berusaha menangani click event. ¬†Ini yang disebut¬†event bubbling:¬†click event yang terjadi pada elemen dengan id tombol telah menggelembung naik ke element dengan id paragraph, lalu naik lagi ke div2, lalu naik lagi ke div1, kemudian ke document (akan diabaikan karena tidak handler onclick disini).

Untuk memperjelas konsep event bubbling, sekarang saya mengubah function pesan() di atas menjadi seperti berikut ini:

function pesan(e) {
  alert("Event target adalah " + e.target.id);
  e.stopPropagation(); // menghentikan event bubbling!
}

Sekarang, bila saya men-klik tombol, maka kotak dialog yang muncul hanya 1 saja. Ini karena saya telah menghentikan event bubbling dengan memanggil method stopPropagation() milik Event Javascript.

Lalu bagaimana cara memanfaatkan event bubbling bagi jlSimpleTableEditor dalam mengurangi overhead?  Bila saya meletakkan event handler di <tbody>, bukan di masing-masing <tr>, maka hanya 2 event handler yang dibutuhkan, tidak peduli sebanyak apapun jumlah baris di tabel.  Hal ini karena setiap kali tombol Edit dan tombol Hapus (baik yang sudah ada maupun yang akan ditambahkan secara dinamis nanti) di-klik, maka event click tersebut akan di-propagate ke <tbody>.

Saya akan meletakkan event handler untuk tombol Edit dan tombol Hapus di method _create(). Karena method ini hanya akan dikerjakan pada saat widget dibuat, maka 2 event handler ini akan berlaku untuk seluruh baris yang sudah ada maupun yang akan ditambahkan secara dinamis nanti.  Isi kode program akan terlihat seperti:

_create: function() {

  // men-handle operasi "edit"
  $("tbody", this.element).on("click", "tr td input.jlEdit", $.proxy(function(e) {

    var indexBaris = jQuery.data(e.target, "baris");
    ... // isi event handler yang lama disini

  }, this));

  // men-handle operasi "Hapus"
  $("tbody", this.element).on("click", "tr td input.jlHapus",  $.proxy(function(e) {

    var indexBaris = jQuery.data(e.target, "baris");
    ... // isi event handler yang lama disini

  }, this));

  .. // kode program lainnya diabaikan
}

Pada kode program di atas,¬†event handler diletakkan pada element <tbody>. ¬†Bila ada event “click” yang di-propagate (bubble) dari "tr td input.jlEdit" ataupun "tr td input.jlHapus", maka kode event handler tersebut akan dikerjakan. ¬† Yang menarik adalah nilai e.target tetap akan merujuk ke pemicu event, yaitu <input type='button'>. ¬†Yup, karena target-nya memang bukan <tbody> walaupun saat ini sedang berada di <tbody>. ¬†Dengan demikian, saya masih bisa tetap mendapatkan index baris mana yang akan di-proses melalui nilai e.target tersebut.

Method _buatTombolEdit() dan _buatTombolHapus() kini cukup hanya memanipulasi elemen tanpa ada kode program event handler.   Dengan demikian, isi kedua method tersebut cukup hanya:

_buatTombolEdit: function(indexBaris, parent) {
  $("<input />", {
    type: 'button',
    value: 'Edit',
    class: 'jlEdit'
  }).data('baris', indexBaris).appendTo(parent).wrap("<td />");
}

_buatTombolHapus: function(indexBaris, parent) {
  $("<input />", {
    type: 'button',
    value: 'Hapus',
    class: 'jlHapus'
  }).data('baris', indexBaris).appendTo(parent).wrap("<td />");
}

Menghasilkan Tabel Editor Sederhana Dengan Widget jQuery

Beberapa waktu lalu, seorang teman yang sedang mengerjakan tesis mencari saya, meminta bantuan untuk membuat sebuah tabel AJAX.    Sebagian besar orang-orang disini memandang bahwa tesis lebih condong ke pembahasan teoritis.  Saya sempat menawarkan padanya sebuah widget jQuery sederhana yang saya buat untuk keperluannya, yaitu jlSimpleTableEditor.  Keperluannya cukup sederhana: ia ingin bisa sebuah tabel HTML bisa di-edit, di-hapus dan di-tambah, tanpa harus repot di sisi kode program server (ia memakai PHP dan framework Symfony).  Wow, seandainya ia bukan seorang yang tidak ingin repot, ia sudah pasti memakai jqGrid yang saya sarankan dari awal.  Akan tetapi, bila memakai jqGrid, maka kode program PHP-nya harus mengembalikan JSON dan XML, dan ini akan membuatnya tidak betah coding.

Widget jlSimpleTableEditor pada dasarnya adalah sebuah tabel dinamis yang sangat sederhana dan tidak membutuhkan banyak modifikasi di sisi server.  Karena saking sederhananya, saya tidak yakin widget ini akan berguna selain untuk  tesis teman saya (bahkan pada akhirnya teman saya tidak ingin memakai widget dan lebih senang men-copy paste plain old JavaScript yang saya buat!)

jlSimpleTableEditor dapat di-download di link berikut ini:  https://docs.google.com/open?id=0B-_rVDnaVRCbc0p2LVo1R0R0clU.  Klik menu File, Download untuk men-dowload file ZIP tersebut. Untuk memakainya, copy paste folder jquery-ui-1.9.1.custom dan widget ke folder proyek.  Lalu, untuk menguji widget tersebut, saya akan sebuah file tampil.php.  Sebagai contoh, saya memakai Zend Studio, sehingga struktur proyek saya terlihat seperti berikut ini:

Struktur Proyek

Struktur Proyek

Lalu, saya akan login ke MySQL sebagai user jocki dan password berupa password, dan memakai database latihan.  Saya juga akan membuat sebuah tabel kosong, seperti yang diperlihatkan oleh perintah di MySQL command line pada gambar berikut ini:

Membuat Tabel Di MySQL

Membuat Tabel Di MySQL

Berikutnya, saya membuat sebuah file CSS yang saya beri nama style.css yang akan menentukan tampilan dari jlSimpleTableEditor.  File ini tidak wajib ada.  Tanpa ada CSS yang memberikan format, tampilan jlSimpleTableEditor akan persis seperti tampilan tabel HTML yang polos.  Isi dari file style.css adalah:

/* tombol tambah */
.tblDinamis-tombol-tambah {
	-webkit-box-shadow: rgba(0,0,0,0.2) 0 1px 0 0;
	-moz-box-shadow: rgba(0,0,0,0.2) 0 1px 0 0;
	box-shadow: rgba(0, 0, 0, 0.2) 0 1px 0 0;
	color: #333;
	background-color: #FA2;
	border: none;
	border-radius: 5px;
	font-family: 'Helvetica Neue', Arial, sans-serif;
	font-size: 16px;
	padding: 4px 16px;
	text-shadow: #FE6 0 1px 0;
	margin-bottom: 10px;
	cursor: pointer;
}
.tblDinamis-tombol-tambah:hover {
	opacity: 0.8;
}

/* tombol edit */
.jlEdit, .jlHapus {
	background: #feda71;
	background: -moz-linear-gradient(top, #feda71 0%, #febb49 100%);
	background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #feda71),color-stop(100%,#febb49));
	background: -webkit-linear-gradient(top, #feda71 0%, #febb49 100%);
	padding: 4px 20px;
	color: #623f1d;
	font-family: 'Helvetica Neue', sans-serif;
	font-size: 12px;
	border-radius: 20px;
	border: 1px solid #623f1d;
	margin-bottom: 5px;
	cursor: pointer;				
}
.jlEdit:hover, .jlHapus:hover {
	opacity: 0.5;
}

/* tabel */
#tblDinamis {
	font: 17px/30px Verdana, Arial, Helvetica, sans-serif;
	border-collapse: collapse;
	width: 520px;
}
#tblDinamis th {
	padding: 0 0.5em;
	text-align: left;
	background-color: #FFE45C;
}
#tblDinamis tr {
	border-top: 1px solid #fb7a31;
	border-bottom: 1px solid #fb7a31;
	background: #ffc;
}
#tblDinamis td {
	border-bottom: 1px solid #ccc;
	padding: 0 0.5em;
}

Berikutnya, saya mengubah kode program pada lihat.php menjadi seperti berikut ini:

<!doctype html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Demo</title>
      <script src="jquery-ui-1.9.1.custom/js/jquery-1.8.2.js"></script>
      <script src="jquery-ui-1.9.1.custom/js/jquery-ui-1.9.1.custom.js"></script>
      <script src="widget/jquery.ui.jlSimpleTableEditor.js"></script>
      <link rel="stylesheet" href="jquery-ui-1.9.1.custom/css/ui-lightness/jquery-ui-1.9.1.custom.css" />
      <link rel="stylesheet" href="style.css" />
      <script type="text/javascript">
	$(function() {
	   $("#tblDinamis").jlSimpleTableEditor({
	     parameterKolom: ["nim", "nama", "nilai"],  
  	     label: ["NIM", "Nama Lengkap", "Nilai (Grade)"],
	     tipeKolom: ["string", "string", ["A","B","C","D","E"]],  
	     dialogOptions: { title: "Edit Data" },
	   });			
	});
      </script>
  </head>
  <body>		
    <form method="post" action="simpan.php">
      <table id="tblDinamis">
	<thead>
	   <tr><th>NIM</th><th>Nama</th><th>Nilai</th></tr>
	</thead>
	<tbody>
	<?php 
	  $cn = mysqli_connect("localhost", "jocki", "password", "latihan");
	  $result = mysqli_query($cn, "select nim, nama, nilai from mhs");
	  while ($baris = mysqli_fetch_row($result)) {
	    print "<tr><td>{$baris[0]}</td><td>{$baris[1]}</td><td>${baris[2]}</tr>";	
	  }					
	?>
	</tbody>
        <input type="submit" value="Simpan" style="margin-top: 20px" />
       </table>		
    </form>
   </body>
</html>

Disini terlihat kelebihan dari jlSimpleTableEditor (sesuai tujuan awalnya!).  Kode program untuk menampilkan tabel adalah kode program yang membentuk tabel HTML biasa (<table>, <tbody>, <tr>, dan <td>).  Tag <table> ini harus berada dalam sebuah tag <form> yang berisi action.  Nantinya, bila tombol submit dipilih, data tabel akan dikirim ke halaman yang ditentukan oleh tag <form>, dalam hal ini adalah halaman simpan.php.

Sekarang, bila saya membuka halaman ini di-browser, saya akan menemukan sebuah tabel yang telah dilengkapi tombol “Tambah” seperti terlihat pada gambar berikut ini:

Tampilan jlSimpleTableEditor Tanpa Data

Tampilan jlSimpleTableEditor Tanpa Data

Bila saya men-klik tombol “Tambah” tersebut, secara otomatis akan muncuk kotak dialog untuk mengisi data baru. ¬†jlSimpleTableEditor akan membuat isi ¬†kotak dialog berdasarkan nilai labelKolom dan tipeKolom yang diberikan pada saat inisialisasi widget. ¬†Pada halaman HTML di atas, tampilan dialog-nya akan terlihat seperti berikut ini:

Dialog Tambah Dari jlSimpeTableEditor

Dialog Tambah Dari jlSimpeTableEditor

Setelah menekan tombol OK, akan muncul sebuah baris baru di tabel. ¬†jlSimpleTableEditor akan secara otomatis memberikan tombol “Edit” dan tombol “Hapus” pada setiap baris yang ada, seperti yang terlihat pada gambar berikut ini:

Tampilan jlSimpleTableEditor Dengan 1 baris data

Tampilan jlSimpleTableEditor Dengan 1 baris data

Tanpa perlu banyak coding, saya telah memperoleh fitur untuk “Tambah”, “Edit” dan “Hapus”. ¬†Perlu diingat bahwa sampai disini, data masih belum tersimpan ke database, melainkan ditampung dalam bentuk <input type=”hidden” />. ¬†Untuk benar-benar menyimpan data ke database, pengguna harus men-klik tombol “Simpan”. ¬†Gambar berikut ini memperlihatkan cara kerja jlSimpleTableEditor:

Menyimpan data di jlSimpleTableEditor

Menyimpan data di jlSimpleTableEditor

Sebagai langkah terakhir, saya perlu membuat halaman simpan.php untuk melakukan penyimpanan data.  Isi simpan.php akan terlihat seperti berikut ini:

<?php
  $jumlahBaris = $_POST['jumlahBaris'];
  $cn = mysqli_connect("localhost", "jocki", "password", "latihan");
  mysqli_query($cn, "DELETE FROM mhs");
  $stmt = mysqli_prepare($cn, "INSERT INTO mhs VALUES (?,?,?)");
  mysqli_stmt_bind_param($stmt, "sss", $nim, $nama, $nilai);
  for ($i=0; $i<$jumlahBaris; $i++) {
    $nim = $_POST["nim$i"];
    $nama = $_POST["nama$i"];
    $nilai = $_POST["nilai$i"];
    mysqli_stmt_execute($stmt);
  }
  mysqli_close($cn);
  header('Location: tampil.php');	
?>

Isi kode program di atas tidak ada yang spesial.  Yang dilakukan hanya membaca nilai yang dikirim oleh jlSimpleTableEditor berupa jumlahBaris, nim0, nama0, nilai0, nim1, nama1, nilai1, nim2, nama2, nilai2, dan seterusnya.  Nama parameter yang dikirim oleh jlSimpleTableEditor ditentukan oleh nilai parameterKolom yang diberikan pada saat inisialisasi widget.

Pada kode program di atas, saya menghapus dulu seluruh isi tabel. ¬†Hal ini karena dibutuhkan kode program yang lebih panjang lagi untuk ¬†mendeteksi update. ¬†Saya pikir teman saya yang menginginkan cara gampang pasti tidak tertarik! ¬†Yup, ia pasti lebih senang dengan satu baris DELETE. “Bukankah semakin cepat tesis selesai bisa semakin cepat mendapat gelar dan bisa lega?” Mungkin kira-kira begitu katanya.

Aptana Journal #2: Menampilkan Info Untuk Content Assist

Pada Aptana Journal #1, saya berusaha untuk menampilkan content assist jQuery di Aptana Studio. Walaupun content assist sudah berhasil muncul, tetapi belum ada context info yang ditampilkan. Padahal, bagi seorang pemula seperti saya, memiliki deskripsi teks menjelaskan fungsi dan parameter jQuery akan sangat membantu. Oleh sebab itu, kali ini saya akan mencoba menampilkan context info untuk content assist yang sedang terpilih.

Permasalahan awal yang saya hadapi adalah bagaimana memperoleh teks yang menjelaskan setiap fungsi jQuery yang ada? Pada bahasa pemograman Java, terdapat Javadoc yang dipakai untuk mendokumentasikan kode program sehingga IDE cukup menampilkan teks informasi berdasarkan Javadoc. Lalu bagaimana dengan JavaScript? Tidak ada sebuah metode dokumentasi standar di JavaScript! Beruntungnya, Aptana Studio 3 mendukung beberapa bentuk dokumentasi JavaScript, salah satunya adalah ScriptDoc XML (file *.sdocml). ¬†jQuery secara resmi tidak menyediakan dokumentasi ScriptDoc XML, tetapi ada beberapa situs yang menyediakan dokumentasi jQuery buatan mereka. Sebagai bahan ‘percobaan‘, saya akan membuat sendiri sebuah file ScriptDoc XML¬†dengan mama jQuery-1.8.2.sdocml yang ¬†isinya seperti berikut ini:

<?xml version="1.0"?>
<!-- Aptana Studio support for the jQuery 1.8.2 JavaScript Libary -->
<javascript>
	<aliases>
		<alias name="$" type="jQuery" />
	</aliases>
	<class type="jQuery">
		<constructors>

			<constructor scope="instance">
				<description>Accepts a string containing a CSS selector which is then used to match a set of elements.</description>
				<parameters>
					<parameter name="selector" usage="required" type="String">
						<description>A string containing a selector expression</description>
					</parameter>
					<parameter name="context" usage="optional" type="Element,Document,jQuery">
						<description>A DOM Element, Document, or jQuery to use as context</description>
					</parameter>
				</parameters>
				<return-types>
					<return-type type="jQuery" />
				</return-types>
				<examples>
					<example>Find all div elements within an XML document from an Ajax reponse.
						<pre>
						$("div", xml.responseXML);
						</pre></example>
				</examples>
			</constructor>

			<constructor scope="instance">
				<description>Accepts a string containing a CSS selector which is then used to match a set of elements.</description>
				<parameters>
					<parameter name="selector" usage="required" type="String">
						<description>A string containing a selector expression</description>
					</parameter>
				</parameters>
				<return-types>
					<return-type type="jQuery" />
				</return-types>
				<examples>
					<example>Find all div elements.
						<pre>
						$("div");
						</pre></example>
				</examples>
			</constructor>

		</constructors>
	</class>
</javascript>

Isi file di atas diambil dari dokumentasi resmi jQuery di http://api.jquery.com.  Isinya masih jauh dari lengkap karena baru berisi dokumentasi dua variasi contructor jQuery! Agar Aptana Studio 3 menampilkan context info, saya perlu menambahkan file sdocml tersebut ke dalam proyek seperti yang terlihat pada gambar berikut ini:

Menyertakan ScriptDoc XML ke Proyek

Menyertakan ScriptDoc XML ke Proyek

Setelah memenuhi persyaratan yang ada, saya pun mencoba membuka content assist untuk melihat hasilnya. Saya sedikit kecewa karena hasil yang saya temukan adalah kejanggalan yang terlihat pada gambar berikut ini:

Tidak semua dokumentasi constructor ditampilkan

Tidak semua dokumentasi constructor ditampilkan

Padahal jelas-jelas saya menyertakan dua variasi constructor jQuery, tetapi kenapa Aptana Studio hanya menampilkan satu saja? Pertanyaan ini tiba-tiba mengingatkan saya pada fakta bahwa JavaScript tidak mendukung polymorphism (secara syntax)!   Dengan kata lain, bila ada lebih dari satu method dengan nama yang sama, tetap hanya satu method yang dikenali!

Lalu kenapa di dokumentasi jQuery ada method polymorphism, termasuk di constructor? Karena di satu method yang sama, kode program jQuery akan mencocokkan tipe parameter yang diberikan dan akan melakukan hal yang berbeda sesuai dengan tipe parameter tersebut, sehingga seolah-olah telah terjadi polymorphism. ¬† Yup! Ini memang adalah bentuk polymorphism secara manual karena JavaScript tidak memiliki syntax yang ketat untuk ‘berjaga-jaga‘. ¬†Bila bahasa pemograman tidak sanggup menerapkan integritas, bukankah alangkah baiknya lebih baik bila IDE bisa membantu sehingga pengguna bahasa pemograman bisa terhindar dari kesalahan?

Saya segera mencari class apa yang  bertanggung jawab untuk mengembalikan daftar method JavaScript.  Pencarian saya berakhir di  file com.aptana.editor.js/src/com/aptana/editor/js/contentassist/JSContentAssistProcessor.java di method getFunctionElement().  Method ini  hanya mengembalikan sebuah nilai FunctionElement.   Saya akan membuat sebuah method baru dengan isi yang tidak jauh berbeda, tetapi method baru akan mengembalikan nilai List<FunctionElement> (bisa lebih dari satu FunctionElement).    Berikut ini adalah isi method baru tersebut:

/**
 * Mendukung lebih dari satu <code>FunctionElement</code>.  Hal ini bisa berguna bila diterapkan pada
 * SDocML, tetapi tidak pada JavaScript karena JavaScript tidak mendukung polymorphism.
 * 
 */
private List<FunctionElement> getFunctionElementList(ITextViewer viewer, int offset) {
    JSArgumentsNode node = getArgumentsNode(offset);
    List<FunctionElement> listReturn = new ArrayList<FunctionElement>();

    // process arguments node as long as we're not to the left of the opening parenthesis
    if (node != null)
    {
        // save current replace range. A bit hacky but better than adding a flag into getLocation's signature
        IRange range = replaceRange;

        // grab the content assist location type for the symbol before the arguments list
        int functionOffset = node.getStartingOffset();
        //LocationType locationAndOffset = getLocationType(viewer.getDocument(), functionOffset);
        LocationType location = getLocationType(viewer.getDocument(), functionOffset);

        // restore replace range
        replaceRange = range;

        // init type and method names
        String typeName = null;
        String methodName = null;

        switch (location)
        {
            case IN_VARIABLE_NAME:
            {
                typeName = JSTypeUtil.getGlobalType(getProject(), getFilename());
                methodName = node.getParent().getFirstChild().getText();
                break;
            }

            case IN_PROPERTY_NAME:
            {
                JSGetPropertyNode propertyNode = ParseUtil.getGetPropertyNode(node,
                        ((JSNode) node).getContainingStatementNode());
                List<String> types = getParentObjectTypes(propertyNode, offset);

                if (types.size() > 0)
                {
                    typeName = types.get(0);
                    methodName = propertyNode.getLastChild().getText();
                }
                break;
            }

            default:
                break;
        }

        if (typeName != null && methodName != null)
        {
            Collection<PropertyElement> properties = indexHelper.getTypeMembers(getIndex(), typeName, methodName);

            if (properties != null)
            {
                for (PropertyElement property : properties)
                {
                    if (property instanceof FunctionElement)
                    {
                        FunctionElement currentFunction = (FunctionElement) property;

                        // Periksa apakah function dengan nama parameter yang sama sudah ada dalam list?
                        boolean sudahAda = false;
                        boolean ketemu = false;
                        for (int i=0; i<listReturn.size(); i++) {
                            if (currentFunction.getParameterNames().containsAll(listReturn.get(i).getParameterNames()) &&
                                currentFunction.getParameterNames().size()==listReturn.get(i).getParameterNames().size()) {
                                ketemu = true;
                                // jika sudah ada function dengan parameter yang sama dalam list									
                                if (currentFunction.getDescription().trim().length()>0) {
                                    listReturn.set(i, currentFunction);
                                    sudahAda = true;										
                                    break;
                                }
                            }
                        }

                        // Jika function belum ada, maka tambahkan ke dalam list.
                        if (!sudahAda && !ketemu) {
                            listReturn.add(currentFunction);
                        }
                    }
                }
            }
        }
    }

    return listReturn;
}

Setelah itu, saya mengubah setiap pemanggilan ke method getFunctionElement() menjadi getFunctionElementList(). Untung saja tidak banyak yang berubah karena FunctionElement tunggal yang dikembalikan juga dimasukkan ke List pada akhirnya.

Sekarang, bila saya mencoba content assist, polymorphism di dokumentasi akan ditampilkan dengan baik seperti yang terlihat pada gambar berikut ini:

Content Assist Yang Mendukung Polymorphism

Content Assist Yang Mendukung Polymorphism

Bila saya memilih salah satu pilihan yang ada, maka context info akan ditampilkan dengan baik seperti yang terlihat pada gambar berikut ini:

Tampilan Dokumentasi jQuery Saat Content Assist Dipilih

Tampilan Dokumentasi jQuery Saat Content Assist Dipilih

Kapan memakai jQuery.proxy()?

Saat membuat widget untuk JQuery UI, saya menemukan contoh yang tepat untuk penerapan jQuery.proxy() sebagai pengganti closure.   Sebagai contoh, ini adalah kode widget saya secara garis besar:

(function($) {

  $.widget("thesolidsnake.jlSimpleTableEditor", {

    _create: function() {
       [[ NILAI this DISINI AKAN MERUJUK PADA PLUGIN SAYA ]]
       this.element;
       this.options;
       ...
    },

    ... // diabaikan

  }
})(jQuery);

Selama berada di method seperti _create, saya dapat memakai this untuk mendapatkan elemen yang diproses dengan this.element. Saya juga dapat memakai this untuk mendapatkan options (parameter) apa saja yang diberikan oleh pengguna dengan this.options.

Saya kemudian membuat sebuah elemen secara dinamis dan melakukan binding pada elemen tersebut, seperti pada kode program berikut ini:

(function($) {

  $.widget("thesolidsnake.jlSimpleTableEditor", {

    _create: function() {
       [[ Nilai this merujuk pada plugin saya ]]
       this.element;
       this.options;
       ...
       $("#test").bind("click", function() {
          [[ Nilai this merujuk pada elemen yang sedang di-klik ]]
          this.element;   // Tidak ditemukan, undefined
          this.options;   // Tidak ditemukan, undefined
       });
    },

    ... // diabaikan

  }
})(jQuery);

Permasalahannya sekarang dalam anonymous function tersebut, this tidak lagi merujuk ke objek plugin melainkan merujuk ke elemen yang sedang di-klik.

Bila saya ingin tetap mengakses this.element atau this.options di dalam anonymous  function tersebut, maka salah satu alternatif yang bisa saya ditempuh adalah dengan memakai closure.  Misalnya, saya bisa membuat sebuah variabel self yang berisi this, kemudian variabel self ini dapat diakses di anonymous  function tersebut, seperti yang terlihat seperti berikut ini:

(function($) {

  $.widget("thesolidsnake.jlSimpleTableEditor", {

    _create: function() {
       [[ Nilai this merujuk pada plugin saya ]]
       this.element;
       this.options;
       ...
       var self = this;
       $("#test").bind("click", function() {
          [[ Nilai this merujuk pada elemen yang sedang di-klik ]]
          this.element;   // Tidak ditemukan, undefined
          this.options;   // Tidak ditemukan, undefined
          self.element;   // Bekerja sesuai dengan yang diharapkan
          self.options;   // Bekerja sesuai dengan yang diharapkan
          ... // diabaikan
       });
    },

    ... // diabaikan

  }
})(jQuery);

Selain memakai closure, ada sebuah cara lain lagi yang lebih hemat memori, yaitu dengan menggunakan jQuery.proxy().  Method ini pada dasarnya akan memanggil Function.apply() di JavaScript yang memang ditujukan untuk memberi makna pada nilai this.  Contoh versi yang memakai jQuery.proxy() akan terlihat seperti berikut ini:

(function($) {

  $.widget("thesolidsnake.jlSimpleTableEditor", {

    _create: function() {
       [[ Nilai this merujuk pada plugin saya ]]
       this.element;
       this.options;
       ...
       $("#test").bind("click", $.proxy(function() {
          ...
          this.element;   // Nilainya sesuai dengan yang diharapkan
          this.options;   // Nilainya sesuai dengan yang diharapkan
          ... // diabaikan
       }, this));
    },

    ... // diabaikan

  }
})(jQuery);

Memahami struktur kode program function($) di jQuery

JavaScript adalah bahasa yang fleksibel.  Dan saking fleksibelnya, kadang-kadang memungkinkan syntax yang terlihat agak aneh.  Misalnya, ini adalah syntax di jQuery yang saya temukan di sebuah buku:

jQuery.noConflict();
(function($) {
  $(function() {
      ... // isi kode program disini
  });
})(jQuery);

Apa maksud dari perintah di atas?  Saya butuh waktu untuk mencernanya!!

Pertama-tama, di JavaScript, dollar ($) dan underscore (_) adalah karakter yang valid sebagai nama variabel.  Dengan demikian, dollar($) adalah nama variabel yang diperbolehkan.  Bila saya membongkar kode program jQuery, saya akan menemukan baris seperti ini:

// Expose jQuery to the global object
window.jQuery = window.$ = jQuery;

Dengan demikian, isi variabel global window.jQuery dan window.$ pada dasarnya adalah sama dengan isi variabel jQuery.  Dan karena jQuery dan $ adalah variabel global, mereka bisa dipanggil tanpa menambahkan window.

Untuk memahami arti syntax referensi di atas, saya akan membuang beberapa bagian sehingga syntax tersebut terlihat seperti berikut ini:

jQuery.noConflict();
(function($){...})(jQuery);

Memanggil keyword function dengan cara ini menunjukkan pemakaian function expression.  Function expression ini akan menghasilkan sebuah anonymous function yang tidak bernama tetapi dapat langsung dipakai.  Versi lain yang lebih mudah dipahami adalah:

// mendeklarasikan anonymous function dengan function expression, dan
// langsung memanggilnya dengan melewatkan parameter berupa jQuery
(function($){...})(jQuery);

// sama saja dengan

// mendeklarasikan function secara eksplisit
function test($) { ... }
// memanggil function dengan melewatkan parameter berupa jQuery
test(jQuery);

Ok, sekarang saya sudah mulai memahaminya.  Ini hanya versi 1 baris untuk mendeklarasikan sebuah function dan langsung memanggilnya.  Sekarang, saya berusaha memahami isi function expression tersebut:

function($) {
  $(function() {
     ...
  }
}

Karena parameter $ akan dipanggil dengan melewatkan variabel jQuery, maka variabel $ didalam function expression akan merujuk pada variabel  jQuery.  Dengan demikian, sebenarnya kode program di atas sama saja dengan:

function($) {
  // karena $ === jQuery
  jQuery(function() {
     ...
  });
}

Seperti yang tertera di dokumentasi jQuery, memanggil jQuery(callback) sama dengan memanggil $(document).ready(callback) dimana callback akan dikerjakan setelah DOM selesai di-load.

Lalu, apa tujuannya? ¬†Syntax yang berusaha saya pahami tersebut dipakai untuk menghindari konflik dimana ada framework lain yang sudah memakai variabel $. ¬†Perlu diingat bahwa $ adalah sebuah nama variabel biasa, sama seperti variabel a, ¬†b, c, dan sebagainya; ¬†hanya saja karena dipilih oleh jQuery ¬†ia menjadi agak ‘keramat’. ¬†Bila ada kode program lain yang mengisi nilai variabel global $, bisa jadi nilai $¬†bukan lagi ¬†jQuery. ¬†Dengan sebuah function expression yang menerima parameter $ dimana dipanggil dengan melewatkan jQuery, maka nilai parameter $¬†bisa dipastikan adalah ¬†jQuery.