Me-manage History Browser Dengan jQuery dan History.js Pada Halaman Web yang Menggunakan AJAX

Salah satu hal yang harus di-handle ketika menggunakan teknik AJAX adalah history di browser. Saat menggunakan AJAX untuk mengambil data di suatu alamat, proses pengambilannya dilaksanakan secara asynchronous tanpa memindahkan halaman, sehingga browser tidak mencatat history dari alamat yang pernah kita kunjungi sebelumnya. Hal ini tentu saja akan merepotkan jika nanti kita ingin mengakses suatu alamat tertentu yang telah diakses sebelumnya dan ternyata datanya diambil dengan menggunakan AJAX.

Pada kasus yang saya alami kali ini, ceritanya saya ingin membuat sebuah aplikasi web yang kira-kira kerangka tampilannya seperti ini:

Skema website yang akan dibangun.

Skema website yang akan dibangun.

Masing-masing link di menu navigasinya akan mengarah ke suatu alamat di server yang nantinya dengan menggunakan AJAX, isinya akan diletakkan di sebuah <div> yang ada di tengah halaman tersebut.

Sederhana bukan? Tapi ternyata implementasinya tidak terlalu sederhana juga. -_-

Saya mengembangkan halaman ini dengan menggunakan MVC framework untungnya, jadi tidak harus repot saat harus menangani request yang masuk ke server.

Untuk mengimplementasikan halaman ini, ceritanya saya memberikan sebuah class bernama ajax-link untuk setiap link yang akan mengakses halaman lain dengan teknik AJAX. Masing-masing link ini diberi 1 buah atribut khusus tambahan yang bernama target-element yang berisi lokasi tempat si data yang telah diambil akan diletakkan.

Ini contoh link-nya:

<a class="ajax-link" href="/site/about" target-element="div#content">Click me!</a>

Link di atas akan mengakses alamat /site/about dengan menggunakan AJAX, dan meletakkan hasilnya ke sebuah div dengan id yang bernama content.

Untuk menangani link ini, saya membuat sebuah fungsi yang akan mengambil konten dari URL yang diberikan (sourceUrl) dan kemudian menampilkannya ke tempat yang ditargetkan (targetElement):

function loadContent(sourceUrl, targetElement, callback) {

    // Fade out existing content in targetElement.
    $(targetElement).fadeOut("slow", function() {

        // Fade in loading image while downloading data.
        $("#loading").fadeIn("slow", function() {

            // Load data from the server.
            $(targetElement).load(sourceUrl, function(response, status, xhr) {
                if (status == "success") {
                    $("#loading").fadeOut("slow", function () {
                        $(targetElement).fadeIn("slow");

                        // Run the callback function if any.
                        typeof callback === 'function' && callback();
                    });
                }
                else if (status == "error") {
                    $("#loading").fadeOut("slow", function () {
                        $(targetElement).text("Error!");
                        $(targetElement).fadeIn("slow");
                    });
                }
                else {
                    // TODO: Handle unknown result.
                }
            });
        });
    });
}

Sisanya, tinggal menambahkan listener di setiap link yang memiliki class ajax-link dengan menggunakan jQuery:

$(".ajax-link").click(function() {

    // Prevent browser to do page redirection when the link is clicked.
    event.preventDefault();

    // Get the data source URL.
    var sourceUrl = $(this).attr("href");

    // Get the target element for data.
    var targetElement = $(this).attr("target-element");

    // Load content from server (the implementation is separated)
    loadContent(sourceUrl, targetElement, function() {
        // Callback function is here.
    });
});

Kode di atas sudah bisa dipakai, tapi masalahnya, setiap berpindah halaman, halaman yang telah diakses sebelumnya tidak akan tersimpan.

