Defining a custom control requires us first to decide how it will be used by authors. This is important because we want to ensure that the expertise required to use our custom control is much less than the expertise required to build it. We can achieve this if we try as much as possible to leverage skills that authors already have. For example, if we were designing an LED display and we wanted to give authors the ability to set the colour of the digits, we should use the CSS color property, rather than defining some function. Similarly, if we were creating an analogue clock, we'd almost certainly want to allows authors to decide how large or small each use of the clock should be, and that could easily be done with the CSS width and height properties.
In this section we show which factors to consider when working out how a control will be used, how authors can link to custom controls and make them available to a form, and finally how to actually create custom control definitions--which we'll illustrate with a simple map widget, and some clocks.
The first thing that we need to do when creating a custom control is to decide how we want it to be used, i.e., what is the mark-up that authors should use to make use of our new features?
One custom control that we're going to create will have an image that is a map, centred on a specified longitude and latitude. We'll therefore use the 'value' of the control to provide that location. However, we'd like to also control the size of the map, since sometimes we'd like to have thumbnails, and sometimes much larger images. The obvious way to do this would be to make use of the CSS properties width and height, and allow them to govern the size of the map. Finally, since we don't want the user to be able to update the value of the location we'll use xf:output as the control that we'll be modifying.
A typical usage of this 'interface' might be:
<xf:output value="'51.523004;-0.106859'" appearance="geolocation" style="width: 200px; height: 100px;" />
which gives us a map centred on the formsPlayer office in London.
We'll also create a number of clock custom controls, to illustrate the many different ways we could approach this. For our LED clock, we might want the background colour of our control to be used to determine the background of the clock, and we might want the text colour to set the colour of the digits. As before, since we don't want the user to be able to update the time we would use an xf:output, so our 'interface' might look like this:
<xf:output value="'14:52:00'" appearance="fp:LED" style="color: red; background-color: black;" />
Custom controls are extensions to more basic controls so we'll always need some control to enhance. But it's a good idea to choose a base control that reflects in some way the functionality of the control you are trying to build. For example, if you are creating a video player widget, base it on xf:output rather than xf:range or xf:upload. Similarly, even if you are creating a complex control that is more like a dialog-box which is made up of lots of other controls--if ultimately all this control does is to allow a user to choose a colour from a palette then it can be based on xf:select1.
By building on a control that already has some of the behaviour you require, you automatically get a good fall-back mechanism if your forms are used on processors that don't support custom controls, and perhaps more interestingly, you also create the possibility of users replacing your custom control with one of their own choosing, from a personalised stylesheet.
Since we are going to want some of our controls to be maps and others to be clocks, we need to provide a 'hook' that identifies which controls need to be extended.
Extensions are added based on rules defined using XPath, so we can add pretty much any hook we want. A common convention is to use the class attribute from XHTML or the appearance attribute from XForms. However, it is also possible to use a more dynamic approach, and add extensions based on the underlying data type in the XForms model. In both of our examples above we used the XForms appearance attribute:
<xf:output value="'52;4'" appearance="geolocation" /> <xf:output value="'14:52:00'" appearance="fp:LED" />
But we could also have used the HTML class attribute. In the following example, if a user browsed to our form using an XForms processor that did not have a clock custom control, they would still see the time in green:
<style type="text/css"> .clock { color: green; } </style> <xf:output value="'14:52:00'" class="clock" />
In the next section we'll look at how the custom controls we create can be made available to authors.
We saw earlier how we generally provide an extension 'hook' on a base control that allows us to extend it with our custom functionality. The binding is expressed in a set of binding rules, so the first step is to provide a link to some rules. We do this by using the HTML link element, as follows:
<?xml version="1.0" encoding="utf-8" ?>
<html
xmlns="http://www.w3.org/1999/xhtml"
xmlns:xf="http://www.w3.org/2002/xforms"
>
<head>
<title>Map widget</title>
<link rel="bindings" href="my-bindings.xml" />
</head>
<body>
<xf:output value="'52;4'" appearance="geolocation" />
</body>
</html>
The binding rules simply map some expression to a file that defines the behaviour we want. For example, to attach the functionality from the file geo.xbl to all xf:output controls that have appearance="geoposition" we would use the following rule (in my-bindings.xml):
<?xml version="1.0" encoding="UTF-16"?> <bindings xmlns="http://www.x-port.net/bindingresolver/" xmlns:xf="http://www.w3.org/2002/xforms"> <binding match="xf:output[@appearance='geoposition']/xf:pe--value" binding="geo.xbl" /> </bindings>
Note that we don't actually bind to the xf:output control as a whole, but the ::value part. This is because a control is actually made up of not just the interactive part (such as an input box), but also includes a label, as well as help, hint and alert messages. Since we may want to bind to each of these items with different custom controls, we always bind to the smallest--or most specific--part of a control that we can. (For more on this, see Anatomy of a control.)
Now that we've made the custom control available to a form, it can be used in any of the ways that the equivalent base control can be used, as we saw earlier in Using custom controls in a form. Our next step is to define the control.
We've established the connection between our form and a list of controls (using link), and we've indicated the rules under which our functionality should be bound to some control (in my-bindings.xml); now we need to define the custom control itself, which we'll place in the file referred to in the binding definitions--geo.xbl.
Our simple control will be based on the standard image control. Since the image control handles its own initialisation and event registration, and because we don't have any special initialisation to do in our map control, then we don't need to add any specific handlers. This makes our control very straightforward, since all it will need to do is support the setValue method which will be called by the backplane whenever the bound data changes.
Since we've decided that the input to our control will be a string that contains a longitude and latitude:
<xf:output value="'51.523004;-0.106859'" appearance="geolocation" style="width: 200px; height: 100px;" />
then the setValue method will need to split the input value apart to get the two coordinates, and then use those values to create a URL for the map image. This is then placed into the src attribute in the image control.
The first thing we need to do is create a document that will hold our custom control functionality. A bindings document can hold one or more control definitions, so the general format is to have one or more binding elements contained within a bindings element:
<?xml version="1.0"?>
<bindings xmlns="http://www.mozilla.org/xbl">
<binding>
...
</binding>
<binding>
...
</binding>
</bindings>
The binding element defines our control, and since it extends the image control, we indicate this using the extends attribute:
<binding extends="http://libxh-apps.googlecode.com/svn/trunk/_chrome/image.xbl">
...
</binding>
It's possible for controls that are extended to in turn be an extension of another control, to any depth.
setValue methodTo create a method we need an implementation element, which will in turn contain as many methods and properties as we care to define for a control. In the simple example we're creating here, we need only one method--setValue--and it will take a single parameter, of the value passed from the backplane:
<binding extends="http://libxh-apps.googlecode.com/svn/trunk/_chrome/image.xbl">
<implementation>
<method name="setValue">
<parameter name="newVal" />
<body>
...
</body>
</method>
</implementation>
</binding>
setValue methodThe actual code for the function is written in JavaScript. The first thing we want to do is obtain the longitude and latitude values from our input string, which we've decided will look something like this:
51.523004;-0.106859
then we need to have a regular expression that looks for a signed floating point number, followed by a separator, followed by another signed floating point number. To give a little flexibility, we might as well allow alternate separators, like a space or comma, and any number of them. This will allow any of the following strings to be parsed:
51.523004 ; -0.106859 51.523004,-0.106859 51.523004 -0.106859 51.523004,; ,; -0.106859
The code to obtain the longitude and latitude is simply:
<method name="setValue">
<parameter name="newVal" />
<body>
try {
newVal.match( /([-+]?\d*\.?\d+)[\,\s\;]*([-+]?\d*\.?\d+)/ );
var nLong = RegExp.$1;
var nLat = RegExp.$2;
You'll recall that we also decided to set the width and height of the map based on values from the CSS:
<xf:output value="'51.523004;-0.106859'" appearance="geolocation" style="width: 200px; height: 100px;" />
To achieve this we need to retrieve the current width and height, and pass it on to the map server:
var nLong = RegExp.$1;
var nLat = RegExp.$2;
var nWidth = parseInt(this.parentElement.currentStyle.width, 10);
var nHeight = parseInt(this.parentElement.currentStyle.height, 10);
Now that we have the longitude and latitude on which to centre the map, and we know how big the map should be, we can work out the URL for the image. Although there are many ways we could do this, in this particular control we're going to use a server that conforms to the OpenGIS Web Map Server (WMS) specification. This provides a service where the URL used to request an image can contain all sorts of information to indicate what part of the world the map should show, what type of image should be returned (JPEG, PNG, etc.), what type of map should be shown (terrain, weather, urban centres, etc.), and much more. To illustrate how WMS should work, the following query should work on any WMS-compliant server:
http://[some server and path]?VERSION=1.1.1&REQUEST=GetMap&SRS=EPSG:4326&BBOX=-30.106859,36.523004,29.893141,66.523004&WIDTH=200&HEIGHT=100&LAYERS=DEM_30sec&STYLES=&FORMAT=image%2Fpng&BGCOLOR=0xFFFFFF&TRANSPARENT=FALSE&EXCEPTIONS=application%2Fvnd.ogc.se_inimage&QUALITY=QUICKEST
The result will be a map image that has a bounding box from -30.106859,36.523004 to 29.893141,66.523004, is 200px by 100px, is a PNG file, and so on. The following is the image obtained from European Space Agency servers, using these exact parameters:
Returning to our code, we'll create our image by concatenating the parameters we need to create the URL, and then storing this value into our control's src property. The image control we've inherited from has conveniently given us a property to write the URL into; we don't need to worry how that property maps to an attribute in mark-up, since all we need to do is set this.src to some value:
var nWidth = parseInt(this.parentElement.currentStyle.width, 10);
var nHeight = parseInt(this.parentElement.currentStyle.height, 10);
this.src = "http://ssems1.esrin.esa.int/mapServer/mapServer?VERSION=1.1.1&REQUEST=GetMap&SRS=EPSG:4326"
+ "&BBOX="
+ (Number(nLat) - 30) + ","
+ (Number(nLong) - 15) + ","
+ (Number(nLat) + 30) + ","
+ (Number(nLong) + 15)
+ "&WIDTH=" + nWidth
+ "&HEIGHT=" + nHeight
+ "&LAYERS=DEM_30sec&STYLES=&FORMAT=image%2Fpng&BGCOLOR=0xFFFFFF&TRANSPARENT=FALSE&EXCEPTIONS=application%2Fvnd.ogc.se_inimage&QUALITY=QUICKEST";
}
catch(e)
{
// some error handling
}
</body>
</method>
If you have a recent version of formsPlayer installed then you can run this demonstration directly. If you'd like to obtain the source files to edit locally, they are available at http://libxh-apps.googlecode.com/svn/trunk/tutorials/custom-controls/map/.