Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

Info
titlePosted by Antranig Basman on 2008-12-12


In preparing for the final release of Fluid Infusion 0.6 (as of writing, we are at "Bug Parade" stage, leading to code freeze for Monday), we found ourselves with the disagreeable requirement to make some incompatible API changes to a part of the framework. Not only why we had to do this, but what we did about it, are interesting.

The code in question was kept in the file jquery-keyboard-a11y.js, and has historically always had a "special status". This holds the parts of our framework which we have always considered closest to being contributed to the wider jQuery community (who we are always eager to develop productive relationships with), as a plugin. This code is treated specially, in that special care is made to keep it as self-contained as possible, and in particular to keep it free of dependence on our standard framework base file Fluid.js, to ease its adoption outside our community.

Unfortunately, as of release 1.5, the jQuery UI project included a file ui.selectabe.js, which registers a plugin named "selectable" with the same name as one in our set. This is interesting, since it highlights what has been considered a weakness in the jQuery plugin model - that it does not support any infrastructure for namespacing, to ensure that loosely cooperating teams can continue to work in parallel without fear of treading on each other's names. This partly reflects its origins - the jQuery "chained filter" idiom is admirably matched for expressing queries and primitive operations on the DOM, and has produced a revolutionary condensation in the amount of code necessary to perform these "bread and butter" tasks. The plugin model is less well suited to an idiom of construction - that is, when one is using the DOM as a "springboard" for the construction of some more complex "component" or tree of objects that is going to spend some durable lifetime managing it. This can be seen from the oddity of some code which constructs say a dialog, or a sortable - where is the dialog in the following call?

 $("#dialog-root").dialog({bgiframe: true});

Any "object" actually referring to the dialog is implicit - the jQuery standard, is that operations on a jQuery object return another jQuery object (barring the exceptions that they are queries for primitive values such as attributes or counts) - therefore, to interact with this "dialog" we must continue to use same-named plugin calls targetted at the same DOM node (another interesting oddity is that the constructed object is not tied to the lifetime of the invoking jQuery object, but to the durability of the DOM nodes addressed by it - this is by necessity, since jQuery objects have no durability).

As a quick aside to those who have not been following these chain of blogs avidly, the Fluid framework design recommendation, since before the 0.5 release, was for "that-ism", a code structuring device originally recommended by Douglas Crockford. I made an earlier blog posting explaining our decision, and there is also a wiki page on How to define a Unit. In brief, the benefits of that-ism include greater reliability and predictability of code - in the months since our decision we have been very happy with it, with many contributors commenting that it has made our codebase easier to understand by reading. That-ism also interacts nicely with Javascript's natural "detached function" idiom - for example it is very easy to write a line such as

        that.viewEl.click(that.edit);

without having to worry about awkward "this" re-dispatching through event cycles etc - the edit method has natural access to its own, user-defined, and limited, context, without the need for "this juggling". The durability of the returned that from a constructor is also matched to the created object - since it is the created object.

In any case - the jQuery model is marvellous for encoding primitive DOM operations drawn from a fixed set, slightly odd for construction, but poor for namespacing - having lost our namespace for selectable our only option is to move out of the path of the elephant. However, there is no procedure how we can guarantee that further of our names will not collide in the future, so now is an excellent opportunity to think of a scheme to make sure that our users will never again suffer this kind of API incompatibility.

Also, over the lifetime of Fluid, jQuery have firmed up their recommendations and published the following page of Guidelines for plugins - which we could do better at meeting. In particular, they recommend that each thing designated a "plugin" use exactly one name in the plugin namespace, in an attempt to head off the namespacing issues I've mentioned.

So, this problem is an excellent opportunity. Can we, at the same time, improve our conformance with jQuery code idioms, solve our namespacing issues permanently, and at the same time improve jquery-keyboard-a11y.js in its agreement with the that-ism enjoyed by the rest of the Fluid codebase?

It sounds impossible, but actually we can. After a lengthy brainstorming session, during which we proved that a "natural" namespacing idiom (that is, one which allows function names to be written out as Javascript symbols, rather than as string) is impossible to apply together with a "this-ist" dispatching model, we came up with the concept of the "that-ist bridge". This is a small 20-line "machine" which now currently sits at the top of jquery-keyboard-a11y.js, and does the work of "exposing", not only the contents of that file, but in fact the entire Fluid framework (where it has potentially conformant arguments) as a single, gigantic jQuery UI plugin named fluid, with exactly the recommended jQuery UI argument semantics.

Here is an example of our old syntax, showing invocation as a jQuery UI plugin named selectable:

$(".my-nodes").selectable({direction: $.a11y.orientation.HORIZONTAL});

"On the ground", this has now been rewritten to a that-ist, Fluid-standard construction, used as follows:

fluid.selectable(".my-nodes", {direction: $.a11y.orientation.HORIZONTAL});

This is now housed in the global fluid namespace owned by us, permanently heading off the potential for collisions in the future. However, owing to the miracle of the "that-ist bridge" machine, this function is now also accessible, for free, using the jQuery UI plugin syntax as follows:

$(".my-nodes").fluid("selectable", {direction: $.a11y.orientation.HORIZONTAL});

Essentially any function, which accepts a "jQueryable" as its first argument (that is, anything which can legally construct a jQuery object when used as an argument to $, that is, selector strings, DOM nodes, or jQuery objects themselves) will be automatically adapted by the "that-ist Bridge" to be invocable using the single, global, Fluid jQuery UI plugin. The arguments are permuted - the first argument to the function becomes the jQuery object itself which the plugin is invoked on, and the symbol for the name of the function becomes a string, the first argument passed to the plugin. The remaining arguments to the function may be passed in an array as the 2nd plugin argument, or if there is a single argument it can be sent directly.

The treatment of return values is also somewhat interesting. The jQuery standard is that jQuery operations return further jQueries - except where they don't. It seems that the general exceptions are those which return primitives - so this rule has been formalised in the bridge code. The return value of the underlying Fluid function is examined for its type. If it is a primitive, or "falsy" value, it is return directly. If it is some other kind of value, it is assumed that it is some kind of "Fluidic that", and the value is hidden - instead, the original jQuery this is returned, to allow the standard jQuery selector chaining idiom to work. However, as a courtesy, the specific jQuery object returned is decorated with an extra method that() by which the originally returned value can be retrieved.

This has been a big win all round - since at the same time we have been able to improve conformance both with our own coding standards, as well as jQuery's. And further, we have now been able to extend to the contents of this file the protection of our version management strategy, first brought in in Fluid 0.5 - by means of a namespacing trick, multiple versions of the Fluid framework may be included into the same document, whilst at the same time not only not colliding in namespace, but even remaining simultaneously addressible from the same scope by specially aware code. Truly necessity is the mother of invention. Or else, a loaf of bread.