I often struggle to get my Javascript code organized, and have tried numerous ways to do so. I have tried putting relevant code into classes and instantiate as needed, then abuse jQuery’s data()
method to store everything (from scalar values to functions and callbacks). Recently, after knowing (briefly) how a jQuery plugin should be written, it does greatly simplify my code.
While there are still a lot to learn about plugin authoring, I would only jot down what I know right now. As the code may not conform to the best practice, please let me know if there is a better way.
To begin, we start with an empty html page
<html>
<head>
<title>Title</title>
</head>
<body>
<input type="text" id="a" /><br />
<input type="text" id="b" /><br />
<input type="text" id="c" />
<script type="text/javascript" src="./jquery-1.4.1.min.js"></script>
<script type="text/javascript" src="./script.js"></script>
</body>
</html>
The first included Javascript script is obviously jQuery, the second file (script.js) is my code, which is not named properly. The basic structure of the file should look like follows:
(function($) {
var module_init = function(plugin, module) {
return function(action) {
var arg = trim_argument(arguments);
return this.each(function() {
if($.fn[plugin][module][action]) {
$.fn[plugin][module][action]
.apply(this, arg);
} else {
throw(action + '() does not exist');
}
})
}
},
module_exists = function(plugin, module) {
return $.fn[plugin][module];
},
shift_argument = function(item, arg) {
result = [item];
for(i = 0; i < arg.length; i++) {
result.push(arg[i]);
}
return result;
},
trim_argument = function(arg) {
result = [];
if(arg.length > 1) {
for(i = 1; i < arg.length; i++) {
result.push(arg[i]);
}
}
return result;
};
// further code below this line
})(jQuery);
Let us just ignore the functions above for now and proceed to the actual plugin code first. What we're trying to achieve here is to post whatever written to #a is posted to #b, and #b post it to #c. The function call should be as follows:
$(selector).foo(sub_module, options);
// or
$(selector).foo(sub_module, action, [parameter_if_any]);
foo
is the name of the example plugin, and sub_module
determines which sub-module is responsible to carry out the actual action specified by action
. If options
is passed in instead, then it would trigger init()
for the respective sub-module.
In short, foo()
is there just to notify the sub-module to call the right action, hence the code should be simple, as follows (add after the last comment in the basic structure code shown above):
(function foo() {
$.fn.foo = function(module, param) {
var arg = trim_argument(arguments);
return this.each(function() {
if(module_exists('foo', module)) {
if(typeof param != 'string') {
$.fn.foo[module]
.apply($(this), shift_argument('init', arg));
} else {
$.fn.foo[module]
.apply($(this), arg);
}
} else {
throw(module + ' module does not exists');
}
});
}
})();
I have a habit to group all the relevant bits together, hence the code is enclosed in an immediately executed function block. It does what it is designed to do -- to delegate the function call to the respective sub-module. trim_argument();
is there to remove the first argument (in this case module
), and shift_argument();
is used to insert an argument into the existing argument list. These two function functions are written to enable us to manipulate the arguments to be used in Function.apply()
method. Of course, if the specified sub-module does not exist, the code will throw an exception.
Now that we have the main plugin code written, next is to start developing the submodules. Firstly, is a plugin for the text box #a. The textbox is supposed to show textbox #b and passes the input text to #b whenever the value is not null. The code is to be added right after the main plugin, as mentioned above.
(function alpha() {
$.fn.foo.alpha = module_init('foo', 'alpha');
var alpha = $.fn.foo.alpha;
alpha.init = function(options) {
$(this)
.data('options', $.extend({}, options))
.keyup(alpha.alpha_update);
};
alpha.alpha_update = function() {
$(this)
.data('options').b
.foo('bravo', 'alpha_update', $(this).val());
};
})();
module_init
is there to do the actual delegation of action. Again, it will check with the remaining arguments after removing the first, which is the action
. The function will then attempt to call the action if available, otherwise a new exception is thrown.
alpha.init
is a compulsory method that initializes the sub-module. On the other hand, alpha.alpha_update
is the actual code that updates another textbox via another sub-module, which is named 'bravo', and coded as follows:
(function bravo() {
$.fn.foo.bravo = module_init('foo', 'bravo');
var bravo = $.fn.foo.bravo;
bravo.init = function(options) {
$(this)
.data('options', $.extend({}, options))
.hide();
};
bravo.hide = function() {
$(this).fadeOut('fast');
};
bravo.show = function() {
$(this).fadeIn('fast');
};
bravo.toggle = function(value) {
if($(this).is(':hidden') && value.length > 0) {
bravo.show.call(this);
} else if($(this).is(':visible') && value.length == 0) {
bravo.hide.call(this);
}
};
bravo.alpha_update = function(value) {
$(this)
.val('bravo: ' + value)
.data('options').c
.foo('charlie', 'alpha_update', value);
bravo.toggle.call(this, value);
};
})();
The bravo code basically does what it is intended to, which is to receive update from alpha, and then toggle the display of the text box depending on the length of passed in value. After that, bravo is going to post the received update to charlie, as follows:
(function charlie() {
$.fn.foo.charlie = module_init('foo', 'charlie');
var charlie = $.fn.foo.charlie;
charlie.init = function(options) {
$(this)
.data('options', $.extend({}, options))
.hide();
};
charlie.hide = function() {
$(this).fadeOut('slow');
};
charlie.show = function() {
$(this).fadeIn('slow');
};
charlie.toggle = function(value) {
if($(this).is(':hidden') && value.length > 0) {
charlie.show.call(this);
} else if($(this).is(':visible') && value.length == 0) {
charlie.hide.call(this);
}
};
charlie.alpha_update = function(value) {
$(this).val('charlie: ' + value);
charlie.toggle.call(this, value);
};
After receiving data from bravo, likewise, it updates the textbox, then display or hide the textbook accordingly. For every action method call, this
keyword is usually similar to the jQuery callback functions, which refers to the respective html element (not wrapped with jQuery functions), hence the function call within the sub-module is often via Function.call() /* or */ Function.apply()
.
To run the code, one only need the following code:
$(function() {
var a = $('#a'),
b = $('#b'),
c = $('#c');
a.foo('alpha', { b: b, c: c }).focus();
b.foo('bravo', { a: a, c: c });
c.foo('charlie', { a: a, b: b });
});
That's all, we have done a very simple, and pointless plugin.