Making Dooitkoo – Bagian 7 : Dashboard

Lanjutan dari Bagian 6

Semua aplikasi yang punya banyak konten terstruktur pasti punya yang namanya dashboard. Untuk Dooitkoo, saya ingin dashboard bisa nampilin sekilas info tentang masing-masing subjek. Jadi sekali lirik, user bisa liat status terakhir semua subjek dalam bulan yang bersangkutan. Karena tujuannya “sekilas info”, ga ada pilihan bulan & tahun. Kalo misalnya user buka aplikasi pada bulan Mei 2013, info yang ditampilin adalah data bulan Mei 2013. Kalo buka di bulan April 2014, data yg ditampilin juga data bulan April 2014. Bulan & tahun diambil dari seting komputer user (local date & time).

Flow di Dashboard juga sederhana. Awalnya user lihat daftar subjek. Dia punya pilihan lihat ringkasan salah satu subjek atau bikin subjek baru. Kalo pilih liat ringkasan, dia punya pilihan untuk edit, hapus, atau buka subjek yang bersangkutan.

Dashboard flow

Wireframe

Sebelum mulai koding bikin desain dulu. Ga perlu bagus, sket wireframe aja udah cukup. Yg penting bisa jadi referensi waktu koding. Layout dashboard punya 5 bagian utama :

  1. Header. Isinya logo, user name (email) & link untuk logout.
  2. Sidebar. Panel di samping kiri untuk tampilin menu.
  3. Content. Untuk nampilin … konten
  4. Breadcrumb. Menu kecil di atas area konten buat tempat shortcut utk kembali ke dashboard & titel konten yang sedang ditampilin
  5. Footer.

Dashboard wireframe

Karena Dashboard punya 3 state yaitu add/edit, ringkasan, & daftar subjek, sekalian bikin wireframe-nya.

Dashboard state wireframe

Page View

Selain berisi layout skeleton & bagian-bagian yg statis, Dashboard View juga berisi sedikit skrip, sekedar untuk booting aplikasi. Skrip yg utama ada 2 yaitu:

  1. dashboardpage.js
  2. yepnope.js

dashboardpage.js adalah startup script yang tugasnya:

  1. Loading file JavaScript & CSS untuk plugin jquery yg dibutuhkan pake Yepnope
  2. Inisialisasi KO binding

Pengennya sih pake RequireJS untuk atur dependency & loading library, tapi nyoba bolak-balik selalu ada aja yg ga jalan. Daripada pusing kelamaan utak-atik, cari bulk loader aja. Untuk Dooitkoo saya pake library yang namanya Yepnope (bisa dipake sendirian atau sebagai bagian dari modernizr). Library ini bisa dipake untuk download resource (.js, .css) secara paralel, tapi nanti injeksi resource & eksekusi file javascript (kalo pake IIFE) sesuai urutan.

Kalo belum tau apa itu IIFE, baca ebook saya yg judulnya Mengenal JavaScript.

Seperti yg saya bilang di bagian sebelumnya, untuk JavaScript & CSS saya pake 2 versi yaitu versi concatenated+minified & versi non-minified. Versi minified dipake untuk production sedangkan versi non-minified dipake selama development di server lokal. Untuk itu saya pake kondisional (baris 7 – 15 di potongan kode di bawah) untuk nentuin versi mana yang harus dipake. Skrip di bawah ini saya tulis di master layout.

var vendorPath = '/vendor/';
var jqueryPath = '/vendor/jquery/';
var koPath = '/vendor/ko/';
var appPath = '/js/app/';

<?php if(Config::get('dooitkoo.asset_suffix') == '.min'): ?>
	var appLibs = [appPath+'app_libs.min.js'];
<?php else: ?>
	var appLibs = [
		appPath+'namespace.js',appPath+'binding-handlers.js'
		,appPath+'validators.js',appPath+'vm/bootstrapmodal-vm.js'
		,appPath+'dooitkoo-utils.js'
	]
<?php endif; ?>

var libs = [
	//jquery
	'/js/corelibs.min.js',
	jqueryPath + 'jquery-ui/jquery-ui-1.10.0.custom' + window.asset_suffix + '.css',
	jqueryPath + 'plugins/toastr/toastr' + window.asset_suffix + '.css',
	//knockout
	koPath + 'ko_all.min.js',
	//amplify
	vendorPath + 'amplify/amplify_all.min.js'
].concat(appLibs);

Di potongan skrip di atas, saya bikin daftar resource apa aja yang harus dipake, jadiin dalam satu global array yg namanya libs & nantinya dikirim ke Yepnope sama dashboardpage.js. Di bawah blok skrip di atas, baru saya pasang dashboardpage.js.

