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

Menghasilkan SDOCML Berdasarkan Dokumentasi jQuery

Pada artikel di Menambah Content Assistant Untuk jQuery di Aptana Studio 3, saya mencoba untuk mempelajari cara kerja IDE dan fitur content assist pada platform Eclipse (karena Aptana Studio memakai Eclipse Platform). Saya berhasil membuat content assist untuk jQuery yang bekerja sesuai dengan harapan saya (belum tentu sesuai dengan harapan orang lain, karena ini adalah proyek eksperimen ūüôā ). Syaratnya adalah saya harus memiliki sebuah file SDOCML yang mendeskripsikan jQuery pada proyek yang sedang aktif. JavaScript memang tidak memiliki fitur dokumentasi yang standar dan SDOCML merupakan salah satu yang ada di pasaran. jQuery sendiri memiliki dokumentasi dalam bentuk XML yang mirip seperti SDOCML tapi tidak compatible sehingga saya tidak bisa memakainya secara langsung di Aptana Studio. Apa yang dilihat di http://api.jquery.com dihasilkan berdasarkan file XML tersebut.

Seiring waktu berlalu, file SDOCML yang saya buat bisa kadaluarsa karena API jQuery juga terus berevolusi. Untuk mengatasi masalah tersebut, saya membuat sebuah script Groovy yang kode programnya dapat dilihat di https://github.com/JockiHendry/sdocml-generator/blob/master/generate-jquery.groovy. Script ini akan menghasilkan SDOCML berdasarkan dokumentasi jQuery yang ada. Karena dibuat dengan bahasa pemograman Groovy, maka untuk menjalankannya dibutuhkan Java dan Groovy.

Kenapa memakai Groovy? Karena Groovy menyediakan segala sesuatu yang saya butuhkan sehingga saya bisa bekerja secara cepat. Groovy menyediakan CliBuilder sehingga saya hanya perlu mendaftarkan parameter dan Groovy (tepatnya Apache Commons CLI) akan memeriksa apakah pengguna mengisi parameter dengan benar. Groovy juga menyediakan XmlSlurper sehingga saya bisa membaca dokumentasi resmi jQuery dalam bentuk XML secara mudah. Selain itu, Groovy juga memiliki MarkupBuilder sehingga saya bisa menulis file XML dengan mudah (SDOCML pada dasarnya adalah file XML).

Script ini dapat dijalankan dengan menggunakan perintah seperti:

C:\> groovy generate-jquery.groovy -help
error: Missing required option: output
usage: generate-jquery [options]
 -file      retrieve XML API documentation from XML file
 -help            print this message
 -output    output JsDoc to this file
 -url        retrieve XML API documentation from this URL. Default
                  URL is http://api.jquery.com/resources/api.xml

Dokumentasi jQuery dipublikasikan dalam bentuk XML di lokasi http://api.jquery.com/resources/api.xml. Yang akan dilakukan oleh script ini adalah membaca dokumentasi jQuery tersebut lalu menerjemahkan menjadi file SDOCML yang dapat dipakai oleh Aptana Studio. Dengan demikian, saya bisa mendapatkan dokumentasi terbaru dengan mudah.

Bila komputer terkoneksi dengan internet, maka saya bisa menghasilkan file SDOCML berdasarkan dokumentasi jQuery yang terbaru dengan menggunakan perintah seperti:

C:\> groovy generate-jquery.groovy -url -output jquery.sdocml
Reading XML from url: http://api.jquery.com/resources/api.xml
File jquery.sdocml has been generated!

Bila seandainya saya tidak terkoneksi dengan internet, maka saya bisa menghasilkan file SDOCML berdasarkan file XML yang sudah saya download sebelumnya dengan menggunakan perintah seperti:

C:\> groovy generate-jquery.groovy -file jquery-api.xml -output jquery.sdocml
Reading XML from file: jquery-api.xml
File jquery.sdocml has been generated!

