Thanks to my newfound ability to use a client's employees as my own (and offload work to
them), I finally got some time to sit down and learn some XBL.
XBL is the way to define reusable bits of UI and code in XUL. It's kinda the equivalent of a ASP.NET custom control, or any other system
where you can define your own "tags" to work with, allowing you to create bigger UI widgets. I've made
a version that makes widgets using document.createElement
and hboxes and vboxes.
Of course, thats a hack, and XBL is designed to do exactly what I want, now let's see if it'll let me do it.
What I want to do:
I want an easy way keep track of my time. I call it a task pool. I enter some text to describe
a task, then add it to my pool. I can then click a task to make it "active". Each task keeps track of how long it has been
active, and then at the end of the day I take the time to enter it all into the billing system so I can charge people more accurately.
I can also delete a task from the pool. Right now I have it all working fairly nicely, but I want to learn XBL, and this is a fine excuse.
The XBL tutorial on XULPlanet is excellent,
and I'm not going to repeat their content, I'm just going to recount my first time trying it out.
Creating my <task /> element
I started out by defining my XBL binding:
<?xml version="1.0" encoding="utf-8" ?>
<bindings xmlns="http://www.mozilla.org/xbl" xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<binding id="task">
<content>
<xul:hbox>
<xul:label value="taskName" />
<xul:label value="del" class="DeleteTask" />
</xul:hbox>
</content>
</binding>
</bindings>
and the corresponding stylesheet to actually bind:
task {
-moz-binding: url('taskPool.xml#task');
}
which allowed me to use the following XUL:
<task />
So now I had my own element to play with. To my delight, a simple:
var task = document.createElement('task');
worked properly, and I could call
appendChild on any node and add my task nicely anywhere I wanted.
Russ had done something
similar, and had ended up with some complicated solution involving RDF and templates to get it on the page, and I'm very glad a simple
method works too. First step down, now to make it do something.
Adding a field
Each task had to keep track of itself, so it was time to futz with some implementation. After reading the XULPlanet sections on adding properties,
I was ready to go. I have a javascript taskData object, and made a field and property for it on my task XBL, like so:
<binding id="task">
<content>...</content>
<implementation>
<field name="taskData" />
</implementation>
</binding>
Ok, easy enough. Then, in my javascript where I add this, I had:
var task = document.createElement('task');
task.taskData = taskData;
holder.appendChild(task);
I figured that would create my element, put the newly added taskData object into the "taskData" field, and add it to the page. That wasn't so.
After adding some alerts I figured out that the XBL binding doesn't actually get bound until the element is added to the DOM, so I had to change that
to:
var task = document.createElement('task');
holder.appendChild(task);
task.taskData = taskData;
Ok, that kinda makes sense, although I'd prefer the binding be done as a result of "createElement". This way I'm adding the task
to the page before it is ready to view, which feels a little dirty.
Adding a property
The next step was to have that piece of data change the label, so the text on the task isn't the default "taskName" all the time.
For this, I needed to create a property (easy), and change the label's value. The label doesn't have a unique id,
as there could be several of them on the page. It's an anonymous node, and the only ways to get to it is using the document.getAnonymousNodes
or document.getAnonymousElementByAttribute function.
I opted for the getAnonymousElementByAttribute route, the XBL becoming:
<binding id="task">
<content>
<xul:hbox>
<xul:label value="taskName" anonid="taskName" />
<xul:label value="del" class="DeleteTask" />
</xul:hbox>
</content>
<implementation>
<field name="_taskData" />
<property name="taskData" onget="return this.getAttribute('_taskData');">
<setter>
this.setAttribute('_taskData', val);
var lbl = document.getAnonymousElementByAttribute(this, 'anonid', 'taskName');
lbl.value = val.name;
return val;
</setter>
</property>
</implementation>
</binding>
Note that the label up top now has an "anonid" attribute. I originally used "id", but then I got a very speedy response on the
a
question I asked on the
XULPlanet Forums, and
Enn (
Neil Deakin, I think) told me to use "anonid" instead. Apparently there are
some special things that happen with "id".
The "taskData" property can't actually store anything, so I made a "_taskData" field to
use, and then set everything the way I wanted. It worked quite nicely. I'm not a fan of defining code using XML, but I'm beginning
to see how very powerful widgets could be built up this way.
Adding a method
Now that I have all my data set up, I want to be able to start and stop my tasks by clicking on the task name. So, I make my "on" property,
and put an "onclick" on the name label to toggle it:
<binding id="task">
<content>
<xul:hbox>
<xul:label value="taskName" anonid="taskName" class="Closed" onclick="this.parentNode.parentNode.on = !this.parentNode.parentNode.on;" />
<xul:label value="del" class="DeleteTask" />
</xul:hbox>
</content>
<implementation>
<field name="_taskData" />...
<field name="_on" />
<property name="on">
<getter>
var on = this.getAttribute('_on') || 0;
return 1 == on;
</getter>
<setter>
this.setAttribute('_on', val ? 1 : 0);
var lbl = document.getAnonymousElementByAttribute(this, 'anonid', 'taskName');
lbl.className = val ? 'Open' : 'Closed';
</setter>
</property>
</implementation>
</binding>
Take a look at the "onclick" of the taskName label. I had to use the standard DOM properties to get up the tree to my original <task> element.
If you look at the "on" property, you see its actually storing a 1 or 0, and then converting those to boolean values in the property. When
I tried to store "false" in the field, it didn't really work right. It would toggle properly once, and then think that !false == false. I
guess it was being interpreted as a string. Now I can click the taskName and flip the css class, letting me see which task is active.
The next step is to actually keep track of the time. This will be done with a closure and a setTimeout.
And now, about 3 hours into this, I now see that fields aren't doing what I want them to. When I passed in a javascript object for taskData, the
field stored the string "[object Object]", not the actual object. This means I can't make a little struct-ish object and put it in a field,
I have to have a field for each piece of data. Also, another limitation of fields is you can't set them directly. You can't say:
fieldName = 1;, you must say:
this.setAttribute('fieldName', 1);. That makes them a pain to deal with unless you wrap
it in a property. So that means some major re-working of my <implementation> section.
With the fields and properties redefined, it's time to make the main counting method:
<implementation>
<method name="count">
<body>
if(this.on){
var f = function(el){
return function(){el.count();};
}
this.seconds++;
setTimeout(f(this), 1000);
}
</body>
</method>
...
</implementation>
The closure is needed to keep the task in scope. Now, after changing some of the properties to also sync up the labels, all that's left is making
the delete link actually delete. Seeing as how this about about 500 lines longer that I intended it to be, I'm going to finish the rest without
detailing it.
Conclusions
I think XBL is a reasonable way to define reusable chunks, but still has its annoyances:
-
The XML syntax gets in the way sometimes, and makes the code much less readable than it could be if
it were all in a script block. It'd be great if there was a src attribute to <implementation>, so
you could just say <implementation src="task.js">, and have you methods and properties defined in
javascript cleanly.
-
The fields don't seem to work well for booleans, and don't accept objects at all. They need to be wrapped
by redundant properties to be easily used in other blocks of code.
I wish there was a <propertyField> element that persisted data without needing the getting and setting of an attribute.
Something like:
<propertyfield name="seconds" />
instead of:
<field name="_seconds" />
<property name="seconds" onget="return this.getAttribute('_seconds');" onset="this.setAttribute('_seconds', val); return val;" />
-
The code and UI are extremely tied together, which makes me a little nervous, but isn't really that much different
from an ASP.NET custom control.
I'll keep playing around with it, and post anything interesting I find. If I can convince Rebecca to do even more work for me, I might
package this up as an extension and post about that process.