{{HTML::script('/js/app/dashboard/dashboardpage'.Config::get('dooitkoo.asset_suffix').'.js')}}

Dari potongan skrip di atas ada beberapa catatan (mungkin pertanyaan dari yang baca artikel ini):

Kenapa KO & Amplify dimuat terpisah dari corelibs.min.js?

Karena ada beberapa halaman yang ga butuh library ini.
Kenapa ga langsung aja pasang skrip secara berurutan terus pake $(document).ready(), kan ga usah pake Yepnope?

$(document).ready() cuman menjamin DOM udah dimuat. Ga ada jaminan semua skrip dimuat dalam urutan yg bener. Semua skrip yang dipake Dooitkoo saya buat modular, jadi urutan skrip sangat penting.

dashboardpage.js

Dalam skrip ini, array libs di master layout ditambah sama library lain yang spesifik untuk halaman dashboard aja (baris 5-12). Setelah semua skrip selesai dimuat, KO binding diinisialisasi.

;(function (suffix,libs,appPath,token) {

	yepnope({
		load    : libs.concat([
			appPath + 'subject/vm/subject-vm' + suffix + '.js',
			appPath + 'dashboard/vm/subjectform-vm' + suffix + '.js',
			appPath + 'dashboard/vm/sidebar-vm' + suffix + '.js',
			appPath + 'dashboard/vm/summary-vm' + suffix + '.js',
			appPath + 'dashboard/vm/dashboardpage-vm'+suffix+'.js',
			//plus library lain2 yang ga masuk master libs
		]),
		complete: function () {
			//default properti untuk ko external template engine
			infuser.defaults.templateSuffix = ".tmpl.html";
			infuser.defaults.templatePrefix = "_";
			infuser.defaults.templateUrl = '/js/app/dashboard/templates'

			var pageVM = new dooitkoo.DashboardPageVM(token);
			ko.applyBindings(pageVM);
		}

	})


})(window.asset_suffix,window.libs,window.appPath,window.token);

Page VM

DashboardPageVM adalah root VM untuk halaman dashboard. Di dalamnya, saya bikin instance viewmodel yang lain jadi saya ga perlu panggil ko.applyBindings lebih dari satu kali. Sesuai saran dari pembuat KO, semakin sedikit eksekusi ko.applyBindings, semakin baik dari segi performa karena setiap kali function ini dipanggil, KO akan melakukan scanning seluruh elemen DOM untuk menentukan elemen mana yang musti di-bind. Untuk info mengenai hirarki viewmodel, baca binding-context di dokumentasi KO.

;(function (dooitkoo) {

	function DashboardPageVM(){

		var self = this;

		self.newSubject = ko.observable();
		self.bootstrapVM = new dooitkoo.BootstrapModalVM();
		self.sidebarVM = new dooitkoo.SidebarVM();
		self.formVM = new dooitkoo.SubjectFormVM();
		self.subjectVM = new dooitkoo.SubjectVM();
		self.summaryVM = new dooitkoo.SummaryVM();
	}

	dooitkoo.DashboardPageVM = DashboardPageVM;

})(window.dooitkoo);

External Template

Dashboard bisa nampilin 3 macam konten: add/edit form, daftar subjek atau ringkasan subjek. Di sini saya punya beberapa alternatif untuk implementasinya:

  1. Bikin 3 Laravel view, untuk form, daftar subjek, & ringkasan.
  2. Bikin 1 view, tapi di dalamnya ada 3 Knockout template (embedded).
  3. Bikin 1 view, plus 3 template eksternal. Isinya fragmen HTML & nanti dimuat waktu aplikasi berjalan, tergantung menu apa yg diklik user.

Pilihan no 3 yg paling bagus karena :

  1. Hemat bandwidth. File HTML utama yg dikirim dari server isinya cuman skeleton, atau bagian-bagian utama aja.
  2. Kontennya baru dikirim pada waktu dibutuhkan. Kalo user ga perlu bikin subjek, ga usah kirim form.
  3. File template eksternal bakal masuk browser cache jadi ga perlu berulang kali diunduh.
  4. Template eksternal bisa dipake bersama oleh beberapa view.

Library JavaScript utk templating ada macem-macem, ada JsRender, Jquery Template, Handlebars, dll. Loading template sih gampang. Masalahnya, apa bisa begitu template di-load otomatis data-binding elemen DOM di template itu langsung jalan. Kayanya perlu eksperimen dulu. Untungnya pas tanya Om Google ketemu plugin buat Knockout yg suport data-binding di template eksternal … maknyus! :-).