Setelah file SDOCML dihasilkan, saya tinggal meletakkannya pada proyek di Aptana Studio dan content assist-pun akan bekerja berdasarkan dokumentasi yang terbaru. Karena file SDOCML ini dihasilkan secara otomatis, bagian example akan terlihat sangat panjang sekali sesuai dengan yang ada di situs http://api.jquery.com.

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 #9: Mengorbankan Static Method

Pada Journal #1, saya melakukan perubahan pada ParserUtil di method getParentObjectTypes() dimana saya menganggap seluruh referensi jQuery adalah ke Function<jQuery> bukan Class<jQuery>.  Dengan kata lain, saya menganggap semua penggunaan jQuery berdasarkan instance, misalnya: $("p").blur(). Padahal, pada kenyataannya, ada beberapa method jQuery yang dapat diakses tanpa harus membuat instance, misalnya: $.ajax().  Method seperti ini adalah method static.   Dengan pendekatan yang saya tempuh saat ini, seluruh method, baik yang static maupun per-instace, akan ditampilkan dalam content assist saat pengguna mengetik jQuery atau $.   Saya pikir trade-off ini masih dapat diterima bila dibandingkan dengan keuntungan yang saya peroleh saat melakukan coding jQuery nanti (membuat widget, misalnya).

jQuery mengandung beberapa definisi class yang dapat dipakai diluar, misalnya Callbacks, Deferred, dan jqXHR.  Kebanyakan dari class tersebut dapat dibuat dengan constructor berupa method static di object jQuery.   Karena saya tidak mendukung method static, maka saya mendokumentasikan seluruh constructor sebagai function biasa (per-instance).

Permasalahan yang saya hadapi adalah content assist tidak bekerja dengan baik untuk constructor tersebut.  Aptana Studio menganggap seluruh constructor tersebut mengembalikan sebuah Object yang universal.

Mengapa demikian?  Untuk menjawab pertanyaan ini, saya pun melakukan penelusuran, yang membawa saya pada class JSNodeTypeInferrer. Class ini memiliki sebuah method dengan nama visit() dimana berisi cuplikan seperti berikut ini:

public void visit(JSGetPropertyNode node)
{
   ... // kode diabaikan
   // TODO Combine with similiar code from ParseUtil.getParentObjectTypes
   if (JSTypeCOnstants.FUNCTION_JQUERY.equals(typeName)
          && lhs instanceof JSIdentifierNode
          && (JSTypeConstants.DOLLAR.equals(lhs.getText()) || JSTypeConstants.JQUERY.
                 .equals(lhs.getText())))
   {
       typeName = JSTypeConstants.CLASS_JQUERY;
   }
   ... // kode diabaikan
}

