(this is a code heavy post, with assumed familiarity with javascript, haskell, and the heist templating library, as a warning)
The motivation of the code / method outlined in this post is rather simple: I would like to encode dynamic aspects of the websites I write in the markup, not in Javascript code (corollary to this: I don’t like having lots of code running in javascript, and prefer writing things declaratively to imperatively, preferably with the aid of a type system, but I do want the sites that I build to have dynamic behavior. This solution is an attempt to reconcile these two things).
This may seem impossible, but it actually turns out to be rather simple. The key is to identify a small set of dynamic primitives that should be available, with clearly defined markup that should result in predictable (preferably local, to make it easier to reason about / check correctness) effects.
Then, global event listeners will look for certain events that are triggered on the specified markup, and will perform the needed operation. This means that you will code the effect once in Javascript, which will effectively extend the dynamic abilities of your markup, anywhere where the small javascript primitives are available.
Once the necessary javascript has been described, built, and tested, using Heist I will build Splices (which are special html tags that generate html based on their attributes, children, etc) that will produce the markup that the javascript needs to recognize / carry out the behavior, while exposing a very simple interface to the user (this reduces the chance that the user, ie, me and others working with this code, could generate markup that would screw up the handlers). From the user’s perspective, these special tags ARE the dynamic behavior, and provide a reliable way of ensuring that the effects always work.
The reason for creating this was rather simple: For a project, the designer in my team wanted to have input fields that, when selected, popped up a box that presented options to select by clicking on them. This is the task that usually a select field would fulfill, but it did not fit with the interface, and it was something that had come up before (when we did settle on a plain old select field) so I set out to create a way of consistently creating this pattern. One such end-use case is shown here:
<box-field name="person-name" value="$(person-name-value)">
<people>
<box-option value="$(personId)">
<personName/>
</box-option>
</people>
</box-field>
The “box-field” and “box-option” are splices that are globally available. They are what encode the idea of this Box Field (a field that presents options in a box to pick between). name is what the field’s name will be when the form is submitted, value is the prefilled value (here dynamic). The “people” splice is what holds the data that is populating the options (and it presents the subSplices “personId” and “personName”) - don’t worry about that for now, as it just as easily could have been a static list of box-options, like:
<box-field name="person-name" value="$(field-name-value)">
<box-option value="1">
Jane
</box-option>
<box-option value="2">
John
</box-option>
<!-- ...etc -->
</box-field>
Since this element might be used in different places, and on fragments of pages returned asynchronously, I did not want to have to make sure I was always attaching the proper handlers, every time the element was created. Having to always attach handlers also runs counter to the idea of encoding the behavior in the HTML - I want declarative dynamic behavior, not imperative.
So after looking a little, I figured what I needed was a way to register a global event listener that would listen for a specific event, match a certain element, and then run a corresponding function on it. I have used the lightweight standalone javascript libraries by @dedfat (of twitter), because they are simple and work well for me, but this could easily be adapted to use jQuery, etc. The function I came up with is “declare” - which takes an event, a selector, a boolean as to whether it should stop propagation (I think the answer will always be yes, but wasnt sure), and then the function to call when this occurs, passing in the element.
/*!
* declarative.js - copyright @dbp 2011
* BSD3 License
*/
function declare(event, selector, nopropagate, fun) {
// bean is an cross-browser standalone event handler
// at https://github.com/fat/bean
bean.add(document.documentElement, event, function(e) {
e = e || window.event;
var elem = e.target || e.srcElement;
// qwery is a cross-browser selector engine
// at https://github.com/ded/qwery
if (!elem || qwery(selector).indexOf(elem) === -1) {
return;
}
fun(elem);
if (nopropagate) {
e.stopPropagation();
}
});
}
Then I came up with the following HTML that I wanted to govern my box-field, the idea being that the hidden input field would contain the person’s ID when it is selected, and that the display field is what will show the name that is selected (styling done inline here, just to keep it self contained):
<div class="box-field ">
<input type="hidden" name="person-name" value="">
<div class="display"
style="width: 200px; height:20px; border: 1px solid black;">
</div>
<div class="box" style="display:none;">
<div data-box-value="1" class="option ">
Jane
</div>
<div data-box-value="2" class="option ">
John
</div>
</div>
</div>
Then I wrote up the handlers to make this box-field actually work:
bean.add(document, 'DOMContentLoaded', function () {
declare("click",".box-field .display",true,function (elem) {
// bonzo is a cross-browser selector engine
// at https://github.com/ded/bonzo
bonzo(bonzo(elem).next()).show(); // show the box
});
declare("click",".box-field .box .option",true,function (elem) {
bonzo(elem.parentNode).hide();
d = bonzo(elem.parentNode).previous()[0];
d.innerHTML = elem.innerHTML;
bonzo(bonzo(d).previous()[0]).attr("value",
elem.getAttribute("data-box-value"));
});
});
Finally, the splices that turn the markup shown at the beginning of this post into the html that the javascript actually operates on:
import qualified Text.XmlHtml as X
import qualified Data.Text as T
import Text.Templating.Heist
boxField :: Monad m => Splice m
boxField = do node <- getParamNode
case X.getAttribute "name" node of
Nothing -> return [] -- without name, useless
Just name -> do
let klass = T.concat
["box-field ",
(fromMaybe "" $ X.getAttribute "class" node)]
let value = fromMaybe "" $
X.getAttribute "value" node
let children =
[ X.Element "input"
[("type","hidden"),
("name",name),
("value",value)] []
, X.Element "div"
[("class","display"),
("style",
"width: 200px; height:20px; border: 1px solid black;")] []
, X.Element "div"
[("class","box"),
("style","display:none;")]
(X.elementChildren node)
]
return [X.setAttribute "class" klass $
X.Element "div" (filter ((/= "name").fst) $
X.elementAttrs node) children]
boxOption :: Monad m => Splice m
boxOption = do node <- getParamNode
case X.getAttribute "value" node of
Nothing -> return []
Just value -> do
let klass = T.concat
["option ",
(fromMaybe "" $ X.getAttribute "class" node)]
let attributes =
("class", klass) : (filter
((flip notElem ["name","class"]).fst) $
X.elementAttrs node)
return [X.setAttribute "data-box-value" value $
X.Element "div" attributes
(X.elementChildren node)]
There is obviously a bit of styling that still needs to be done to make the box look like it should, but everything from here on out (and any number of uses of this box) should be outside of the world of javascript - which was my intention in the first place!
PS. I just realized a small flaw in the example I presented. The splice “box-field” should also have a “display” attribute that has predefined what the field should show (to match up with the predefined “value”). This is easy to do, and should not detract from the presentation.