Building a jQuery UI Dashboard

Sometimes you have an existing website or template that needs to function as a dashboard. The only solution then is to create drop-in widget functionality without necessarily modifying the HTML. This is a bit of an update to a much older post

The good news is that if the layout is by and large uncluttered and follows a predictable set of elements (key issue), it’s fairly straightforward to give a layout dashboard functionality without actually touching the HTML structure. jQuery and jQuery UI are perfect for this.

The tricky bit is adding any additional elements for control while handling each segment of content as a widget. In this case, every “widget” can be any element that has the CSS class column applied to it which means we can use the existing <h5> title element as the header for it.

Take as an example, my Simply theme which has no “widget-like” functionality whatsoever and the only JavaScript is Modernizr for backwards compatibility since it was recently rewritten in HTML5. But because all the content elements are predictable (there are sections and one level of nested sections that act as columns), it can be turned into a dashboard without touching the rest of the HTML.

You can take a look at a running demo of it and see below for an explanation of what’s going on.

A running demo of "Simply" turned into a dashboard

For this example, I needed the dashboard widgets to be able to be moved around on each row, have the title, content and the article icon changed and of course be able to close them. Naturally, this is only a client-side example. The final version would need to post these changes server-side for saving.

The Procedure

We can first start by adding the jQuery UI theme CSS file right above the original stylesheet. I’m using a customized version of the Smoothness theme. Just a few tweaks to make it match mine.

<link rel="stylesheet" type="text/css" href="themes/jQueryUI/ui-style.css" id="ui-theme" />
<link rel="stylesheet" type="text/css" href="themes/Simply/style.css" id="theme" />

After that we can add the jQuery and jQuery UI libraries to the template. In my project, there was also a need to add a wysiwyg and I had to use their propietary one, but for this example, I can simply use TinyMCE.

For this example, I’m going to put all the scripts into a folder called /lib. This is also where I’ll extract TinyMCE (the jQuery plugin version).

Modernizr, very usefully, can load other scripts by URL, so we can simply do the following right after adding its JS own file :

<script type="text/javascript" src="lib/modernizr.custom.js"></script>
<script type="text/javascript">
	Modernizr.load([{
		load: "https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"
	}, {
		load: "https://ajax.googleapis.com/ajax/libs/jqueryui/1.8.16/jquery-ui.min.js"
	}, {
		load: "lib/tiny_mce/jquery.tinymce.js"
	}, {
		load: "lib/dash.js"
	}]);
</script>

And that’s all we need to do on the template itself. That last file being called, “dash.js” in the /lib folder is where we’ll put everything else.

dash.js

I’m going to start by adding some custom CSS to this file rather than cluttering up the head section of the template. These are just a few added classes to make the content more presentable.

All this will be added inside the jQuery $(fuction() { }); section.

$('head').append('<style type="text/css">' +
	'.icon { width:75px; height:75px; }' +
	'.ticon { float:left; margin: .3em .5em; width:25px; height:25px; box-shadow:1px 1px 1px #aaa; }' +
	'.ui-dialog { text-align:left; }'+
	'.ui-dialog input[type="text"], .ui-dialog textarea { display:block; width:90%; }' +
	'</style>');

Next, since we’re working with TinyMCE, we’ll store those settings in a variable.

var tinySettings = {
	script_url : 'lib/tiny_mce/tiny_mce.js',
	plugins: "inlinepopups",
	theme: "advanced",
	theme_advanced_buttons1 : "bold,italic,underline,strikethrough," +
		"separator,cut,copy,paste,separator,bullist,numlist," +
		"separator,undo,redo,separator,link,unlink,image",
	theme_advanced_buttons2 : "",
	theme_advanced_buttons3 : "",
	theme_advanced_toolbar_location : "top",
	theme_advanced_toolbar_align : "left",
	theme_advanced_statusbar_location : "bottom",
	width: "100%",
	height: 250,
	content_css : "content.css",
};

You’ll notice the last line refers to a “content.css” file. This basically styles the wysiwyg content as it’s being edited. In my case, this is all it contained…

body {
	font: normal 90% Sans-serif,tahoma,verdana;
	color: #575757;
}

p {
	line-height:170%;
}

Dialogs

Dashboards frequently use dialogs when making changes, presenting confirmations etc… In this case, I just created two dialogs for editing content and to confirm when removing the widget…

