Ransford Okpoti's Dev Notes

October 2, 2009

Decorating A Form Using JavaScript

Filed under: Javascript — admin @ 12:44 pm

Javascript plays an important role in the development of any web application, irrespective of the server-side language employed, since it resides on the client’s browser and can interact with the Document Object Model (DOM) of the web browser. Over the last couple of years, we have seen the emergence of great javascript utility libraries providing various functionalities which takes away the nightmare of handling browser hacks when you decide to develop your own javascript libraries from scratch. Prototype, Script.aculo.us, jQuery, ExtJs, YUI, Dojo, and Mootools are some of the javascript libraries out there providing functionalities such as serving as general utility libraries, visual effects, drag and drop, and providing customizable user interface (UI) widgets which can be used to build entire UIs using only javascript.
Enough of the talking, now to the issue. A good form design must be able to communicate a reasonable amount of information to the user to enable him (no gender segregation intended) perform the task required , such as filling out a new entry, editing an existing entry, performing a search, etc, without any ambiguity whatsoever. Simply put, users need to be armed with all the instructions/information to enable them complete a form, such as

  • which fields are mandatory/required
  • which fields can be used to perform a quick search on the form
  • acceptable data formats for some fields such as date field, etc
  • any other useful instruction

in a short and concise manner.
Lets take a look at the form below where a user is required to complete with the following fields been required:

  • Name
  • Gender
  • Country
  • Postal Address
  • Preferable work countries

and the these fields can be used in a search:

  • Name
  • Gender
  • Country

Below is a screen shot of an undecorated form.

Undecorated Form

Undecorated Form

Despite the fact that this form may have these validations in place, it does not communicate that to the user. The user is only brought into the loop when something goes wrong (annoying, huh? Why do you wait for me to do the wrong thing before informing me?) and this is just not good enough.
Now, we will take that form and make it friendly for our unsatisfied users. There are several ways to achieve this task, but i think a better approach would be not to introduce any complexities into the form designing by sticking little images/icons beside controls that need to be decorated. It should be as painless as the introduction of intuitive custom attributes like:

  • required – marks fields that are required
  • searchable – marks fields that can be used as paratemers in a search operation, with the ability to click on the icon to perform a search directly
  • info – displays an information icon beside the control, and on a mouse move shows any additional information about the field

Sample usage (our custom attribute is displayed in green):

?View Code HTML4STRICT
<input type="text" id="email" required="true" />

Now, lets build our little javascript library by making use of the following libraries:

  • prototype
  • wz_tooltip, which creates easy-to-use cross-browser tooltips

We will first start by defining our function in a formUtil namespace which can be extended by adding other form related useful functionalities.

?View Code JAVASCRIPT
1
2
3
var formUtil = {
     version = '1.0'
}

The decorator namespace under the formUtil will contain all the codes related to the problem we want to solve.

?View Code JAVASCRIPT
4
formUtil.decorator = {}

The next lines of code are pretty straight forward, we define all the attributes that determines whether a form element needs decorating or not.

?View Code JAVASCRIPT
5
6
7
8
9
10
/**
 * Defines our custom attributes
 */
formUtil.decorator.attributes = [
    "required", "searchable", "info"
]

The icons/images displayed beside each decorating element is defined here, so point these to actual image paths on your server or any appropriate place.

?View Code JAVASCRIPT
/**
 * Images used to decorate the form.
 * These must point to images in the application.
 */
formUtil.decorator.img = {
    REQUIRED: 'img/required.gif',
    SEARCH: 'img/search.gif',
    INFO: 'img/info.png'
}

These are the templates which determines the markup for the various decorations. Template is a class from prototype.

?View Code JAVASCRIPT
formUtil.decorator.templates = {
    DEFAULT: new Template('<span id="#{id}"><img src="#{imgSrc}" style="vertical-align:top"></span>'),
    INFO : new Template('<span id="#{id}"><img alt="Info" src="#{imgSrc}" style="vertical-align:top" onmouseover="Tip(\'#{title}\',SHADOW, true,STICKY, 1, CLICKCLOSE, true,DELAY, 1000)" onmouseout="UnTip()"></span>'),
    LINK:  new Template('<span id="#{id}"><a href="javascript:#{fn}"><img src="#{imgSrc}" style="vertical-align:top;border:0"></a></span>')
}

Next, we will define the layout which will come in handy when determining which element to decorate in a group of related fields, such as radio buttons, check boxes, etc.

?View Code JAVASCRIPT
11
12
13
14
15
16
17
/**
 * Layout arrangment for grouped controls like checkboxes or radio buttons.
 */
