Memulai Pembuatan Aplikasi Web dengan Express.js (7): Pagination dan Upload Foto

Muhammad Arslan 5 November 2016

Memulai Pembuatan Aplikasi Web dengan Express.js (7): Pagination dan Upload Foto

Setelah mengikuti episode sebelumnya. Sekarang saatnya kita membuat CRUD untuk mencatat anggota yang bergabung dengan organisasi ktia. Sebelum mengikuti tutorial ini, Anda diharuskan untuk mengikuti tutorial sebelumnya yaitu:

Persiapan

Sebagai persiapan, silahkan buat sebuah folder dengan nama uploads di dalam folder public. Folder tersebut akan digunakan untuk menyimpan foto yang di-upload oleh user untuk mengganti foto anggota. Lalu, karena kita akan beberapa modul Node.js berikut:
$ npm install formidable
$ npm install pagination
Dan ada perubahan sedikit untuk model anggota.js dengan menambahkan field propic_path seperti pada kode berikut:
var mongoose = require('mongoose');
var Schema = mongoose.Schema;

var anggotaSchema = new Schema({ nama: { type: String, required: true, unique: true }, tempat_lahir: String, tanggal_lahir: Date, email: String, telepon: String, gender: String, keterangan: String, propic_path: String,

alamat: [{alamat: String, types: String}], // multirecord kontak: [{kontak: String, types: String}], // multirecord

}, { timestamps: true });

var Anggota = mongoose.model('Anggota', anggotaSchema);

module.exports = Anggota;

Pagination di Halaman Index Anggota

Ubah bagian import modul pada file routes/anggota.js menjadi seperti berikut:
var express = require('express');
var Anggota = require('../models/anggota');
var Auth_mdw = require('../middlewares/auth');
var moment = require('moment');
var formidable = require('formidable');
var fs = require('fs');
var pagination = require('pagination');

var router = express.Router(); var session_store;

Pada kode diatas, kita menggunakan modul formidable, fs, dan pagination. Sekarang mari kita rombak route "/" untuk menghasilkan dan menampilkan pagination:

router.get('/', function(req, res, next) {
    session_store = req.session;


    // pagination
    page = 1;
    offset = 0;

    if (req.query.hasOwnProperty('page')){
        page = req.query.page;
        offset = (req.query.page - 1)*3;
    }

    Anggota.find({}, function(err, rows){
        Anggota.count({}, function(err, count){

            var bootstrapPaginator = new pagination.TemplatePaginator({
                                    prelink:'/anggota', current: page, rowsPerPage: 3,
                                    totalResult: count,
                                    template: function(result) {
                                        var i, len, prelink;
                                        var html = '<div><ul class="pagination">';
                                        if(result.pageCount < 2) {
                                            html += '</ul></div>';
                                            return html;
                                        }
                                        prelink = this.preparePreLink(result.prelink);
                                        if(result.previous) {
                                            html += '<li><a href="' + prelink + result.previous + '">Sebelumnya</a></li>';
                                        }
                                        if(result.range.length) {
                                            for( i = 0, len = result.range.length; i < len; i++) {
                                                if(result.range[i] === result.current) {
                                                    html += '<li class="active"><a href="' + prelink + result.range[i] + '">' + result.range[i] + '</a></li>';
                                                } else {
                                                    html += '<li><a href="' + prelink + result.range[i] + '">' + result.range[i] + '</a></li>';
                                                }
                                            }
                                        }
                                        if(result.next) {
                                            html += '<li><a href="' + prelink + result.next + '" class="paginator-next">Berikutnya</a></li>';
                                        }
                                        html += '</ul></div>';
                                        return html;
                                    }
                                });

            res.render('anggota/index', { session_store:session_store, anggota: rows, paginator: bootstrapPaginator.render(), offset: offset });

        });

    }).skip(offset).limit(3);
});

