Belajar Membuat Widget Di Android

Pada artikel Membuat Aplikasi Android Dengan Groovy, saya menunjukkan cara membuat activity. Pada artikel Belajar Membuat Service Di Android, saya membuat sebuah service. Kali ini, saya akan mencoba membuat sebuah bentuk aplikasi Android yang disebut sebagai widget. Sama seperti activity, widget juga memiliki UI. Hanya saja UI pada widget biasanya terbatas karena ia ditujukan untuk disertakan di dalam aplikasi lain yang disebut sebagai widget host. Salah satu contoh widget host adalah home launcher bawaan Android atau pihak ketiga seperti Samsung TouchWiz.

Sebagai latihan, saya akan membuat sebuah widget sederhana yang akan menyalakan atau mematikan lampu flash bila di-klik. Dengan demikian, saya dapat memanfaatkan perangkat saya sebagai sebuah senter. Untuk itu, saya akan mulai dengan membuat sebuah proyek baru di Android Studio dengan nama MySenter. Seperti biasa, saya memilih Add No Activity pada saat membuat proyek baru. Setelah itu, saya men-klik kanan package com.snake.mysenter dan memilih menu New, Widget, App Widget seperti pada gambar berikut ini:

Membuat Widget Baru di Android Studio

Membuat Widget Baru di Android Studio

Saya mengisi dialog yang muncul seperti pada gambar berikut ini:

Membuat Widget Baru di Android Studio

Membuat Widget Baru di Android Studio

Saya mengisi nilai Minimum Width dan Minimum Height dengan 1 sel karena widget ini hanya sebuah tombol sederhana. Saya memilih Not Resizable agar widget ini selalu berukuran 1×1 sel. Seusai men-klik tombol Finish, Android Studio akan menambahkan sebuah <receiver> pada AndroidManifest.xml berupa:

<receiver android:name=".MySenter" >
    <intent-filter>
        <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
    </intent-filter>

    <meta-data
        android:name="android.appwidget.provider"
        android:resource="@xml/my_senter_info" />
</receiver>

Pada XML di atas, terdapat <meta-data> yang berisi referensi ke my_senter_info.xml. Isi my_senter_info.xml yang dihasilkan oleh Android Studio akan terlihat seperti berikut ini:

<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:minWidth="40dp" android:minHeight="40dp" android:updatePeriodMillis="86400000"
    android:previewImage="@drawable/example_appwidget_preview"
    android:initialLayout="@layout/my_senter" android:widgetCategory="home_screen|keyguard"
    android:initialKeyguardLayout="@layout/my_senter"></appwidget-provider>

Karena saya tidak perlu memperbaharui widget secara periodik, saya bisa mengubah nilai android:updatePeriodMillis menjadi "0".

Nilai android:initialLayout berisi referensi ke XML yang mendefinisikan layout untuk widget ini. Android Studio sudah membuatkan sebuah layout dengan nama my_senter yang berisi sebuah TextView. Sebagai latihan, saya akan mengubah layout tersebut sehingga hanya berisi sebuah ImageButton seperti pada:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent" android:layout_height="match_parent"
    android:padding="@dimen/widget_margin" android:background="@null">

    <ImageButton android:layout_width="fill_parent" android:layout_height="fill_parent"
        android:background="@null" android:id="@+id/tombol"
        android:contentDescription="Senter" android:src="@drawable/senter_mati"
        android:layout_alignParentStart="true" />

</RelativeLayout>

Saya perlu menambahkan dua gambar dengan nama senter_aktif.png dan senter_mati.png pada directory drawable yang mewakili tampilan widget. Karena ini hanya latihan, saya akan menggunakan gambar lingkaran berwarna kuning dan abu-abu. Untuk memakai latar belakang transparan, saya mengisi nilai android:background dengan @null. Agar hasil preview widget konsisten dengan tampilan widget, saya juga perlu mengubah nilai android:previewImage di my_senter_info.xml menjadi "@drawable/senter_mati".

Sekarang, saya perlu memberikan event handler yang akan dikerjakan bila ImageButton di-klik oleh pengguna. Untuk itu saya perlu melakukan perubahan pada class MySenter (sebuah turunan dari AppWidgetProvider) sehingga isinya menjadi seperti berikut ini:

package com.snake.mysenter;

import ...;

public class MySenter extends AppWidgetProvider {

    public static final String TOGGLE_SENTER = "TOGGLE_SENTER";