formUtil.decorator.layout ={
    HORIZONTAL: 'horizontal',
    VERTICAL: 'vertical'
}

The default configuration for the decorate function is defined below. It provides a flexible and convenient way of turning off the decoration of any of the attributes defined above.

?View Code JAVASCRIPT
18
19
20
21
22
23
24
25
26
27
28
29
/**
 * Default configuration options which may be overriden by the user
 */
formUtil.decorator.cfgDefaults = {
    renderRequired: true,
    renderSearchable: true,
    renderInfo: true,
    searchable: {
        clickable: false,
        fn: 'void(0)' // function to be clicked when clickable is set to true
    }
}

The decorateForm function does all the dirty work by iterating through all the form elements and decorating elements having our custom defined attributes.

?View Code JAVASCRIPT
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
/**
 * @param
 */
formUtil.decorator.decorateForm = function(form, cfgOptions){
    // Iterating through all the elements/controls in each form
    for(var j=0, k=form.elements.length; j<k ; j++){
        var elem = form.elements[j];
 
        var isDecoratable = false;
        for(var i in this.attributes){
            if($(elem).readAttribute(this.attributes[i])!==null){
                isDecoratable = true;
                break;
            }
        }
 
        if(!isDecoratable) continue
 
        var uniqueId = ( elem.id || elem.name);
 
        // ids for the icons displayed beside each field
        for(i in this.attributes){
            attribute = this.attributes[i];
            if(typeof attribute=='string'){
                var str = 'var ' + attribute + 'Id="' + uniqueId + '_' + attribute+'";';
                eval(str);
            }
        }
 
        /**
         * The element besides which the various decorators
         * will be rendered. This will normally be the element with any of
         * the custom attributes, expect in grouped controls, such as checkboxes,
         * or radio buttons, where the rendering element may either be the first
         * or last control in the group depending on the layout arrangement.
         */
        var renderingEl = elem;
        var numOptions = document.getElementsByName(elem.name).length;
        var layout = $(elem).readAttribute("layout") || this.layout.HORIZONTAL;
        layout = layout.toLowerCase();
 
        if(numOptions>1){ // it's either a radio button or a checkbox
            // if the options are arranged horizontally, then the last option
            // in the list is used.
            if(layout==this.layout.HORIZONTAL)
                renderingEl = document.getElementsByName(elem.name)[numOptions-1];
            else
                renderingEl = elem;
            renderingEl=renderingEl.next();
        }
 
        if(cfg.renderInfo && $(elem).readAttribute("info")!==null){
            renderingEl.insert({after: formUtil.decorator.templates.INFO.evaluate({ id:infoId, imgSrc:this.img.INFO, title:$(elem).readAttribute("info") })});
            renderingEl=renderingEl.next();
        }
 
        if(cfg.renderRequired && $(elem).readAttribute("required")=="true"){
            renderingEl.insert({after: formUtil.decorator.templates.DEFAULT.evaluate({ id:requiredId, imgSrc:this.img.REQUIRED })});
            renderingEl=renderingEl.next();
        }
 
        if($(elem).readAttribute("searchable")=="true"){
            if(cfg.searchable.clickable){
                renderingEl.insert({after: formUtil.decorator.templates.LINK.evaluate({ id:searchableId, fn:cfg.searchable.fn, imgSrc:this.img.SEARCH })});
            }
            else{
                renderingEl.insert({after: formUtil.decorator.templates.DEFAULT.evaluate({ id:searchableId, imgSrc:this.img.SEARCH })});
            }
        }
    }
}

This is a convenient method to call since it iterates through all the forms in a document and calls the decorateForm passing it each iterated form for decoration.

?View Code JAVASCRIPT
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
/**
 * Decorates a forms elements having the custom attributes
 * required, searchable, and info.
 * @example
 * 1. formUtil.decorator.decorate(); // default config applies
 *
 * 2. formUtil.decorator.decorate({renderInfo: false}); // overrides renderInfo
 */
formUtil.decorator.decorate = function(cfgOptions){
    var userCfg = cfgOptions || {};
    cfg = apply(userCfg, this.cfgDefaults);
 
    // Iterating through all the forms
    for(var i=0,j=document.forms.length; i<j; i++){
        this.decorateForm(document.forms[i], cfg);
    }
}

It is alway important to ensure that functions which accepts objects are passed the right objects for the function to work as expected. The best way to do this in javascript is to define the expected object and applying it to the incoming object the function receives, this way missing properties from the incoming object can be appropriately be replaced with the one from the expected object type, which i term the default configuration. An implementation of such a method is shown below.