Setelah membaca-baca dan melakukan coba-coba, ternyata saat menggunakan AJAX, operasi terkait dengan history (termasuk tombol back dan forward harus di-handle sendiri.

Setiap browser punya operasi untuk menyimpan history saat membuka halaman web. Di setiap browser, operasi untuk menangani history bisa didapatkan dengan mengakses objek window.history. Panduan untuk memanipulasi history bisa dilihat di sini.

Pertamanya saya tidak terlalu khawatir soal history ini, tetapi kekhawatiran itu tiba-tiba timbul saat saya melihat tabel ini di halaman Mozilla Developer Network:

Setelah membaca-baca sumber lain lagi sembari googling, ternyata ada Javascript library yang bisa digunakan untuk menangani hal ini, namanya History.js.

Kekhawatiran soal isu kompatibilitas setidaknya bisa diminimalisir dengan menggunakan library ini. Dengan menggunakan History.js, fungsi untuk melakukan operasi manipulasi history telah tertanam di dalamnya dan tinggal digunakan saja.

$(document).ready(function() {

    // Initialize History.js
    var historyJs = window.History; // 'History', not 'history'.

    // Listen for click in every link that has .ajax-link class.
    $(".ajax-link").click(function() {

        // Prevent browser to do page redirection when the link is clicked.
        event.preventDefault();

        // Get the data source URL.
        var sourceUrl = $(this).attr("href");

        // Get the target element for data.
        var targetElement = $(this).attr("target-element");

        // Load content from server (the implementation is separated)
        loadContent(sourceUrl, targetElement, function() {

            // Save history.
            historyJs.pushState(null, '', sourceUrl);
        });
    });
});

Fungsi pushState yang dipanggil di bagian callback akan menyimpan URL yang sedang dibuka ke dalam history. Yap, dengan ini, fungsi history sudah berfungsi.

Ups, tapi belum selesai. Belum cukup perjuangannya.😀

Kalau kode di atas dicoba, walaupun tombol back ditekan, halaman yang ditampilkan tidak akan berubah. Itu karena fungsi untuk meng-handle tombol back di browser ketika diklik oleh pengguna belum dibuat.

Untuk menangani tombol back, sebenarnya browser memiliki event khusus bernama onpopstate yang bisa di-listen. Tetapi karena halaman ini menggunakan History.js, sebisa mungkin interaksi langsung ke implementasi browser sebaiknya dijembatani oleh library saja agar implementasinya lebih konsisten.

Jadi, untuk menangani tombol back dan forward, fungsi pushState dapat dimanfaatkan dengan lebih maksimal. Fungsi pushState membutuhkan 3 parameter: sebuah state object, judul halaman, dan URL.

Soal state object, dikutip dari Mozilla Developer Network:

  • state object — The state object is a JavaScript object which is associated with the new history entry created by pushState(). Whenever the user navigates to the new state, a popstate event is fired, and the state property of the event contains a copy of the history entry’s state object.

    The state object can be anything that can be serialized. Because Firefox saves state objects to the user’s disk so they can be restored after the user restarts the browser, we impose a size limit of 640k characters on the serialized representation of a state object. If you pass a state object whose serialized representation is larger than this to pushState(), the method will throw an exception. If you need more space than this, you’re encouraged to use sessionStorage and/or localStorage.

Untuk state object, saya menyimpan sebuah angka state counter dan target elemen dari si link yang diklik sebelumnya. Angka state counter tersebut disimpan dalam sebuah variabel global yang bernilai awal 0. Setiap kali link diklik, variabel state counter ini akan bertambah, kemudian dengan menggunakan History.js, dengan memasang fungsi bind, jika terdeteksi bahwa ada state yang berubah, secara otomatis akan dilakukan pengecekan apakah nomor state counter yang berubah telah ada sebelumnya. Jika ternyata angkanya telah ada, tentu saja artinya si pengguna telah mengklik tombol back di browser.

Implementasinya kira-kira seperti berikut:

var GLOBAL_STATE_COUNTER = 0;

$(document).ready(function() {

    // Initialize History.js
    var historyJs = window.History; // 'History', not 'history'.

    // Bind the adapter to listen 'statechange' event.
    historyJs.Adapter.bind(window, "statechange", function() {

        // Get state object.
        var state = historyJs.getState();

        // Check whether back button is pressed.
        if (state.counter < GLOBAL_STATE_COUNTER) {

            // Reload the content.
            loadContent(state.url, state.targetElement, function() {
                // Callback function is here.
            });
        }
    });

    // Listen for click in every link that has '.ajax-link' class.
    $(".ajax-link").click(function() {

        // Prevent browser to do page redirection when the link is clicked.
        event.preventDefault();

        // Get the data source URL.
        var sourceUrl = $(this).attr("href");

        // Get the target element for data.
        var targetElement = $(this).attr("target-element");

        // Load content from server (the implementation is separated)
        loadContent(sourceUrl, targetElement, function() {

            // Save history, and save the state object.
            historyJs.pushState(
               {
                   counter: GLOBAL_STATE_COUNTER,
                   targetElement: targetElement
               },
               '', // It should be filled with document title, but just ignore it for now.
               sourceUrl
            );

            // Increment global state counter.
            GLOBAL_STATE_COUNTER++;
        });
    });
});

Nah, dengan ini, tombol back dan forward-nya bisa berfungsi.😉

Oh iya, ada beberapa catatan terkait dengan kasus ini yang rasanya penting untuk diketahui saat membangun aplikasi web yang menggunakan AJAX, untuk implementasi di sisi server-nya. Berikut beberapa hal yang saya rasa perlu diperhatikan:

Judul Halaman

Kode di atas tidak meng-handle judul yang ditampilkan di halaman web yang dibuat. Hal ini tentunya nanti harus diatur agar tampilan halamannya lebih informatif. Untuk mengubah judul halaman, pembaca tinggal mengubah atribut document.title lewat Javascript.

Menangani Request AJAX dan non-AJAX

Jika aplikasi web yang dikembangkan semakin kompleks, masing-masing alamat di server bisa jadi akan mengembalikan response yang content type-nya berbeda-beda. Data yang dikembalikan mungkin bisa saja berupa JSON, gambar, atau mungkin pecahan dari suatu halaman jika request yang masuk ternyata adalah sebuah AJAX request. Namun jika request yang masuk ternyata request biasa, sang server harus mengembalikan response yang berbeda.

AJAX request bisa diidentifikasi dari header-nya. Biasanya ada entry di header yang bernama HTTP_X_REQUESTED_WITH yang menjadi penanda bahwa request dikirimkan dari sebuah XmlHttpRequest. Javascript library pada umumnya sudah menyertakan header ini, dan sang server tinggal membacanya saja.

Untuk mengenali apakah suatu request berasal dari AJAX atau tidak, beberapa framework tertentu sudah menyediakan fungsi built-in. Di ASP.NET misalnya, sang programmer tinggal menggunakan statement Request.IsAjaxRequest() di view-nya. Di framework MVC lain seperti Laravel ada fungsi Request::ajax(), dan di CakePHP ada fungsi $this->request->is('ajax'). Implementasinya berbeda-beda, tergantung bahasa dan framework yang digunakan.

Fiuh, kira-kira demikian cerita saya tentang menangani history dengan Javascript ini. Rasanya AJAX nanti (atau mungkin sudah?) akan jadi trend untuk aplikasi web modern yang dinamis dan responsif. Karena itu, hal-hal kecil seperti ini saya rasa cukup penting untuk diketahui. Semoga bermanfaat.😀

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s