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

Iklan

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 bubblingclick 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.

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