Sebelum masuk ke query, kita konfigurasi terlebih dahulu untuk menghitung pagination yang akan ditampilkan. Mulai dari offset dan banyaknya item yang akan ditampilkan. Setelah query mengambil dokumen, kita eksekusi di dalamnya query untuk menghitung semua dokumen yang ada di collection anggota. Dan kita racik elemen pagination yang akan ditampilkan dengan cukup rumit. Kode tersebut merupakan adaptasi dari contoh kode yang ada di dokumentasi resmi NPM pagination. Anda dapat melihatnya untuk melihat contoh lainnya.

Barulah kita tampilkan paginator di halaman index anggota. Lalu untuk view anggota/index.jade sekarang berubah menjadi seperti ini:

extends ../layout/base
block content  
    section.content-header
        h1
            | Daftar Anggota
            small Kelola anggota organisasi Anda disini
    // Main content
    section.content
        .row
            .col-xs-12
                - if (messages.msg_error)
                    .alert.alert-danger.alert-dismissable
                        button.close(type='button', data-dismiss='alert', aria-hidden='true') ×
                        | #{messages.msg_error}
                - if (messages.msg_info)
                    .alert.alert-success.alert-dismissable
                        button.close(type='button', data-dismiss='alert', aria-hidden='true') ×
                        | #{messages.msg_info}
                .box
                    .box-header
                        h3.box-title Anggota
                        .box-tools
                            a(href="/anggota/add").btn.btn-primary.btn-sm
                                i.glyphicon.glyphicon-plus  
                                | &nbsp; Tambah Anggota
                    // /.box-header
                    .box-body.table-responsive.no-padding
                        table.table.table-striped
                            thead
                                th No.
                                th Nama
                                th Info
                                th Action
                            tbody
                                for row, index in anggota
                                    tr
                                        td #{index + offset + 1}
                                        td #{row.nama}
                                        td #{row.keterangan}
                                        td(style="width:100px;")
                                            div(style="display:inline-block;margin-right:5px;")
                                                a(href="/anggota/#{row._id}").btn.btn-primary.btn-xs
                                                    i.glyphicon.glyphicon-eye-open
                                            div(style="display:inline-block")
                                                form(method="POST", action="/anggota/delete/#{row._id}")
                                                    input(type="hidden", name="_method", value="DELETE")
                                                    button(type="submit").btn.btn-danger.btn-xs
                                                        i.glyphicon.glyphicon-trash

                    // /.box-body
                    .box-footer.clearfix
                        !{paginator}
                // /.box
    // /.content

Berikut adalah tampilan baru halaman index anggota setelah diberi pagination:

Selection_002 Selection_003

Upload Foto di Halaman Detail Anggota