function initDialogs() {
	// Close widget dialog
	$('article').append('<div id="dialog-confirm-close-widget" title="Close widget" style="display:none;">' +
		'<span class="ui-icon ui-icon-alert" style="float:left;"></span>' +
		'You are about to delete this widget. Are you sure?</div>');

	// Config widget dialog
	$('article').append('<div id="dialog-config-widget" title="Modify" style="display:none;">' +
		'<form><fieldset><legend>Change widget content</legend>' +
		'<p><label>Title <input type="text" id="widget-title-text" /></label></p>'+
		'<p><label>Content <textarea rows="5" cols="50" id="widget-content-text">Some test content</textarea></label></p>'+
		'<p id="icon-field"><label>Icon URL <span>Will be resized to 75x75 pixels</span><input type="text" id="widget-icon-text" /></label></p>'+
		'</fieldset></form></div>');

	// Create and destroy these dialogs to hide them
	$('#dialog-confirm-close-widget').dialog("destroy");
	$('#dialog-config-widget').dialog("destroy");
}

That textarea will be replaced with TinyMCE later.

3 Little Helpers

Often overlooked, helpers can save a lot of time and help unclutter your JS quite a bit.

The following three check for null or empty strings, initializes and applies TinyMCE to an element with the given settings and removes the wysiwyg after sending the changes back to the textarea… in that order.

function notEmpty(t) {
	if(t) {
		if($.trim(t) != "")
			return true;
	}
	return false
}

function initTiny(t, s) {
	t.tinymce(s);
}

function closeTiny(t) {
	var e = t.attr('id');
	t.val(tinyMCE.get(e).getContent());
	tinyMCE.execCommand('mceRemoveControl', false, e);
}

Widget controls

In jQuery UI, there are ui-icons that we can use here without having to invest extra time to create our own. This also means we can apply our controls to these icons by using something like the “title” or “alt” attribute as the command string. In this case, I opted to use the “title” attribute since I’ll be using these icons in <span> tags.

function setControls(ui) {
	ui = (ui)? ui : $('.ui-icon');
	ui.click(function() {
		var b = $(this);
		var p = b.parentsUntil('.column').parent();
		var i = p.children('img[alt="icon"]:first').eq(0);

		var h = p.children('.ui-widget-header h5:first').eq(0);

		// Control functionality
		switch(b.attr('title').toLowerCase()) {
			case 'config':
				widgetConfig(b, p);
				break;

			case 'toggle':
				widgetToggle(b, p, i);
				break;

			case 'close':
				widgetClose(b, p);
				break;
		}
	});
}

Note, the three widget functions that will “config” (as in edit the content), “toggle” (collapse/expand) and “close” the widgets.

The toggle function is pretty simple. I wanted to minimize the .widget-content class (which we’ll add later), change the minus into a plus on the side of the widget after collapsing and turn the big image icon seen on most of those segments into a smaller one that fits on the header. The .ticon class is what we added to the head earlier.

// Toggle widget
function widgetToggle(b, p, i) {
	// Change the + into - and visa versa
	b.toggleClass('ui-icon-minus').toggleClass('ui-icon-plus');

	// Turn the big icon into a small one or visa versa
	if(i.hasClass('icon'))
		i.switchClass('icon', 'ticon', '300');
	else
		i.switchClass('ticon', 'icon', '300');

	// Show/Hide widget content
	p.children('.widget-content').eq(0).toggle(300);
};

Closing a widget is also pretty straightforward. We’ll be using the closing dialog we added earlier.

// Close widget with dialog
function widgetClose(w, p) {
	$("#dialog-confirm-close-widget").dialog({
		resizable: false,
		modal: true,
		buttons: {
			"Close widget": function() {
				p.toggle('slide', {}, 500, function() {
					p.remove();
				});
				$(this).dialog("close");
			},
			Cancel: function() {
				$(this).dialog("close");
			}
		}
	});
}

The last of the widgets is the config. This is the trickiest part, since we’ll need to create and destroy TinyMCE instances and get input from the dialog and push to the content.

// Modify widget
function widgetConfig(w, p) {

	// Input elements in the dialog
	var dt = $('#widget-title-text');
	var dc = $('#widget-content-text');
	var du = $('#widget-icon-text');

	// Widget elements to change
	var wt = p.children('h5:first').eq(0);
	var wc = p.children('.widget-content').eq(0);

	// If there is no icon on the widget, there's nothing to change
	var wi = p.children('img[alt="icon"]:first');
	if(wi.length > 0) {
		wi = p.children('img[alt="icon"]:first').eq(0);
			$('#icon-field').show();
	}
	else {
		$('#icon-field').hide();
	}

	$("#dialog-config-widget").dialog({
		resizable: false,
		modal: true,
		width: 500,
		open: function() {
			if(wi != null)
				du.val(wi.attr('src'));

			dt.val(wt.text());
			dc.val(wc.html());

			// Initialize TinyMCE
			initTiny(dc, tinySettings);
		},
		buttons: {
			"Save changes": function(e, ui) {

				// Some widgets don't have an icon
				if(wi.length > 0) {
					if(notEmpty(du.val()))
						wi.attr('src', du.val());
				}

				// Remove editor (also gets content from TinyMCE back to textarea)
				closeTiny(dc);

				// Update
				if(notEmpty(dc.val()))
					wc.html(dc.val());

				// Careful here, don't wanna lose the control icons
				if(notEmpty(dt.val())) {
					var ci = wt.children('span.ui-icon');
					wt.html(dt.val());

					// Reset controls
					wt.prepend(ci);
					setControls(ci);
				}

				$(this).dialog("close");
			},
			Cancel: function() {

				// Destroy TinyMCE
				closeTiny(dc);

				$(this).dialog("close");
			}
		}
	});
}