?View Code JAVASCRIPT
/**
 * Copies all the non-existent properties in o from defaults
 *
 * @param {Object} o
 * @param {Object} defaults
 * @return {Object}
 */
function apply(o, defaults){
    var no = new Object();
    for(var p in defaults){
        no[p] = o[p] || defaults[p];
    }
 
    return no;
}

Below is the modified html that produced the undecorated form show above with our new attributes and the inclusion of our little javascript file.


Notice the custom attributes have been displayed in green for easy recognition.


?View Code HTML4STRICT
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
    <head>
        <title>Javascript Form Decorator</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <link type="text/css" rel="stylesheet" href="style.css" >
    </head>
    <body>
        <div class="notice">
            Complete the form and hit the Submit button.<br/>
            You can perform a quick search
            by providing values for searchable fields and click on the Search button.
        </div>
        <form id="applicationForm">
            <fieldset>
                <legend>APPLICATION FORM</legend>
                <p>
                    <label for="name" class="label">Name</label>
                    <input type="text" id="name" info="Official name as appears on the document" required="true" searchable="true" />
                </p>
                <p>
                    <label for="" class="label">Country</label>
                    <input type="text" id="country" required="true" searchable="true"/>
                </p>
                <p>
                    <label for="gender" class="label">Gender</label>
                    <input type="radio" name="gender" id="male" value="M" required="true" searchable="true"/><label for="male">Male</label>
                    <input type="radio" name="gender" id="female" value="F" /><label for="female">Female</label>
                </p>
                <p>
                    <label for="salary" class="label">Salary</label>
                    <input type="text" id="salary" />
                </p>
                <p>
                    <label for="postalAddr" class="label">Postal Address</label>
                    <textarea id="postalAddr" required="true"></textarea>
                </p>
                <p>
                    <label for="preferableCountries" class="label">Choose preferable work countries</label>
                <fieldset>
                    <input type="checkbox" value="gh" name="preferableCountries" id="gh" required="true" layout="Vertical"/><label for="gh">Ghana</label><br/>
                    <input type="checkbox" value="tg" name="preferableCountries" id="tg" /><label for="gh">Togo</label><br/>
                    <input type="checkbox" value="ch" name="preferableCountries" id="ch" /><label for="ch">China</label>
                </fieldset>
                </p>
                <p>
                    <label class="label" ></label>
                    <input type="submit" value="Save" /><input type="submit" value="Search" />
                </p>
            </fieldset>
        </form>
 
        <script type="text/javascript" src="../js/prototype-1.6.0.3.js"></script>
        <script type="text/javascript" src="../js/wz_tooltip/wz_tooltip.js"></script>
        <script type="text/javascript" src="form-util.js"></script>
        <script type="text/javascript">
        /*<![CDATA[*/
            window.onload = function(){
                form.decorateForm({searchable:{clickable:true, fn:'doSearch()'}});
                //form.decorateForm();
            }
            function doSearch(){
                console.info('Performing search...');
            }
        /*]]>*/
        </script>
 
    </body>
</html>

Here is the accompanying Cascading Style Sheet (CSS) which formats our page.

/*style.css*/
body{
    font: caption;
    font-size: 12px;
}
 
div.notice {
    border:1px solid orange;
    background: yellow;
    margin-bottom:5px;
    padding:5px;
    font-family:monospace;
}
 
label.label {
    display: block;
    float: left;
    width: 220px;
    text-align: right;
    margin-right: 5px;
    padding: 2px 0;
}
 
fieldset {
    border: none;
    padding: 0;
    margin: 0;
}
 
legend {
    /*font-weight: bold;*/
    margin-bottom: 0px;
    padding: 0px;
}
 
form p{
    margin-top: 1px;
    margin-bottom: 5px;
}

The form below shows the results of the javascript form decorator in action.

Decorated Form

Decorated Form

This approach to the problem could be explored to build a validator for a form using javascript. If you are not such a big fan of adding custom attributes to XHTML elements, you could alter the decorate function by passing it the list of elements to be decorated instead of picking all the elements having our custom attributes on them.

August 14, 2009

A hack to automatically upgrade all WordPress plugins with a single click

Filed under: Javascript,PHP,WordPress — Tags: , , — admin @ 1:45 pm

WordPress rocks!! I love wordpress as much as I love to watch Friends,one of my favourite television series courtesy ViaSat 1. You care to know who my favourite character is? Hmm, am not telling you. Lets hit the road running now. As a blogging tool, wordpress has created a niche for itself among it peers. It has great features like assigning an entry, or a post, to multiple categories, has a nice tagging system and recently, you can actually upgrade/update your WordPress Plugins automatically without the need to download the plugin and extract it into the plugins folder of your wordpress installation. The plugins upgrade indicator which displays the number of plugins that need upgrading is a pretty cool enhancement, so you don’t go looking for which plugins have latest releases you are not aware of.