Masuk kembali ke halaman detail anggota, kita akan mengubah sedikit kode views/anggota/detail.jade untuk menampilkan shortcut dan panel ganti foto. Silahkan ubah kode view tersebut menjadi seperti berikut:
extends ../layout/base
block content  
    section.content-header
        h1
            | Detail Anggota
            small Anda dapat melihat detail anggota disini
    // Main content
    section.content
        .row
            .col-xs-8
                - if (messages.msg_error)
                            .alert.alert-danger.alert-dismissable
                                button.close(type='button', data-dismiss='alert', aria-hidden='true') ×
                                | #{messages.msg_error}
                - if (messages.msg_info)
                    .alert.alert-success.alert-dismissable
                        button.close(type='button', data-dismiss='alert', aria-hidden='true') ×
                        | #{messages.msg_info}
                .box
                    .box-header
                        h3.box-title #{anggota.nama}
                        .box-tools
                            div(style="display:inline-block; margin-right:5px;")
                                a(href="/anggota/edit/#{anggota.id}").btn.btn-success.btn-sm
                                    i.glyphicon.glyphicon-pencil  
                                    | &nbsp; Edit
                            div(style="display:inline-block")
                                form(method="POST", action="/anggota/delete/#{anggota._id}")
                                    input(type="hidden", name="_method", value="DELETE")
                                    button(type="submit").btn.btn-danger.btn-sm
                                        i.glyphicon.glyphicon-trash Hapus
                    // /.box-header
                    .box-body
                    ul
                        li tempat / tanggal lahir: #{anggota.tempat_lahir} / #{anggota.tanggal_lahir}
                        li gender: #{anggota.gender}
                        li e-mail: #{anggota.email}
                        li telepon: #{anggota.telepon}
                    p #{anggota.keterangan}
                // /.box-body
            // /.box
        .col-xs-4
            .box
                .box-header
                    h3.box-title Foto
                    .box-tools
                        a(href="/anggota/change_profile_picture/#{anggota.id}").btn.btn-success.btn-sm
                            i.glyphicon.glyphicon-picture  
                            | &amp;nbsp; Change Photo
                        | &amp;nbsp;
                // /.box-header
                .box-body
                    div(style="text-align:center;")
                        img(src="/uploads/#{anggota.propic_path}", style="height:125px;")
                // /.box-body
            // /.box
    .row
        .col-xs-6
            .box
                .box-header
                        h3.box-title Kontak
                        .box-tools
                            a(href="/anggota/kontak/#{anggota.id}").btn.btn-success.btn-sm
                                i.glyphicon.glyphicon-pencil  
                                | &amp;nbsp; Edit
                            | &amp;nbsp;
                .box-body.table-responsive.no-padding
                    table.table.table-striped
                        thead
                            tr
                                td No.
                                td Kontak
                                td Tipe
                        tbody
                            for row, index in anggota.kontak
                                tr
                                    td #{index+1}
                                    td #{row.kontak}
                                    td #{row.types}
        .col-xs-6
            .box
                .box-header
                    h3.box-title Alamat
                    .box-tools
                        a(href="/anggota/alamat/#{anggota.id}").btn.btn-success.btn-sm
                            i.glyphicon.glyphicon-pencil  
                            | &amp;nbsp; Edit
                        | &amp;nbsp;
                // /.box-header
                .box-body.table-responsive.no-padding
                    table.table.table-striped
                        thead
                            tr
                                td No.
                                td Alamat
                                td Tipe
                        tbody
                            for row, index in anggota.alamat
                                tr
                                    td #{index+1}
                                    td #{row.alamat}
                                    td #{row.types}
            // /.box
// /.content

Pada kode diatas, kita hanya menambahkan panel ganti foto di sebelah kanan panel detail anggota. Sekarang tampilannya dapat dilihat seperti berikut:

Selection_005

Membuat Form Upload Foto

Setelah menambahkan panel ganti foto, sekarang kita buat form untuk ganti foto. Silahkan buat kode jade berikut di dalam file views/anggota/change_photo.jade:
extends ../layout/base
block content  
    section.content-header
        h1
            | Ganti Foto Anggota
            small Ganti foto anggota organisasi Anda disini
    // Main content
    section.content
        // Your Page Content Here
        // /.row
        .row
            .col-xs-12
                .box
                    .box-header
                        h3.box-title Upload Foto
                    // /.box-header
                    .box-body
                        - if (messages.msg_error)
                            .alert.alert-danger.alert-dismissable
                                button.close(type='button', data-dismiss='alert', aria-hidden='true') ×
                                | !{messages.msg_error}
                        - if (messages.msg_info)
                            .alert.alert-success.alert-dismissable
                                button.close(type='button', data-dismiss='alert', aria-hidden='true') ×
                                | #{messages.msg_info}
                        form(action="/anggota/change_profile_picture/#{anggota_id}", method="POST", role="form", enctype="multipart/form-data").form-horizontal
                            .form-group
                                label(for="nama").col-md-3.control-label Pilih Foto Baru
                                .col-md-6
                                    input(type="file", name="nama", id="nama", value="").form-control
                            .form-group
                                .col-sm-offset-3.col-sm-6
                                    button(type="submit").btn.btn-primary
                                        i.fa.fa-save
                                        | &nbsp; Simpan
                                    | &nbsp;
                                    a(href="/anggota").btn.btn-danger Batal
                // /.box-body
            // /.box