Saya bikin 3 file eksternal, penamaannya ngikutin “standar” yang biasa dipake untuk template, diawali underscore, diakhiri “.tmpl.html”. Jadi saya punya file : _subject_table.tmpl.html, _subject_form.tmpl.html, & _summary.tmpl.html.

Dashboard External Template

Gimana cara nentuin template mana yang harus dimuat, cara memuatnya, & kapan?

Saya tambahin sebuah observable bernama displayMode di dalam DashboardPageVM. Observable ini berisi data bertipe string yang nilainya bisa all,new, atau summary. Nilai dari observable ini kemudian saya pake untuk memuat eksternal template pake deklarasi virtual element seperti berikut:


Sidebar

SidebarVM punya properti subjects yang bertipe observableArray. Isi array ini adalah SubjectVM, viewmodel yang merepresentasikan sebuah subjek. Nilai dari setiap properti diisi dgn JSON data dari server.

;(function (dooitkoo,ko) {

	function SubjectVM() {
		var self = this;
		self.id = ko.observable();
		self.name = ko.observable();
		self.user_id = ko.observable();
		self.currency = ko.observable();
		self.note = ko.observable();
		self.total_expense = ko.numericObservable(0);
		self.total_income = ko.numericObservable(0);
		//array of js object
		self.categories = ko.observableArray([]);
		self.selected = ko.observable(false);

		self.summary = ko.observable();

	};

	dooitkoo.SubjectVM = SubjectVM;

})(window.dooitkoo,window.ko);

numericObservable adalah custom observable dan bukan bagian dari paket standar KO.

Sidebar saya bind ke sidebarVM pake deklarasi with:sidebarVM. Untuk daftar subjek yang ditampilin di sidebar, saya pake foreach:subjects yang berarti elemen ul pake properti subjects yang ada di sidebarVM. Untuk setiap subjek, saya ingin KO bikin elemen li. Kalo saya punya 10 subjek, KO akan bikin 10 elemen li yang masing-masing berisi teks nama subjek & struktur HTML-nya sama dengan li (baris 7 – 11) di potongan kode di bawah ini.




Di bagian bawah sidebar saya bikin link untuk bikin subjek baru. Link ini saya bind dengan function add() di sidebarVM.

Form VM

FormVM adalah viewmodel untuk form yang dipake buat bikin atau edit subjek. Formnya sendiri ada di template eksternal dan saya bind ke properti formVM dari pageVM. Setiap input di form saya bind ke observable properti dari formVM.

Subjek

dan seterusnya ...

Daftar Subjek

Daftar subjek saya tampilin dalam bentuk tabel. Tadinya saya mau bikin viewmodel sendiri buat tabel ini tapi setelah dipikir-pikir, kenapa ga pake sidebarVM aja, toh datanya sama cuman beda tampilannya aja. Jadi mirip dengan sidebar, tabel untuk daftar subjek saya bind ke sidebarVM.

titel widget ...
table head ...

Untuk menampilkan nominal sebagai formatted text seperti baris 21-22 dalam kode di atas, saya bikin custom binding seperti berikut.

ko.bindingHandlers.moneyText = {
	update: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
		var value = ko.utils.unwrapObservable(valueAccessor());
		$(element).text(accounting.formatNumber(value, 2, '.', ','));
		$(element).removeClass();
		//set warna teks, merah kalo minus, ijo kalo plus
		if (value > 0) {
			$(element).addClass('numeric text-success')
		} else if (value < 0) {
			$(element).addClass('numeric text-error')
		} else {
			$(element).addClass('numeric text-muted')
		}
	}
}

Ringkasan Subjek (Summary)

Ringkasan berisi data agregat dari subjek yang bersangkutan. Isi datanya data transaksi selama 12 bulan terakhir, transaksi per kategori di bulan ini, & total transaksi bulan ini. Saya buat SummaryVM yang tugas utamanya meminta & memproses data dari server dan menyediakan observable untuk dipake sama template _summary.tmpl.html.

Untuk menampilkan chart, saya kembali pake custom binding.

ko.bindingHandlers.chart = {
	init  : function (element, valueAccessor) {
		var data = ko.utils.unwrapObservable(valueAccessor());
		if(data !== undefined ){
			$(element).highcharts(data);
		}
	},
	update: function (element, valueAccessor) {
		var data = ko.utils.unwrapObservable(valueAccessor());
		if(data !== undefined ){
			$(element).highcharts(data);
		}

	}
}

