General MooTools UI
While I was trying to fully utilize progressive Enhancement I had another idea. Why not separate functionality and layout even while writing javascript/MooTools. That way it would be far easier to create different layouts for the same functionality without the need to reproduce code. Again let’s just look at an example:
simple tabs (my favorite example… :p)
var myTabs = new FlowTabs.inline( $$('.FlowTabs > h4'), $$('.FlowTabs > div') );
If you look at the source you will find an “$require(…);” it’s just the same as including the file directly with script or link tag. You can find more info about that in my previous posts about MPR. So what do we get with this code – It just gives us the functionality and looks pretty shitty, as only the necessary things are defined. Now let’s take at an example where we use the UI. simple ui tabs
UI.render('Plain');
So it looks now a little bit nicer, but that can all be done with CSS – and you are right as if we look at the source of the UI.Plain.Tabs it does nothing – it just includes 2 css files….
$require(MPR.path + 'Ui/Plain/Plain.js');
$require(MPR.path + 'Snippets/Css/Resources/clearfix.css');
$require(MPR.path + 'Ui/Plain/Resources/Plain.Tabs.css');
Ui.Plain.Tabs = new Class({
Implements: [Options],
options: {
},
initialize: function(options) {
this.setOptions(options);
}
});
How boring is that?!?! Seems like we need a little more complicated example. I guess most of you know the MochaUI – how about adopting some style from it…
MochaUI Tabs
$require(MPR.path + 'Ui/Mocha/Mocha.Tabs.js');
UI.render('Mocha');
only those 2 lines changed – we now load a different UI class and render a different theme called ‘Mocha’… so let’s look at it’s functions
initialize: function(tabs, options) {
this.setOptions(options);
this.attach(tabs);
},
attach: function(tabs) {
if ( $type(tabs) == 'string' ) tabs = $$(tabs);
if ( $type(tabs) == 'element' ) tabs = [tabs];
tabs.each( function(el) {
var inside = new Element('span', { html: el.get('html') } );
el.empty();
el.grab(inside);
}, this);
}
It’s again not really funny as it just creates an span inside every tab – we could have easily put this inside the “original” Tab Class. However what if the problem is a little bit more complex and the layout is also quite heavy on it’s own. Wouldn’t it be nice to separate these problems… Yeah I guess with just words you won’t believe me. So I will show you something more complex afterward.
But for now let’s find out how the UI knows what theme to render and with which parameters. For that we need to look at the FlowTabs.inline.js file… at the bottom you will find:
if ( typeof(UI) !== 'undefined' ) UI.registerClass({ 'Tabs': { 'param': '.ui-tab', 'name': 'FlowTabs.inline' } });
So what this does is basically it registers the FlowTabs Class as a Tabs Class with it’s default parameter and it’s name… Now when the UI is rendered it calls UI.theme.Tabs(param) and that’s exactly what we created earlier: “Ui.Mocha.Tabs = new Class({”
So let’s summaries this a little… until now it works like:
- create all Tabs + Content
- call the UI Class that will look for predefined classes and it will modify the layout accordingly
That’s pretty straight forward and in many situations this will work pretty well. However what about dynamically created tabs after the initial creation. I don’t know maybe loading with ajax or as a user populates some fields… So my FlowTabs class my support creating these Tabs on the fly but I somehow need to tell the UI that new Tabs are created and they need to be styled.
So now how do we do that:
FlowTabs.inline.js in the attach function (creating of the tabs) we just fire an Event and give it some things to work with:
this.fireEvent('onUiAttach', [el, j-1, content[i], this.tabsContainer]);
now what we need to do is use this onUiAttach Event. For that we just define this.options.refactor
Ui.Mocha.Tabs = new Class({
Implements: [Options],
options: {
refactor: {
options: {
onUiAttach: function(el, i) {
if ( (typeof(UI) !== 'undefined') && ( typeof(UI.Uis.Tabs) !== 'undefined' ) ) {
UI.Uis.Tabs.attach( $(el) );
}
}
}
}
},
If this is provided the UI will either refactor the class (if it hasn’t been instantiated) or it will set it options if the class has already been instantiated. [This doesn't seem to work currently - so note at the end] To detect that we have another function in FlowTabs.inline that will be called on initialize:
registerUi: function() {
if ( typeof(UI) !== 'undefined' )
UI.registerClass({ 'Tabs': { 'param': '.' + this.options.ui.tab['class'], 'name': 'FlowTabs.inline', 'class': this } });
}
it’s basically the same as the registration above, only that we now get the param from the option – this will help for example if we want to change the classes of the tabs; with that we don’t need to change them on the Tab creation script and on the Tab UI script… it will be automatically updated on the UI script. And we give the ui as ‘class’ this (that’s pretty important!).
So now with all that it doesn’t matter when the UI [UI.render('Mocha');] is called as:
- If you call it at the beginning before all the tabs are created it basically does nothing except that it refactors every Class that it might need. So these classes then automatically do the right thing when the Event onUiAttach is fired.
- If you call it at the end it will create all the necessary layout and set the options for every Class that’s registered so further elements that get attached get the layout automatically.
I still prefer to call the UI at the and as it might need a little bit less performance. [so also note at the end]
FlowWindows
So now let’s go for an more complex example. Let’s talk about windows… I think there are quite a few implementations out there… MochaUI for example – it’s Window Class is really cool. However it’s also really really big. So what if I just want a little div to resize and drag around. I think functionality and UI should be separated. I mean now I needed to write my own class as I didn’t “wanted” to use a complex layout as MochaUI…. the functionality would be great but you can’t get MochaUI Window without the Layout…
So basically that’s what I came up: simple windows. (I looked up some things at MochaUI window so it wasn’t that hard… :p)
If you look at it you see:
$require(MPR.path + 'Windows/FlowWindows/FlowWindows.js');
[...]
var myWindows = new FlowWindows( $$('.FlowWindow > h3')[0], $$('.FlowWindow > div')[0] );
myWindows.attach( $$('.FlowWindow > h3')[0], $$('.FlowWindow > div')[0] );
So this just creates all the divs needed for all the handlers to resize, the header, the controlls… all you need for a nice window… but it’s ugly. yeah sure it’s just for functionality no UI at all. Now let’s get some UI – the plain UI is not really something special (again no change with js – just css). To use it – there are just some small differences…
$require(MPR.path + 'Ui/Plain/Plain.Full.js');
[...]
UI.render('Plain');
we include Plain.Full which will give me all available UI elements of Plain – currently Tabs and Windows. In the end we just need to start UI.render …
So now comes the nice stuff, if I want to just change the style of the Tabs and the Windows I don’t need to change any of the Functionality Classes. I just need to change the UI with:
$require(MPR.path + 'Ui/Mocha/Mocha.Full.js');
[...]
UI.render('Mocha');
and we get Mocha Layout for Tabs and Windows.
The Mocha.Windows is a little bit more complicated as it needed a Canvas which gets updated on every size change. But in the end it’s really flexible and the best thing is if you want to change the functionality you don’t have to think (to much) about they layout. Moreover this also works the other way around. It’s far easiere to create just the layout for a specific structure than to create all the functionality…
one other thing that might come in handy is that you can start the UI without any functionality classes….Mocha UI only. The html markup has to be created accordingly by a server scripting language or so… Starting the UI is again pretty simple
$require(MPR.path + 'Ui/Mocha/Mocha.Full.js');
window.addEvent('domready', function() {
//say we want to use the WindowsUI and the TabsUI
UI.render('Mocha', {
'Windows': {
'param': '.ui-window'
},
'Tabs': {
'param': '.ui-tab'
}
});
});
I currently can only think of one rule you need to follow (maybe I get some more ofer time…):
- include the UI classes always after the functionality classes
Limitation:
- the setOptions method (if the UI is called at the end) only support options while with refactor you can override and define new functions.
Note at the end:
The setOptions doesn’t seem to work for subclasses if… yeah right if I knew that I would fix it, but for now the UI should be called at the top if you need some complex update functions like for MochaUI Window (for all the other stuff it also works at the end) [I guess the problem lies somewhere in between the UI - fireEvent('onUiInit'); and the UI.Mocha - this.options.refactor.options.onUiInit. If you catch any error - I'm open for suggestions...]