// /.content

Karena yang baru saja kita buat masih cangkangnya saja, kita buat juga route "/change_profile_picture/(:id)" untuk menampilkan form ganti foto tersebut. Silahkan tambahkan route berikut tepat diatas route detail anggota:


..............................................


router.get('/change_profile_picture/(:id)', function(req, res, next){
    session_store = req.session;

    res.render('anggota/change_photo', {session_store: session_store, anggota_id: req.params.id})
});

router.get('/(:id)', function(req, res, next){
    session_store = req.session;

    Anggota.findById(req.params.id, function(err, row){
        if (err) 
        {
            console.log(err);
            req.flash('msg_error', 'Punten, sepertinya anggota yang dimaksud sudah tidak ada. Dan kebetulan lagi ada masalah sama sistem kami :D');
            res.redirect('/anggota');
        }
        else
        {
            res.render('anggota/detail', {session_store: session_store, anggota: row})
        }
    });
});

module.exports = router;

Bila kita jalankan maka akan tampil seperti pada gambar berikut:

Selection_004

Proses Upload Foto

Untuk menangani proses upload file, kita harus membuat lagi route dengan metode POST. Di dalamnya akan ada penggunaan formidable untuk membaca file yang dikirimkan dari form dan akan disimpan ke folder public/uploads dengan bantuan modul fs. Path yang didapatkan dari pembacaan oleh formidable, akan disimpan di dalam field propic_path yang ada di dalam model models/anggota.js.

Silahkan tambahkan route ganti foto dengan metode POST berikut diatas route detail anggota dan dibawah route ganti foto bermetode GET:


..............................................


router.post('/change_profile_picture/(:id)', function(req, res, next){
    session_store = req.session;

    var form = new formidable.IncomingForm();

    form.parse(req, function(err, fields, files) {
        if (err)
        {
            console.log(err);
        }
        else
        {
            console.log(files.nama);
            var tmp_path = files.nama.path;
            var upload_path = './public/uploads/'+files.nama.name;

            fs.rename(tmp_path, upload_path, function(err){
                if (err) 
                {
                    console.log(err);
                }

                Anggota.findById(req.params.id, function(err, row){
                    row.propic_path = files.nama.name;
                    row.save();

                });

                res.redirect('/anggota/'+req.params.id);

                fs.unlink(tmp_path, function(){
                    if (err)
                    {
                        console.log(err);
                    }
                });

            })
        }
    });

});

router.get('/(:id)', function(req, res, next){
    session_store = req.session;

    Anggota.findById(req.params.id, function(err, row){
        if (err) 
        {
            console.log(err);
            req.flash('msg_error', 'Punten, sepertinya anggota yang dimaksud sudah tidak ada. Dan kebetulan lagi ada masalah sama sistem kami :D');
            res.redirect('/anggota');
        }
        else
        {
            res.render('anggota/detail', {session_store: session_store, anggota: row})
        }
    });
});

module.exports = router;

Sekarang mari kita coba dengan mengganti foto beberapa kali:

Selection_003x Selection_002x Selection_001

Penutup

Di tutorial yang keenam ini kita masih memantapkan diri untuk membuat CRUD yang lebih kompleks daripada CRUD sebelumnya. Di tutorial berikutnya, kita akan mencoba untuk membuat fitur upload foto anggota dan menambahkan pagination di halaman daftar anggota. Tentunya fitur tersebut akan semakin menarik bila kita pelajari.

Tetep pantengin Codepolitan yah :D, disini ajaa....

(codepolitan/nodejs/expressjs)