    public static boolean NYALA = false;

    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        for (int id : appWidgetIds) {
            Intent toggleIntent = new Intent(context, MySenter.class);
            toggleIntent.setAction(TOGGLE_SENTER);
            PendingIntent pendingToggleIntent = PendingIntent.getBroadcast(context, 0,
                    toggleIntent, 0);
            RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.my_senter);
            views.setOnClickPendingIntent(R.id.tombol, pendingToggleIntent);
            appWidgetManager.updateAppWidget(id, views);
        }
    }

    @Override
    public void onReceive(@NonNull Context context, @NonNull Intent intent) {
        super.onReceive(context, intent);
        if (intent.getAction().equals(TOGGLE_SENTER)) {
            NYALA = !NYALA;
            AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
            RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.my_senter);
            int iconBaru = NYALA ? R.drawable.senter_aktif: R.drawable.senter_mati;
            if (NYALA) {
                nyalakanSenter();
            } else {
                matikanSenter();
            }
            views.setInt(R.id.tombol, "setImageResource", iconBaru);
            appWidgetManager.updateAppWidget(new ComponentName(context, MySenter.class), views);
        }
    }

    public void nyalakanSenter() {

    }

    public void matikanSenter() {

    }

}

Menangani aksi untuk sebuah tombol di widget lebih rumit daripada di activity. Hal ini karena tombol di layar dapat langsung di-klik tanpa sebuah activity yang melekat padanya sehingga saya harus menggunakan PendingIntent. Selain itu, pengguna juga bisa menambahkan beberapa widget yang sama di dalam widget host yang berbeda (maupun sama) sehingga saya perlu menggunakan RemoteViews. Untuk menambahkan aksi yang akan dikerjakan pada saat tombol di-klik, saya menggunakan method setOnClickPendingIntent() milik RemoteViews. PendingIntent yang saya lewatkan akan melakukan broadcast yang dapat diterima melalui onReceive() (sebuah AppWidgetProvider adalah sebuah BroadcastReceiver!).

AppWidgetProvider sudah men-override method onReceive() untuk menangani broadcast penting bagi widget. Karena saya menambahkan broadcast baru saat tombol di-klik, maka saya perlu men-override onReceive() sambil tetap menyertakan super.onReceive(). Pada method ini, saya menyimpan status senter pada sebuah variabel statis bernama NYALA yang di-share bersama untuk seluruh ‘instance’ dari widget ini. Untuk mengubah icon dari warna kuning menjadi abu-abu, saya menggunakan RemoteViews.setInt() dengan melewatkan nama method berupa "setImageResource".

Untuk mengaktifkan lampu flash sebagai senter, pada dasarnya saya akan mengaktifkan preview kamera sambil menyalakan LED flash. Hasil preview tidak perlu ditampilkan pada UI karena saya hanya membutuhkan lampu flash saja. Bila preview kamera ini saya tutup, maka lampu flash juga akan ikut mati.

Karena memakai kamera, saya perlu menambahkan permission berikut ini di AndroidManifest.xml:

<uses-permission android:name="android.permission.CAMERA" />

Setelah itu, saya menambahkan kode program seperti berikut ini:

public class MySenter extends AppWidgetProvider {

  public static Camera camera;

  ...

  public void nyalakanSenter() {
    if (camera==null) {
        camera = Camera.open();
        Camera.Parameters params = camera.getParameters();
        params.setFlashMode(Camera.Parameters.FLASH_MODE_TORCH);
        camera.setParameters(params);
        camera.startPreview();
    }
  }

  public void matikanSenter() {
    if (camera!=null) {
        camera.stopPreview();
        camera.release();
        camera = null;
    }
  }

}

Pada kode program di atas, saya menyimpan instance Camera sebagai sebuah variabel statis. Menyimpan state pada BroadcastReceiver bukanlah sesuatu yang disarankan, misalnya state pada BroadcastReceiver tidak thread-safe. Untuk sesuatu yang lebih aman (namun lebih kompleks), saya perlu membuat sebuah service dan memanggilnya. Selain itu, kode program di atas hanya berlaku untuk perangkat yang mendukung Parameters.FLASH_MODE_TORCH dimana LED flash dapat diaktifkan selama melakukan preview atau merekam video.

Untuk menjalankan aplikasi ini, saya perlu men-edit konfigurasi app dan memilih Do not launch Activity seperti pada gambar berikut ini:

Menjalankan Widget Dari Android Studio

Menjalankan Widget Dari Android Studio

Setelah aplikasi berhasil di-deploy pada perangkat, saya perlu men-install widget pada sebuah widget host. Sebagai contoh, saya bisa menyisipkan widget pada home launcher. Langkah ini bisa berbeda karena setiap perangkat dari produsen berbeda biasanya juga memiliki home launcher yang berbeda. Pada Samsung TouchWiz, saya dapat melakukan ini dengan menahan agak lama pada home launcher sehingga muncul menu Widget seperti pada gambar berikut ini:

Memilih menu Widget untuk men-install widget

Memilih menu Widget untuk men-install widget

Setelah itu, saya dapat memilih Widget seperti pada gambar berikut ini:

Mencari widget yang hendak di-install

Mencari widget yang hendak di-install

Perhatikan bawah gambar yang dipakai disini adalah nilai dari android:previewImage. Setelah men-drag widget ke home launcher, saya dapat menyentuh widget tersebut untuk menyalakan LED flash kamera belakang:

Menyalakan senter dengan menyentuh tombol di widget

Menyalakan senter dengan menyentuh tombol di widget

Iklan

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