Consider a simple HTML element that we want to expand and contract by tapping a different target element.
<a href='expandable-element-1' data-expander>
<!-- some link markup styled using an `expanded` class -->
</a>
<!-- unknown number of elements -->
<div id='expandable-element-1'>
<!-- some markup -->
<div>
This could be (and usually is) accomplished quite easily with a few tightly coupled JavaScript methods, defined anonymously and/or globally.
$('[data-expander]').on('click', function(e){
var $trigger = $(this);
var $target = $($trigger.attr('href'));
e.preventDefault();
if($target.is(":visible")){
$target.slideUp('slow', function(){
$trigger.removeClass('expanded');
});
}else{
$target.slideDown('fast', function(){
$trigger.addClass('expanded');
});
}
});
This can also be achieved with an object oriented design, using fundamental JS constructs (e.g. no additional dependencies). This allows a single 'class' to encapsulate all the responsibility for the JavaScript associated with this element.
With such a design, the cost of future change is drastically improved compared to a pile of procedural JavaScript. This is especially true when considering a real-world project as a whole, with dozens of elements all with often overlapping needs.
There are, of course, many optimizations and best practices available and recommended (e.g. settings/options injection and defaults, prototype inheritance, firing custom events, etc.). Below is a concise, though real-world, example that provides a foundation for future changes such as these, and others.
Note: the
self
variable is used as a convenience to reduce common confusion around thethis
keyword.
// assets/javascripts/elements/_expander.js
function Expander($el) {
var self = this;
self.$trigger = $el;
self.$target = $el.attr('href');
self.activeClass = 'expanded';
self.init();
}
Expander.prototype = {
constructor : Expander,
init : function() {
var self = this;
console.log('Expander initialized');
// ensure expansion class matches initial state
if(self.isExpanded()){
self.$trigger.addClass(self.activeClass);
}
// register to expand or contract on target click
self.$trigger.on('click', function(e){
e.preventDefault();
if(self.isExpanded()){
self.contract();
}else{
self.expand();
}
});
},
expand : function() {
var self = this;
self.$target.slideDown('fast', function(){
self.$trigger.addClass(self.activeClass);
});
},
contract : function() {
var self = this;
self.$target.slideUp('slow', function(){
self.$trigger.removeClass(self.activeClass);
});
},
isExpanded : function() {
var self = this;
return self.$target.is(":visible");
}
}
$(window).load(function(){
// initialize objects for each expander available on the page
$('[data-expander]').each(function(){
var expander = new Expander($(this));
});
});