Basically, the above function grabs the text fields from the dialog and puts the existing content on page into them. The user makes some edits and clicks on “Save changes” which causes the script to take the changed content and apply it back to the page. In a real-world example, this content will actually be posted back to the server.

Note: Since the control icons are also located in the title, when we change the title text, we have to make sure that the control icons are spared from any edits.

The Initialization.

This basically takes every section with a “column” class that has a <h5> element inside into a sortable widget. It adds the control icons as <span> tags and wraps the content in a widget-content <div> (which we can minimize) and adds the ui-widget classes from jQuery UI to aid with sorting.

function Init() {

	// Initialize dialogs
	initDialogs();

	// Portlet and sort related CSS classes
	var sortClasses = "ui-widget ui-widget-content ui-helper-clearfix";

	// Set every column segment with h5 element as a sortable widget
	$('.column:has(h5)').each(function() {

		var s = $(this);
		var p = s.parentsUntil('section').parent();
		var h = s.children('h5:first').eq(0);

		if(!p.hasClass('ui-widget'))
			p.addClass(sortClasses);

		// Function icons
		h.addClass('ui-widget-header')
			.prepend('<span class="ui-icon ui-icon-gear" title="Config"></span>')
			.prepend('<span class="ui-icon ui-icon-minus" title="Toggle"></span>')
			.prepend('<span class="ui-icon ui-icon-close" title="Close"></span>');

		// Need this to drag not highlight
		h.disableSelection();

		// Interaction cues
		h.css('cursor', 'move');
		$('.ui-icon').css('cursor', 'pointer');

		// Wrap control stuff (like icons and headers) in a widget-header div
		// and the rest in a widget-content div
		s.children().not('img[alt="icon"], .ui-widget-header, .ui-icon')
			.wrapAll('<div class="widget-content" />');

		s.children().not('widget-content').wrapAll('<div class="widget-header" />');
	});

	// Group sortable widgets in each section to one sort-area
	$('section').has('.column').each(function() {
		$(this).children().not('header,hr')
			.wrapAll('<div class="sort-area" />');
	});

	// Trigger control initialization
	setControls();
}

And lastly, if we don’t have sorting ability, we can’t really call this a dashboard. Ironically the most defining characteristic in dashboards is the most simple to implement once all the wrapper divs are in place.

$(".sort-area").sortable({
	connectWith: ".sort-area",
	opacity: 0.6,
	helper: 'clone',
	dropOnEmpty: true
});

This post was prompted by yet another example of why I have a dim view of project managers.

Can you put together a dashboard for us?

OK, no problem.

We already have the backend completed and a template in place although it doesn’t have any widget markup. Except for the head tag, we need you to not touch the rest of it because we do have other stuff on the page. BTW… We need it in an hour.

@$#& me!

I wish the above was an exaggeration, but sadly, it happens more often than I care to tolerate.

The problem is easy enough to come by. Someone first builds a backend thinking they’ll have all sorts of widget like functionality in the future, but doesn’t develop the front end for any of it. Time goes by… The next developer comes along and decides that since the template is already in place, they’ll finagle the output to be more “widget-like” and create a separate handler to manage user changes. This also explains why so many “legacy” dashboards are notoriously slow.

So now they bring in someone else to drop in a dashboard where there previously existed no frontend for it… And they want it yesterday.

Oh, right New Year resolution…
Less grumpy. Less grumpy. Less grumpy.

Update

Changed the notEmpty(t) function line if(t != “”) into if($.trim(t) != “”).

About these ads

3 thoughts on “Building a jQuery UI Dashboard

  1. Pingback: Building (clean) dashboards for your web app « This page intentionally left ugly

  2. It will prevent angry customers from leaving negative feedback about your business and what you are doing.

    Ok fashion outlets of chicago ASUS, you win. Below is a list of requirements you know you are getting opinions
    from are people you can trust. But for the next season.
    Wide-legged slacks or jeans will make you look slim.
    There’s no need to forgo looking your best if you or your child can look back at photos of my childhood and as an ode to the ’20s.

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