MooTools Classes: How to use them

February 5th, 2008, posted by Valerio

A very entry-level article: if you think you can beat MooTools Hero in expert mode, feel free to totally skip this.

Cars

So, what is a Class? A Class is a car factory. A factory that throws out an infinite number of cars.

If you need to make a trip, then another one, then another one, you will probably use the same car over and over. Only if you decide to change something about your car, like the engine, or the color, or the materials for the seats, then you'll need another one.

Let's make a totally facetious example out of it, and buy a Car from the Car store! Say I want to buy a Smart Car:

var mySmartCar = new Car({model: 'smart'});

Now that I have my car, it's parked in my virtual garage. Let's make a virtual trip!

mySmartCar.goTo('Washington DC');

That was nice! Let's go to NYC:

mySmartCar.goTo('New York');

As you can see, I'm using the same car to go to Washington DC and New York City. They're geographically close, so my smart car will suffice. What happens if I need to cross the cold Russia? Then I'm gonna need another car. A bigger, more powerful car. That's exactly when I must use new again.

var myBigCar = new Car({model: 'Jeep'});

Let's use it now:

myBigCar.goTo('Russia').goTo('China');

I wish going to China was this easy. :P

Classes

In MooTools, we have classes to perform different types of actions. We have Classes that deal with data (Request) or classes that display visual effects (Fx) and many more that I'm not talking about here today.

Fx.Morph, for example, is your factory. It makes Fx.Morph cars. Let's buy a car now: I want it to have a duration of 1000 ms, and I want it to be linked to this certain HTML element called "element":

var myMorph = new Fx.Morph(element, {duration: 1000});

I have a car now, that we call Instance. Let's use it to change the height and width for the element previously specified to 200:

myMorph.start({height: 200, width: 200});

Nothing easier.

These days, I see the same mistake being made over and over again: People buy a Car, and they make only one trip with it:

new Fx.Morph(element, {duration: 1000}).start({height: 200, width: 200});

This is usually attached to a click event, which makes things even worse:

element.addEvent('click', function(){
    new Fx.Morph(this, {duration: 1000}).start({height: 200, width: 200});
});

Well, this is not how life MooTools works! Just like in real life, there is no need to buy a car each trip you make, unless you want to significantly change the properties of this car. This use and discard approach is wrong in so many ways.

First of all, it's memory inefficient, as a new Class instance is created every time you click the button. Second, it's also speed inefficient, as the class constructor is called every time you click the button. Third, using this approach will make it impossible to take advantage of the Instance properties.

In the Fx Class Instances there are a number of properties and checks being made to see if the effect is in execution, and a number of options you can define to determine the behaviour of subsequent start calls if a previous one is still running.

This is how you write the previous example correctly:

var myMorph = new Fx.Morph(element, {duration: 1000});

element.addEvent('click', function(){
    myMorph.start({height: 200, width: 200});
});

This way, I create the Instance only once, and reuse it every time the user clicks the button! It won't screw up the effect for multiple clicks, and our OCD will have a rest for a change.

Same goes for the Request classes. What happens if I send the same request multiple times? Should I cancel the last one? Should I send both? Should I queue them?

If you do not properly use instances, a new request will be made each time. Same goes for Fx: a new fx on the same element will start at the same time, basically screwing up the whole animation.

MooTools is not Script.aculo.us

This approach of "use and discard" was used (and its still used afaik) in Script.aculo.us.

As an early scriptaculous user, I always struggled with the "multiple start calls" problem, and more often than I should, I took advantage the onComplete (afterFinish, in Script.aculo.us) option to set flags, and checked them in my click events, to avoid multiple executions at the same time.

That's exactly why I decided to write my own effects library, called moo.fx (which evolved into MooTools), so I could use classes--and their instances, as reusable objects.

This is not to criticize the Script.aculo.us approach. Since the library is designed to work that way, that's exactly how you should use it. MooTools however, is not designed to work this way, and using it the same way as Script.aculo.us is a mistake.