Wow!  Sesuai dengan komentar TODO di atas-nya,  disini telah terjadi duplikasi kode program.  Bila saya menghilangkan logic di ParseUtil.getParentObjectTypes() (petualangan di journal #1), maka saya WAJIB  menghilangkan bagian yang ini juga!  Ini bisa membingungkan orang-orang, terutama saya yang berasumsi bahwa perubahan pada ParseUtil sudah menyelesaikan masalah, tetapi disini juga perlu diubah.  Untuk itu, saya memberikan komentar pada baris di atas sehingga mereka tidak akan mengubah Function<jQuery> menjadi Class<jQuery>.

Tapi petualangan belum berakhir sampai disini.  Setelah tipe class bisa ditemukan dengan baik, Apatana Studio tetap tidak mengembalikan method yang spesifik untuk class tersebut, melainkan hanya class bersifat umum yaitu Object.

Mengapa demikian?  Penelusuran kode program membawa saya ke method addTypeProperties() di class JSContentAssistProcessor.  Pada method ini terdapat sebuah baris kode program seperti berikut ini:

Collection properties = indexHelper.getTypeMembers(index, allTypes);

Lalu, apa isi dari method getTypeMembers()? Isinya seperti berikut ini:

public Collection getTypeMembers(Index index, List<String> typeNames) {
return CollectionsUtil.union(getMembers(index, typeNames), getMembers(getIndex(), typeNames));
}

Pada saat melakukan tracing, masing-masing getMembers() telah mengembalikan method yang valid dan benar. ¬†Tetapi pada saat keduanya digabungkan kedalam sebuah Set, selalu ada yang hilang! ¬†Hal ini tiba-tiba mengingatkan saya pada ‘bug‘ yang saya temui di journal #4¬†sehubungan dengan nilai di Set yang menghilang. ¬† Ternyata benar, class FunctionElement belum memiliki method equals() dan hashCode(). ¬†Saya segera memilih menu Source, Generate hashCode() and equals().

Sampai disini semua sudah mendingan, tapi petualangan belum berakhir.  Mengapa demikian?  Karena method yang muncul selalu terduplikasi, ada yang mengandung dokumentasi dan ada yang tidak.  Padahal, saya menginginkan hanya yang mengandung dokumentasi saja yang ditampilkan (dan tampilkan versi tidak terdokumentasi bila seandainya tidak ada yang lebih baik).

Untuk mengatasi permasalahan tersebut, saya mengubah method visit di JSNodeTypeInfererrer menjadi seperti:

public void visit(JSGetPropertyNode node)
{
   ... // kode program diabaikan
   if (properties!=null)
   {
      for (PropertyElement property: properties)
      {
          if (property instanceof FunctionElement)
          {
              FunctionElement function = (FunctionElement) property;
              boolean adaVersiTerdokumentasi = false;
              if (function.getDescription()==null || function.getDescription().length()==0) {
                  for (PropertyElement item: properties) {
                     if (item instanceof FunctionElement && item.getName().equals(function.getName()) && 
                         item.getDescription()!=null && item.getDescription().length()>0) {
                         adaVersiTerdokumentasi = true;
                     }
                  }
              }
              if (adaVersiTerdokumentasi) continue;

              ... // kode program diabaikan
          }

          ... // kode program diabaikan
      }
   }
}

Belajar dari petualangan hari ini, saya sudah beberapa kali melakukan filtering elemen yang mengandung dokumentasi.  Sepertinya logika ini bisa dikumpulkan ke sebuah class sehingga saat melakukan perubahan, saya cukup mengubah class tersebut.

Sekarang, saya akan melakukan pengujian, misalnya, saya akan memanggil method static Callback seperti berikut ini:

Menampilkan Content Assist Untuk $

Menampilkan Content Assist Untuk $

Context info untuk method tersebut akan muncul seperti yang terlihat di gambar berikut ini:

Menampilkan Context Info Untuk Callbacks

Menampilkan Context Info Untuk Callbacks

Setelah itu, bila saya memanggil salah satu method object Callbacks, maka content assist akan muncul, seperti berikut ini:

Menampilkan Content Assist Untuk  Object Callbacks

Menampilkan Content Assist Untuk Object Callbacks

Beruntungnya, content assist pada saat terjadi chaining tetap bekerja dengan baik seperti yang terlihat di gambar berikut ini:

Content  assist setelah method add di Calllbacks

Content assist setelah method add di Calllbacks

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

Aptana Journal #1: Menambahkan Content Assistant jQuery

Melanjutkan dari artikel tentang Mengubah Kode Program Aptana Studio 3, kali ini saya ingin melakukan sedikit perubahan.   Pada saat memakai Aptana Studio 3 untuk meng-edit kode program jQuery, saya menemukan bahwa content assist tidak berfungsi sebagaimana harusnya.  Sama sekali tidak ada bantuan untuk fungsi-fungsi di class JQuery, seperti yang terlihat pada gambar berikut ini:

Function jQuery Tidak Ditampilkan Oleh Aptana

Function jQuery Tidak Ditampilkan Oleh Aptana

Sementara itu, pada file JavaScript (js), kadang-kadang content asisst untuk jQuery dapat ditampilkan dengan baik dan kadang-kadang tidak sama sekali.

Mengapa demikian?  Untuk menjawab pertanyaan ini, saya perlu membedah isi file JSContentAssistProcessor.java yang ada di plugin com.aptana.editor.js.   Saya dapat menampilkan class ini secara cepat dengan menekan tombol Ctrl+Shift+R di Eclipse RCP dan mengetikkan namanya.  Method yang menjadi pusat perhatian adalah doComputeCompletionProposal(). Berikut ini adalah cuplikan dari isi method tersebut:

protected ICompletionProposal[] doComputeCompletionProposal(ITextViewer viewer, int offset, char activationChar, boolean autoActivated) {
  ... // diabaikan
  LocationType location = getLocationType(document, offset);
  switch (location) {
    case IN_PROPERTY_NAME:
       addProperties(result, offset);
       break;

    case IN_VARIABLE_NAME:
    case IN_GLOBAL:
    case IN_CONSTRUCTOR:
       addKeywords(result, offset);
       addCoreGlobals(result, offset);
       addProjectGlobals(result, offset);
       addSymbolsInScope(result, offset);
       break;
    ... // diabaikan
  }
  ... // diabaikan
}

Agar method pada object JQuery ditampilkan, Aptana Studio harus mengerjakan method addProperties(). Atau dengan kata lain, getLocationType() harus mengembalikan IN_PROPERTY_NAME. Tapi ternyata yang terjadi adalah nilai IN_GLBOAL yang dikembalikan.

Wow, kenapa bisa begitu?  Untuk menjawab pertanyaan ini, saya perlu menelusuri lagi method getLocationType(). Salah satu method yang dipanggil olehnya adalah method getActiveASTNode(). Dari namanya, method ini berfungsi untuk mengembalikan Abstract Syntaxt Tree (AST). Berikut ini adalah cuplikan dari method tersebut:

IParseNode getActiveASTNode(int offset)
{
  IParseNode result = null;
  try 
  {
    ... // diabaikan
  } 
  catch (Exception e)
  {
    // ignore parse error exception since the user will get markers and/or entriesi n Problems View
  }
  return result;
}

Ok, mulai terlihat ada titik terang.  Saya melihat bahwa jika ada kesalahan parsing, maka kesalahan tersebut akan diabaikan!  Dengan perasaan ingin tahu, saya menambahkan e.printStackTrace() pada bagian catch. Ternyata benar! Ada kesalahan parsing yang terjadi padahal syntax JavaScript saya adalah syntax yang valid dan benar.

Mengapa bisa demikian?  Untuk melakukan parsing, Aptana Studio akan memanggil class JSParser.  Dan perlu diketahui bahwa saya sedang menyertakan JavaScript di dalam HTML!  Karena dokumen saya adalah campuran dari HTML dan JavaScript, tentu saja JSParser yang hanya mengola JavaScript akan protes keras.

Lalu apa saya harus mengubah isi file JSParser.java?  Tidak!  Tidak secara langsung, karena file ini dihasilkan oleh Beaver, sebuah parser generator.   Aptana Studio juga memakai JFlex sebagai scanner generator.

Apa itu scanner? ¬†Agar komputer bisa mengerti kode program yang diketik, maka ia perlu memecah kode program tersebut ke dalam token (ibaratkan dengan “kata” dalam dunia manusia). ¬†Bagian kode program yang memiliki tugas seperti ini disebut sebagai scanner.

Apa itu parser? ¬†Token-token yang terpisah hasil dari scanner perlu dikelompokkan berdasarkan aturan yang berlaku sehingga bisa dimengerti komputer (ibaratkan dengan “kalimat” yang dirangkai dari “kata” dalam dunia manusia). ¬†Bagian kode program yang memiliki tugas seperti ini disebut sebagai parser.

Lalu apa itu scanner generator dan parser generator?  Setahun setelah saya lulus kuliah, saya membantu adik kelas untuk membuat sebuah bahasa pemograman beserta compiler-nya.  Saat itu kami harus membuat sendiri scanner dan parser secara manual.  Ternyata, scanner dan parser dapat dihasilkan secara otomatis dimana kita hanya perlu memberi tahu aturan token & syntax yang berlaku.  Scanner generator seperti JFlex akan menghasilkan file Java berdasarkan aturan di file *.flex.  Parser generator seperti Beaver akan menghasilkan file Java berdasarkan aturan di file *.grammar.  Kenapa tidak buat sendiri saja dari awal?  Selain alasan waktu, juga algoritma yang kita pakai belum tentu lebih baik dari yang sudah ada.

Saya dapat menemukan file-file yang berhubungan generator di folder parsing seperti yang terlihat pada gambar berikut ini:

Isi Folder Parsing

Isi Folder Parsing

Karena saya ingin membuang HTML pada saat melakukan parsing JavaScript, tanpa mengubah struktur bahasa JavaScript, maka saya cukup melakukan perubahan pada JFlex di file JS.flex.  Perubahan yang saya lakukan bersifat naif tanpa memperhatikan aspek lain dan hanya untuk keperluan pribadi.  Saya tidak tahu apakah ini berguna bagi orang lain dan saya tidak ada kaitannya dengan tim pengembang Aptana Studio (karena ini software open-source, saya pikir, saya bebas mengubahnya untuk keperluan pribadi).

Untuk mempermudah melihat kesalahan selama bermain-main dengan JFlex, saya menambahkan option %debug di file JS.flex.  Selain itu, saya menambahkan definisi macro seperti berikut ini:

ScriptOpen = "<script" ("type=" \.*)? ~">"
ScriptClose = "</script>"
HTMLTag = "<html>"

Macro di JFlex pada dasarnya berisi regular expression yang dicocokkan dengan input (sehingga bisa mewakili sebuah token).

Berikutnya saya juga menambahkan state baru yaitu HTML sehingga terlihat seperti berikut ini:

%state DIVISION, REGEX, HTML

State YYINITIAL adalah state yang selalu ada dan akan aktif pertama kali.   Saya menambahkan dua buah lexical rule di YYINITIAL sehingga bila ditemukan tag HTML atau setelah tag </script>, akan beralih ke state HTML.  Isinya akan terlihat seperti berikut ini:

<YYINITIAL> {

  {HTMLTag}  { yybegin(HTML); }

  {ScriptClose}  { yybegin(HTML); }

  ... // diabaikan

}

Setelah itu, pada state HTML, saya akan beralih ke state YYINITIAL (melakukan parsing JavaScript secara normal) bila ditemukan tag <script> pembuka.  Isi lexical rule untuk state HTML akan terlihat seperti berikut ini:

<HTML> {
  {ScriptOpen}  { yybegin(YYINITIAL); }
  [\"']\\?|.  { /* ignore in HTML */ }
}

Langkah berikutnya setelah melakukan perubahan file JS.flex adalah menghasilkan sebuah class scanner berdasarkan aturan yang baru.  Class yang dimaksud adalah JSFlexScanner.java.  Class ini akan dihasilkan secara otomatis oleh JFlex.  Caranya adalah dengan men-klik kanan pada file build.js.xml, memilih Run As,  Ant Build.

Setelah proses build selesai,  saya perlu men-klik kanan package com.aptana.js.core.parsing dan memilih refresh seperti yang terlihat pada gambar berikut ini:

Class yang dihasilkan oleh scanner & parser generator

Class yang dihasilkan oleh scanner & parser generator

Bila terjadi kesalahan pada file JSParser.java, saya perlu membuka file tersebut, kemudian menekan tombol Ctrl+Shift+O untuk memperbaharui import.  Selain itu, build.js.xml juga mengandung salah ketik sehingga menghasilkan package di lokasi baru (saya tidak memeriksa apakah versi terbaru sudah diperbaiki oleh tim developernya).

Langkah terakhir, saya membuka file ParseUtil.java di package com.aptana.editor.js.contentassist.  Pada method getParentObjectTypes(), terdapat komentar FIXME: (hopefully temporary) hack to fixup static properties on $ and jQuery. Saya menghilangkan bagian tersebut dan hanya menyisakan result.add(type);.

Sekarang, bila saya menjalankan proyek ini, saya akan menemukan bahwa content assist untuk jQuery telah bekerja seperti yang terlihat di gambar berikut ini:

Content Assist Untuk jQuery Yang Sudah Bekerja

Content Assist Untuk jQuery Yang Sudah Bekerja

Perlu diperhatikan bahwa saya perlu menyertakan sebuah file source ¬†jQuery seperti file¬†jquery-1.8.2.js di proyek (letaknya boleh dimana saja). ¬†Aptana Studio akan men-parsing file ini dan memberikan item content assist berdasarkan hasil parsing tersebut. ¬†Hal ini berbeda dengan cara yang ditempuh di Adobe Dreamweaver CS5.5, dimana hint untuk jQuery bersifat ‘statis‘ sehingga untuk menyesuaikan dengan jQuery versi baru, saya perlu meng-update Dreamweaver.

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

Memakai Closure Di JavaScript

Belakangan ini, saya kerap terlibat pada pengembangan front-end dan mau tidak mau saya harus berhadapan dengan JavaScript.  Walaupun namanya mirip-mirip Java, tapi fiturnya jelas berbeda.  Salah satunya adalah JavaScript memiliki apa yang disebut sebagai closure.   Awalnya, sangat sulit bagi saya untuk mengerti apa itu closure dan bagaimana penerapannya.  Sejuta kata-kata yang saya temui serasa tidak bisa dicerna.  Tapi beruntung, sebuah kasus dalam development membuat saya memahami closure dan penerapannya.

Saya memiliki HTML + jQuery sederhana yang beri 4 tombol (<button>) seperti berikut ini:

<html>
  <head>
  <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.8.1/jquery.min.js"></script>
  <script>
     $(document).ready(function(){
       $("#tombolA").click(function(){alert("Solid");});
       $("#tombolB").click(function(){alert("Solid");});
       $("#tombolC").click(function(){alert("Snake");});
       $("#tombolD").click(function(){alert("Snake");});
     });
  </script>
  </head>
  <body>
    <button id="tombolA">Tombol A</button>
    <button id="tombolB">Tombol B</button>
    <button id="tombolC">Tombol C</button>
    <button id="tombolD">Tombol D</button>
  </body>
</html>

Tombol A dan Tombol B bila ditekan akan memunculkan pesan “Solid”. ¬†Sementara itu, Tombol C dan Tombol D bila ditekan akan memunculkan pesan “Snake”. ¬†Jika dilihat-lihat, saya membuat 4 function berbeda untuk itu! ¬†Ini berarti jika saya mau mengganti pesan “Solid” menjadi “Liquid”, saya harus mengubah 2 function yg berbeda… ¬†Kurang efisien, bukan?

Saya menginginkan hanya 1 function yang bisa dipakai oleh keempatnya. ¬†Dan function ini harus memiliki parameter untuk menampung nilai seperti “Solid”, “Snake”, dan sebagainya. ¬†Kira-kira saya ingin seperti begini:

function buatPesan(pesan) {
  alert(pesan);
}
$(document).ready(function(){
  $("#tombolA,#tombolB").click(buatPesan("Solid"));  // KODE PROGRAM INI MASIH SALAH
  $("#tombolC,#tombolD").click(buatPesan("Snake"));  // KODE PROGRAM INI MASIH SALAH
});

Tapi sayangnya kode program di atas masih salah.  Saya tidak bisa memanggil sebuah function begitu saja di click(), tapi saya harus mengembalikan sebuah function (dalam bentuk nama function).

Beruntungnya, JavaScript mendukung inner function (function di dalam function).  Jika saya mengembalikan sebuah inner function, maka telah terjadi closure, dimana variabel yang ada di outer function (function diluarnya) akan tetapi di-ingat!  Jadi, saya bisa membuat kode program seperti berikut ini:

function buatPesan(pesan) {
  function f() {
    alert(pesan);
  }
  return f;
}
$(document).ready(function(){
  $("#tombolA,#tombolB").click(buatPesan("Solid"));
  $("#tombolC,#tombolD").click(buatPesan("Snake"));
});

Pada kode program di atas, sebuah fungsi yang sama dapat dipakai ulang untuk setiap tombol yang ada.  Dan saya tidak membutuhkan variabel global dan sebagainya, karena dengan closure, nilai parameter yang diberikan akan tetap diingat oleh JavaScript.

Menambah Tombol Delete Untuk Setiap Baris Di jqGrid

jqGrid (http://www.trirand.com/blog/) adalah sebuah plugin untuk JQuery yang dapat dipakai untuk menampilkan data dalam bentuk tabel/grid.  jqGrid dapat melakukan proses filter dan pagination (navigasi halaman) secara otomatis, sementara programmer tinggal memberikan data dari server dalam bentuk JSON atau XML.

Salah satu pertanyaan yang muncul pada saat saya memakai jqGrid adalah bagaimana membuat tombol delete (hapus) untuk setiap baris di tabel? ¬†Apa saya harus memodifikasi data yang dikirim dari server untuk membuat kolom ‘semu’? ¬†Setelah melakukan google sejenak, saya menemukan beberapa contoh penyelesaian yang agak ‘rumit’ yaitu dengan melakukan data setelah di-load (dengan demikian membutuhkan JavaScript pada event gridComplete). ¬†Tapi tidak lama kemudian, saya menemukan jawaban yang lebih sederhana namun tetap elegan, yaitu dengan menggunakan formatter dan ¬†formatoptions.

Pada saat saya membuat jqGrid, saya menambahkan sebuah kolom kosong di colNames.  Saya tidak perlu mengirimkan data dari server untuk kolom ini, karena nantinya kolom ini akan berisi tombol hapus.   Lalu, pada colModel, saya menambahkan formatter dan formatOptions untuk kolom kosong ini seperti berikut ini:

...
colNames: ['First Name', 'Last Name','Birth Date', ''],

colModel: [
  {name:'firstName', index:'firstName', width:150},
  {name:'lastName', index:'lastName', width:100},
  {name:'birthDateString', index:'birthDate', width:100},
  {name: '', index: 'action', width:75, sortable:false, formatter:'actions',
  formatoptions:{
     editbutton: false, delbutton:true, mtype: "GET",
     delOptions:{
        caption: 'Hapus',
        msg: 'Anda ingin menghapus record ini?',
        bSubmit: 'Hapus',
        mtype: 'GET',
        url: 'contacts/listrowdelete',
        bCancel: 'Batal'
     }
  }} ],
  ...

Dengan memberi nilai formatter dengan actions, maka kolom ini akan di-isi dengan tombol-tombol yang mewakili aksi tertentu.  Untuk melakukan pengaturan lebih lanjut, saya melakukan formatoptions.   Nilai false pada editbutton menyebabkan tombol tersebut tidak akan ditampilkan.    Nilai mtype secara default adalah POST.  Nilai ini menentukan bagaimana data dikirim ke server pada tombol delete ditekan.  Saya memilih GET untuk mempermudah debugging.   Berikut ini adalah contoh tampilannya:

Tampilan jqGrid dengan Tombol Delete Per Baris

Tampilan jqGrid dengan Tombol Delete Per Baris

Nilai delOptions menentukan bagaimana karakteristik dialog yang muncul pada saat tombol hapus ditekan.  Nilai ini secara internal akan dikirimkan pada saat memanggil fungsi delGridRow().  Dokumentasi untuk nilai yang ada dapat dilihat di http://www.trirand.com/jqgridwiki/doku.php?id=wiki:form_editing#delgridrow.

Berikut ini adalah contoh dialog yang muncul pada saat tombol hapus ditekan:

Dialog Hapus

Dialog Hapus

Bila tombol Delete ditekan, maka URL yang ditentukan di url akan dipanggil.  Pada contoh ini, URL yang dipanggil adalah http://localhost:8080/test/contacts/listrowdelete?oper=del&id=1.  jqGrid secara otomatis menambahkan parameter oper dan id, dimana id adalah baris yang sedang terpilih.

Dengan demikian, pada sisi server, di controller, saya tinggal membuat sebuah handler seperti:

@RequestMapping(value="/listrowdelete", params="oper=del", method=RequestMethod.GET)
public String listRowDelete(@RequestParam(value="id", required=true) Long id, Model uiModel) {
  // kode program hapus dari database disini
}