Saya tambahin beberapa properti observable sebagai data source untuk chart binding. Salah satunya untuk chart transaksi per kategori.

self.categoryChartData = ko.computed(function(){
	if(self.summary() !== undefined && self.summary().hasOwnProperty('categories')){
		var data = self.summary().categories;

		var total_incomes = 0;
		var incomes = [];
		_.each(_.pluck(data,'total_income'),function(x){
			incomes.push(x)
			total_incomes += x;
		});

		var total_expense = 0;
		var expenses = [];
		_.each(_.pluck(data,'total_expense'),function(x){
			expenses.push( Math.abs(x));
			total_expense += x;
		});

		if(total_incomes === 0 && total_expense === 0){
			self.hasTransactions(false);
			return;
		}

		self.hasTransactions(true);

		var now = moment();
		var chartData = {

			chart:{ type :'bar'},
			title:{
				text:'Pengeluaran & Pemasukan per Kategori'
			},
			subtitle:{text:now.format('MMMM YYYY')},
			xAxis:{
				categories:_.pluck(data,'name'),
				title:'Kategori'
			},
			yAxis:{
				min:0,
				title:{
					text:self.currency(),
					align:'middle'
				},
				labels:{
					overflow:'justify',
					formatter:function(){
						return accounting.formatNumber(this.value/1000000,1,'.',',')+'jt'
					}

				},
				tickPixelInterval:50
			},
			tooltip:{

			},

			legend: {
				layout: 'horizontal',
				floating: false,
				align:'center',
				verticalAlign:'top',
				borderWidth: 0,
				backgroundColor: '#FFFFFF',
				shadow: false,
				y:40
			},
			credits: {
				enabled: false
			},
			exporting:{
				enabled:false
			},
			series: [{
						 name: 'Pengeluaran',
						 data: expenses,
						 color:'#FF6666'

					 }, {
						 name: 'Pemasukan',
						 data: incomes,
						 color:'#008000'
					 }]

		}


		return chartData;
	}
})

Dalam template HTML, saya lakukan binding antara data di SummaryVM dengan custom binding.

Data service

Semua akses ke server harus lewat modul dataservice karena data dari server bisa jadi perlu diproses oleh lebih dari satu bagian aplikasi. Karena proses komunikasi dengan server sifatnya asinkron, saya juga pake fasilitas publisher-subscriber (pub/sub) yang disediakan oleh Amplify. Jadi setiap kali ada respon dari server, modul dataservice kirim notifikasi lewat channel ini yang kemudian ditangkap oleh objek lain yang berkepentingan. Untuk mengurangi resiko salah ketik, semua notifikasi yang bisa dikirim oleh dataservice saya definisikan sebagai global properti misalnya:

dooitkoo.subjects_loaded = "subjects_loaded";

Sebagai contoh, untuk ambil daftar subjek, saya bikin function getSubjects() di dataservice seperti contoh di bawah. Setelah data diterima, notifikasi subjects_loaded sekalian datanya akan dikirim ke pub-sub channel.

;(function (dooitkoo,token) {

	amplify.request.define('get_subjects', 'ajax', {
		url    : '/api/user/subjects',
		type   : 'get',
		data   : {
			token     :token
		},
		//jsonResult adalah nama custom decoder yg
		//didefinisikan di global namespace
		decoder: 'jsonResult'
	});

	dooitkoo.SubjectDataService = {

		getSubjects:function(){
			dooitkoo.showSpinner();
			amplify.request({
				resourceId: 'get_subjects',
				data      : {},
				success   : function (data) {
					toastr.success('Daftar subjek berhasil dimuat.')
					amplify.publish(dooitkoo.subjects_loaded, data);
				}
			})
		},

	}
	
//dan seterusnya …

})(window.dooitkoo,window.token);

Proses pengambilan data subjek dilakukan oleh SidebarVM, jadi di dalamnya saya buat subscriber untuk notifikasi dooitkoo.subjects_loaded & sebuah private function untuk memproses data yang dibawa sama notifikasi itu.

var _refresh = function(subjects){
	for (var i = 0; i < subjects.length; i++) {
		var obj = subjects[i];
		var sub = new dooitkoo.SubjectVM();
		sub.mapJS(obj);
		self.subjects().push(sub);
	}
	self.subjects.valueHasMutated();
}

amplify.subscribe(dooitkoo.subjects_loaded,_refresh);

Dashboard

Beberapa hari kemudian, setelah beberapa kali tes & refactoring, dashboard bisa dinikmati 🙂

Dashboard

Also in this category ...


3 Comments

Comments are closed.