Despite the fact that it is a great tool, upgrading a plugin can be a real pain in the a** of users since you need to click on the upgrade automatically link , depending on your chosen translation, for each of the plugins that need upgrading. 20 clicks for 20 different plugins that need upgrading to give you a clearer picture. So I posed to question to myself “Couldn’t this process have been made a lot easier on the user?“. And obviously, the answer was a “YES!!, it can.”

A WordPress admin console showing the need to click on upgrade automatically link for each installed plugin

A WordPress admin console showing the need to click on upgrade automatically link for each installed plugin

WHAT WE WANT TO ACHIEVE

We want to select the plugins to be upgraded, using the checkboxes provided beside each plugin, from the plugins page in the admin console, and choose Upgrade from the list of actions under the Bulk Actions menu, and click on the Apply button to upgrade all the selected plugins that need upgrading.

HACKING WORDPRESS

Since we don’t want to mess with the codes in plugins.php too much, we’ll make use of javascript and a little bit of PHP to achieve our task by doing the following:

  • Adding the Upgrade option to the list of Bulk Actions
  • Attaching a click event listener to the Apply action/button.
  • Creating a JSON (Javascript Object Notation) object to contain the plugins that requires upgrading.

Here is the code that does the trick.

<script type="text/javascript">
/*<![CDATA[*/
var json = {
	<?php
	if ( ! empty($upgrade_plugins) ){
		$url = 'update.php?action=upgrade-plugin&amp;plugin=';
		$noUpgradePlugins = count($upgrade_plugins);
		$i = 0;
		foreach($upgrade_plugins as $plugin=>$v){
			$i++;
			$actionurl = $url.$plugin;
			$action = 'upgrade-plugin_'.$plugin;
			$pluginurl=  str_replace( '&amp;', '&amp;', urldecode(wp_nonce_url($actionurl, $action)));
			$pluginId = str_replace('-','', substr($plugin,0,strpos($plugin,'/')));
	?>
			"<?php echo $pluginId ;?>": "<?php echo $pluginurl ;?>" <?php if($noUpgradePlugins>$i) echo ",\n";?>
	<?php	}
	}
	?>
}

/*
the 1 and 2 at the end of the some of the variable names represents
the top and down instances of the component respectively
*/
var btnApply1 = document.getElementsByName('doaction_active')[0];
var btnApply2 = document.getElementsByName('doaction_active')[1];

var cbo1 = document.getElementsByName('action')[0];
var cbo2 = document.getElementsByName('action2')[0];

// This adds the Upgrade option to the list of bulk actions
cbo1.options.add(new Option("Upgrade","upgrade-selected"));
cbo2.options.add(new Option("Upgrade","upgrade-selected"));

btnApply1.onclick = function(){
	if(cbo1.value!='upgrade-selected'){
		return true;
	}

	upgradePlugins();
	return false;
}

btnApply2.onclick = function(){
	if(cbo2.value!='upgrade-selected'){
		return true;
	}

	upgradePlugins();
	return false;
}

// Upgrades only the checked plugins that have upgrades/updates available
function upgradePlugins(){
	var tags = document.getElementsByName('checked[]');

	for(var i=0; i<tags.length; i++){
		if(tags[i].checked){
			p = tags[i].value;
			pluginId = p.substring(0,p.indexOf('/'));
			pluginId = pluginId.replace(/-/g,''); // replaces all instances of -

			url='';
			try{
				url = eval('json.'+pluginId);
			}
			catch(e){}

			if(typeof url=='string' &amp;&amp; url.length>0){
				var frame = document.createElement('frame');
				frame.src = url;
				document.getElementsByTagName('head')[0].appendChild(frame);
			}
		}
	}

}
/*]]>*/

</script>

All you need to do now is to copy the above codes and paste it at the bottom of the plugins.php file located in the wp-admin folder in your preferred editor. Save the changes, and refresh your plugins page, and give it a try.

HOW IT WORKS

WordPress automatic upgrade hack in action

WordPress automatic upgrade hack in action


On the plugins page, select Upgrade from the Bulk actions list, check the plugins you want to upgrade and click on the Apply button.

CONCLUSION

This demonstrates a practical application of DOM manipulation using Javascript. The downside of our little practical javascript solution to this problem is that each time you update WordPress itself, the plugins.php file will be replaced with the one from the latest version of WordPress.
I hope it was helpful.

Powered by WordPress