MooTools for Beginners Part 7 - Creating Flexible Classes Using Options, Events, and Event Management
By Ryan Florence, published 2010-01-27
Part of the issue MooTools for Beginners (1.2).
Last time we compartmentalized some code into a nice little MooTools Class called BouncyMenu that we can use anywhere. However, there are some HUGE improvements to be made. In this article we’ll be talking about how to make your MooTools Class flexible by using Implements
, Options
, and Events
. We’ll also talk about managing events added to elements and some discussion about binding. If you still consider yourself a beginner, understanding this article should be a big deal. Once you master Class
your javascript will never be the same.
Implements
MooTools exposes the prototypal inheritence of javascript with Class
. There are two properties of Class
used regularly to create really modular code called Implements
and Extends
. There are some major differences between them (for another post) but they both are similar in that they essentially copy all the methods of one class into another.
var ImageGallery = new Class({
showImage: function(){
alert('check it out!');
},
hideImage: function(){
alert("I'm out like brown trout");
}
});
var AwesomeGallery = new Class({
Implements: ImageGallery,
beAwesome: function(){
alert('Holy crap!');
}
});
window.addEvent('domready',function(){
var gallery = new AwesomeGallery();
gallery.showImage(); // alerts 'check it out!'
});
Options
, choices are good
There are two very common mixins (that’s what they’re called) that are used by a lot of mootools classes: Options
and Events
. First we’re going to drop Options
into our BouncyMenu from last time. We’re going to put the person using our class in control of the duration and distance of the animation.
var BouncyMenu = new Class({
Implements: Options,
options: {
duration: 500,
distance: 30
},
...
This sets up the defaults. If the user doesn’t set any of these options then the class will use what you put there. It’s important to choose good defaults. Next we have to set the options in the initialize function (remember, initialize
gets called when the class is instantiated.) Don’t forget to pass the options in as an argument.
initialize: function(options){
this.setOptions(options);
}
setOptions
is inherited from Options
and figures out if there were any options defined or if the instance ought to just use the defaults. Now, to access the options:
this.options.duration; // 500 or whatever you declare when constructed
So here’s a mooshell showing off our new functionality. Notice how we can define our own options when we instantiate the class. We define the duration
but not the distance
. So the class will use the default distance but our defined duration.
var menu = new BouncyMenu('container',{
duration: 250
});
A good indication that you should make something an option is when you hard code in stuff like strings and numbers. In setupElements
we use one of the options where before we hard-coded in some number:
setupElements: function(){
this.elements.each(function(element){
element.store('originalPadding', element.getStyle('padding-top'));
element.set('tween',{
// duration: 500 // old
duration: this.options.duration // new
});
}, this);
},
Bind: Binding the class instance to functions
Whenever you see this
in a class it should always reference the class instance. Don’t let your this
s get out of control! Consider this code:
var sauce = 'awesome';
var someFunction = function(){
console.log(this); // DOMWindow
};
var someFunction = function(){
console.log(this); // sauce
}.bind(sauce);
So we see that bind
allows us to control what this
is. And again, this
, in a class, should always reference the class so that you’ve always got access to its methods and properties (including its options.)
It just so happens that each
takes two arguments, a function, and binding, so we use it a bit differently in our setupElements
method:
setupElements: function(){
this.elements.each(function(element){
// inside this function `this`
// would refer to the element ...
element.set('tween',{
duration: this.options.duration // so this wouldn't work ...
});
}, this); // but `each` takes a final argument for binding
// so we bind `this` (the class instance) so `this.options` is
// the options of the class instance instead of element.options
// which isn't what we want
},
But we use it more traditionally in our attach
method:
attach: function(){
this.elements.each(function(element){
var events = {
mouseenter: function(){
element
.set('tween',{ transition: 'bounce:out' })
.tween('padding-top', this.options.distance); // another option
}.bind(this), // so we have to bind the class instance again
...
To reiterate, we have to bind this
to reference the class instance so we can access things like this.options
or the other properties and methods of a class. Excluding your class methods, whenever you type function(){}
you’re going to need to add .bind(this)
on the end if anything in that function needs access to the class’s stuff.
Adding and removing events with attach
and detach
.
As a general rule, if your class adds any event, no matter how small, you ought to give yourself (and other developers) a way to remove the events–even if you don’t think you’ll ever need to, I’m surprised how regularly I use detach.
To remove an event from an element we have to point to the same function as when we added it:
var feedWife = function(){
console.log('my wife is always hungry when she is pregnant');
};
$('element').addEvent('click', feedWife);
$('element').addEvent('click', otherFunction);
// later
$('element').removeEvent('click', feedWife);
// clicking will still fire `otherFunction`;
Easy enough. Keep in mind that you can’t easily remove an event that has an anonymous function assigned to it:
$('element').addEvent('click',function(){
console.log("Can't kill me without killing all click events!");
});
$('element').removeEvents('click'); // removes ALL click events
So that’s bad to do in your class. You’ll blow away any other events you may have added to the element. In order to properly remove events we need functions stored somewhere so we can add and remove the same function. When working with multiple elements it’s easiest to just store the function(s) with the element and retrieve them when we want to remove them. Let’s look at the entire attach
method again.
attach: function(){
this.elements.each(function(element){
var events = {
mouseenter: function(){ /* some stuff */ }.bind(this),
mouseleave: function(){ /* some stuff */ }
};
element.store('menuEvents', events);
// store the functions with the element
// which allows us to find the same functions
// later to remove
element.addEvents(events); // add them as events
}, this);
return this;
},
That’s pretty straightforward. Now the flexible part, detaching or removing the events:
detach: function(){
this.elements.each(function(element){
element.removeEvents(element.retrieve('menuEvents'));
});
return this;
}
Kaboom! Gone. However, by storing the functions with the element and then retrieving them to remove the events we don’t remove any other events that may be present on the element–only the ones we put on them in the class.
Should you always name these methods attach
and detach
? I think so.
Events make classes infinitely more useful
If you’ve used Fx
much you’ve probably used some events like onStart
, onComplete
. We can have our own custom events in our menu class. Events make classes way way way more flexible because it allows you to do other things as the user interacts with the website, like communicate with other classes. Very few classes don’t deserve some events. To use them is dead simple. First implement it and then call fireEvent
whenever you want that event to fire:
var Bomb = new Class({
Implements: [Options, Events],
detonate: function(){
// do explosive stuff
this.fireEvent('explode'); // do whatever else you want
}
});
And finally when the class in constructed:
var bomb = new Bomb({
onExplode: function(){
bomb2.detonate();
}
});
var bomb2 = new Bomb({
onExplode: function(){
alert('We all exploded!');
}
});
Here’s a shell with some events (onMouseenter
, onMouseleave
) added to our BouncyMenu class. Then in the constructor we’ve decided we want to change the text in log
to whatever the text in our menu is.
You may have noticed in the options we’ve got some stuff commented out:
options: {
/*
onMouseenter: $empty,
onMouseleave: $empty,
*/
duration: 500,
distance: 50
},
That just makes it easier to remember what events the class supports when you come back to it in a few months. You don’t need it, obviously, but it’s good practice. FYI, $empty
is just an empty function, in case somebody uncomments it out.
To get the events to fire we just plop in the fireEvent
method wherever we want. In this case, it’s in the attach
method in the mouseenter
and mouseleave
functions.
var events = {
mouseenter: function(){
element
.set('tween',{ transition: 'bounce:out' })
.tween('padding-top', this.options.distance);
this.fireEvent('mouseenter', element); // <-- here
}.bind(this),
mouseleave: function(){
element
.set('tween',{ transition: 'circ:out' })
.tween('padding-top', element.retrieve('originalPadding'));
this.fireEvent('mouseleave', element); // <-- here
}.bind(this)
};
Note that mootools doesn’t care about the on
business here. You can do it either way:
this.fireEvent('onDoinStuff'); // works
this.fireEvent('doinStuff'); // works
But:
new SomeClass({
doinStuff: function(){} // doesn't work
onDoinStuff: function(){} // works
});
So that’s it. Three tools to make your classes flexible and awesome: Options, Events, and event management (attach / detach.)