16 Responses to “MooTools Classes: How to use them”

  1. Jesse Donat Says:

    I am guilty of

    $$('#nav a').addEvent('click', function(){ new Fx.Morph(this, {duration: 1000}).start({height: 200, width: 200}); });

    which as you made it sound would be terrible

  2. SchizoDuckie Says:

    I always do stuff like this:

    $$('#nav A').addEvent('mousover', function() { if typeof this.morph == 'undefined' || !this.morph) this.morph = new Fx.Morph(this, {duration: 1000}); this.morph.start({height: 200, width: 200}); });

    no harm, no foul :)

  3. nfq Says:

    Great article guys. As a designer by heart, this is easy to understand, as it contains everyday logic, which is great for us simple folk. Makes total sense once read, especially the mistake of rehashing (oops, a pun?) code when not necessary!

    Hats off! Write more articles like this please, they help allot!

  4. Matt Says:

    This is how I would do it in 1.2:

    element.set('morph', {duration: 1000}).addEvent('click', function(){ element.morph({height: 200, width: 200}); });

    Does all the class instantiation work for you, and looks pretty doing it. :)

  5. Francisco Says:

    Just to say that I really appreciate this insight view. Even though this is more or less what I was doing it's really great to hear from you. And it's always good to remember the basics.

    Thanks, Francisco

  6. I use something like

    element.retrieveOrStore("fx",function () { return this.effects(/.../); }).start(/.../);

    So it'll always be one instance of the effect. The function parameter inside retrieveOrStore is called only once, and the returned value is stored in the element storage.

    retrieveOrStore is my own extension to Element:

    Element.implement({ retrieveOrStore:function (key,getValueFunction) { if (!this.retrieve(key)) this.store(key,getValueFunction.call(this)); return this.retrieve(key); } });

  7. Marc Says:

    Actually, Element::retrieve accepts a second optional parameter which will return a 'default' if the key doesn't exist, so you don't need to retrieveOrStore.

  8. Caio Says:

    var myBigCar = new Car({model: 'Jeep'}); Let's use it now: myJeep.goTo('Russia').goTo('China');

    Shouldn't it be myBigCar.goTo('Russia').goTo('China');?

  9. Valerio Says:

    @SchizoDuckie: definitely memory-wise!

    @Caio: Yup, fixed, thanks!

  10. Pavan Says:

    nice article, and amazing explanation for people taking their first baby steps.. I would appreciate some more articles showing more insights into mootools

  11. Dave Says:

    Thanks for clearing up classes, I thought they were quite difficult :D

  12. Nicolas Says:

    http://clientside.cnet.com/wiki/mootorial/02-class <--- This is what thought me about MooTools classes more than anything (the rest of the mootorial is also very very interesting and very up-to-date).

    Cheers.

  13. Ciao Valerio, I was playing around with mooTools, because I need it in a project (i'm writing a Treeview using lists) I want to use MT to do some pretty nifty effects, Like using FX.Slide for exploding/imploding the nodes of the treeview..

    So why post this comment? Simply because is perfectly in topic with this post.. imagine what happens when i have lots of node on the treeview? for each node i had to set up a FX.Slide effects, so if i have 100 nodes i have 100 Effects created..(or 100 car bought..) so i thought to use Event bubbling to shrink down memory stressing.. but like FX.slide work now I just can't do it.. I had to rewrite the function for passing the object to toggle so i can have only 1 FX.Slide object for all the 100 nodes. :p

  14. Xeoncross Says:

    That is JUST what people need to read.

    I deal with the same thing in PHP. You only make 1 object for a function and then use it forever. And if you plan ahead right, you will never need a new car for your purpose because you will have built it right from the start. Just imagine trying to find the right car in hundreds of instances!

    If people built cars right I we wouldn't need to buy a new one as often as we do ;)

  15. @Marc - Sure i need retrieveOrStore, otherwise an instance will always be created

    element.retrieve("fx",element.effects()) // -- An instance is created and sent as an argument to Element::retrieve, though only the first call to this retrieve actually puts the value in the element's storage.

    element.retrieveOrStore("fx",function () { return thie.effects(); }) // -- A function reference is created and sent to retrieveOrStore but it won't get called unless i call it manually. retrieveOrStore checks whether the value is in the element's storage, and if it doesn't -- calls the function and stores the returned value. Notice the next time you call the retrieveOrStore, the function argument won't get called and new instance won't be created.

  16. Njy Says:

    @Elad: I was thinking about the same thing, in general a callback is too much verbose for most of the case (is better to use a value) but in some cases it's better to provide a callback instead of an actual value. But how can this be done? It is not possible to make parameter-type detection (if it's a function, call it, otherwhise use it directly) because a function may be the wanted result. Maybe adding a third parameter to the retrieve method to specify to execute the callback, instead of returning it, something like (code written in crappy-but-quite-clear-way ;) ...):

    retrieve : function(key, defaultValue, isACallback) { isACallback = $pick(isACallback, false); [...] // must return the default value if (isACallback) { return defaultValue(); } else { return defaultValue; } }

    what do you think Valerio, Olmo & Co? So all the old calls remain unchanged, but with the third parameter you may treat the default value as a callback instead of the actual value, and save a lot more memory, cpu time and so on...

    Anyway, thanks for the good work!

Sorry, comments are closed for this article.