XUL supports templating to create a block of content from a datasource query. The XUL Template Guide has lots of detailed information on using XUL templates. XUL provides template query processors for RDF, XML and SQL (mozStorage). The templating system also supports building custom query processors. Custom query processors are XPCOM components, must implement the nsIXULTemplateQueryProcessor interface and follow some conventions for when registering the component.
In this example, we will create a simple JavaScript XPCOM component. The component will hold a small array of JavaScript objects as its datasource. In practice, you would use your own custom source of data.
Here is an example of what our XUL might look like when using a custom query processor:
<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<grid>
<columns>
<column flex="1"/>
<column flex="3"/>
<column flex="2"/>
<column flex="1"/>
</columns>
<rows datasources="dummy" ref="." querytype="simpledata">
<template>
<row uri="?">
<label value="?name"/>
<label value="?age"/>
<label value="?hair"/>
<label value="?eye"/>
</row>
</template>
</rows>
</grid>
</window>
A few things to note. We are not really using the datasources in our sample component, so we set it to a dummy value. An empty string would be ok too. The querytype is important. It is used to create an instance of our XPCOM object. The contract id of our XPCOM component should be of the form "@mozilla.org/xul/xul-query-processor;1?name=xxx", where the xxx is the querytype used in the XUL template block.
Here is our sample JavaScript XPCOM query processor:
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
// basic wrapper for nsIXULTemplateResult
function TemplateResult(aData) {
this._data = aData;
// just make a random number for the id
this._id = Math.random(100000).toString();
}
TemplateResult.prototype = {
QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsIXULTemplateResult]),
// private storage
_data: null,
// right now our results are flat lists, so no containing/recursion take place
isContainer: false,
isEmpty: true,
mayProcessChildren: false,
resource: null,
type: "simple-item",
get id() {
return this._id;
},
// return the value of that bound variable such as ?name
getBindingFor: function(aVar) {
// strip off the ? from the beginning of the name
var name = aVar.toString().slice(1);
return this._data[name];
},
// return an object instead of a string for convenient comparison purposes
// or null to say just use string value
getBindingObjectFor: function(aVar) {
return null;
},
// called when a rule matches this item.
ruleMatched: function(aQuery, aRuleNode) { },
// the output for a result has been removed and the result is no longer being used by the builder
hasBeenRemoved: function() { }
};
// basic wrapper for nsISimpleEnumerator
function TemplateResultSet(aArrayOfData) {
this._index = 0;
this._array = aArrayOfData;
}
TemplateResultSet.prototype = {
QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsISimpleEnumerator]),
hasMoreElements: function() {
return this._index < this._array.length;
},
getNext: function() {
return new TemplateResult(this._array[this._index++]);
}
};
// The query processor class - implements nsIXULTemplateQueryProcessor
function TemplateQueryProcessor() {
// our basic list of data
this._data = [
{name: "mark", age: 36, hair: "brown", eye: "brown"},
{name: "bill", age: 25, hair: "red", eye: "black"},
{name: "joe", age: 15, hair: "blond", eye: "blue"},
{name: "jimmy", age: 65, hair: "gray", eye: "dull"}
];
}
TemplateQueryProcessor.prototype = {
QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsIXULTemplateQueryProcessor]),
classDescription: "Sample XUL Template Query Processor",
classID: Components.ID("{282cc4ea-a49c-44fc-81f4-1f03cbb7825f}"),
contractID: "@mozilla.org/xul/xul-query-processor;1?name=simpledata",
getDatasource: function(aDataSources, aRootNode, aIsTrusted, aBuilder, aShouldDelayBuilding) {
// TODO: parse the aDataSources variable
// for now, ignore everything and let's just signal that we have data
return this._data;
},
initializeForBuilding: function(aDatasource, aBuilder, aRootNode) {
// perform any initialization that can be delayed until the content builder
// is ready for us to start
},
done: function() {
// called when the builder is destroyed to clean up state
},
compileQuery: function(aBuilder, aQuery, aRefVariable, aMemberVariable) {
// outputs a query object.
// eventually we should read the <query> to create filters
return this._data;
},
generateResults: function(aDatasource, aRef, aQuery) {
// preform any query and pass the data to the result set
return new TemplateResultSet(this._data);
},
addBinding: function(aRuleNode, aVar, aRef, aExpr) {
// add a variable binding for a particular rule, which we aren't using yet
},
translateRef: function(aDatasource, aRefstring) {
// if we return null, everything stops
return new TemplateResult(null);
},
compareResults: function(aLeft, aRight, aVar) {
// -1 less, 0 ==, +1 greater
var leftValue = aLeft.getBindingFor(aVar);
var rightValue = aRight.getBindingFor(aVar);
if (leftValue < rightValue) {
return -1;
}
else if (leftValue > rightValue) {
return 1;
}
else {
return 0;
}
}
};
var components = [TemplateQueryProcessor];
function NSGetModule(compMgr, fileSpec) {
return XPCOMUtils.generateModule(components);
}
Our sample query processor is very simple. A few explanatory notes:
- We are using a the
getBindingForrather thangetBindingObjectForto simplify the code. The variable passed intogetBindingForstill has the "?" prepended to it, so be sure to handle it appropriately. - We are not handling any
datasources, but instead hard code the datasource in the component. You could easily extend this sample to handle multiple datasources by checking thedatasourcesvalue ingetDatasourceandinitializeForBuilding. - We do not make use of any
<query/>or<rule/>elements in the XUL template block. These could be used to support filtering the datasource.
