A List Apart

Menu
Issue № 194

JavaScript Triggers

by Published in JavaScript · 140 Comments

Warning: Experimental/controversial content. Proceed with caution.

The front end of a website consists of three layers. XHTML forms the structural layer, which contains structural, semantic markup and the content of the site. To this layer you can add a presentation layer (CSS) and a behavior layer (JavaScript) to make your website more beautiful and user-friendly. These three layers should remain strictly separate. For instance, it should be possible to rewrite the entire presentation layer without touching either the structural or the behavior layer.

Despite this strict separation, the presentation and behavior layers need instructions from the structural layer. They must know where to add that touch of style, when to initiate that smooth bit of behavior. They need triggers.

CSS triggers are well known. The class and id attributes allow you to fully control the presentation of your websites. Although it is possible to work without these triggers, by putting the instructions in inline style attributes, this method of coding is deprecated. If you want to redefine your site’s presentation while using them you’re forced to change the XHTML structural layer, too; their presence violates the separation of presentation and structure.

JavaScript triggers

The behavior layer should function in exactly the same way. We should separate behavior and structure by discarding inline event handlers like <code>. Instead, as with CSS, we should use triggers to tell the script where to deploy the behavior.

The simplest JavaScript trigger is the id attribute:

<div id="navigation">
<ul>
<li><a href="#">Link 1</a></li>
<li><a href="#">Link 2</a></li>
<li><a href="#">Link 3</a></li>
</ul>
</div>

var x = document.getElementById('navigation');
if (!x) return;
var y = x.getElementsByTagName('a');
for (var i=0;i<y.length;i++)  y<i>.

Now the script is triggered by the presence or absence of the id=“navigation”. If it’s absent nothing happens (if (!x) return), but if it’s present all the link elements that descend from it get a mouseover behavior. This solution is simple and elegant, and it works in all browsers. If ids serve your needs, you don’t have to read the rest of this article.

Advanced triggers

Unfortunately there are situations where you can’t use id as a trigger:

  1. An id can be used only once in a document, and sometimes you want to add the same behavior to several (groups of) elements.
  2. Occasionally a script needs more information than just “deploy behavior here.”

Let’s take form scripts as an example of both problems. It would be useful to add form validation triggers to the XHTML, for instance something that says “this field is required.” If we’d use such a trigger we’d get a simple script like the one below.

function validateForm()
{
 var x = document.forms[0].elements;
 for (var i=0;i[this field is required] && !x<i>.value)
    // notify user of error
 }
}

But how do we create an XHTML trigger that tells the script a certain field is required? Using an id isn’t an option: we need a solution that works on an unlimited amount of form fields. It would be possible to use the class attribute to trigger the behavior:

<input name="name" class="required" />if (<strong>x<i>.className == 'required' && !x[ i ].value)
  // notify user of error

However, the class attribute’s proper use is defining CSS triggers. Combining CSS and JavaScript triggers is not impossible, but it can quickly lead to a confused jumble of code:

<input name="name" class="largefield required" />if (
  <strong>x<i>.className.indexOf('required') != -1 &&
  !x<i>.value
)

In my opinion, the class attribute should only be used for CSS. Classes are XHTML’s primary triggers for the presentation layer, and making them carry behavior information, too, confuses the issue. Triggering both layers from the class attribute violates the separation of behavior and presentation, though in the end this remains an issue you have to take your own decision on.

Information-carrying triggers

Besides, triggers can grow to be more complicated than just a “deploy behavior here” command. Sometimes you’ll want to add a value to the trigger. A trigger value would make the behavior layer much more versatile, since it can now respond to each XHTML element’s individual requirements instead of mindlessly executing a standard script.

Take a form in which some textareas have a maximum length for their value. The old MAXLENGTH attribute doesn’t work on textareas, so we have to write a script. In addition, not all textareas in the form have the same maximum length, making it necessary to store the maximum length of each individual textarea somewhere.

We want something like this:

var x = document.getElementsByTagName('textarea');
for (var i=0;i[this textarea has a maximum length])
  x<i>.onkeypress = checkLength;
}function checkLength()
{
 var max = <strong>[read out maximum length];
 if (this.value.length > max)
  // notify user of error
}

The script needs two bits of information:

  1. Does this textarea have a maximum length? This is the general trigger that alerts the script that some behavior is coming up.
  2. What is the maximum length? This is the value the script needs to properly check user input.

And it is here that the class-based solution doesn’t really serve any more. Technically it’s still possible, but the necessary code becomes too complicated. Take a textarea with a CSS class “large” that is required and has a maximum length of 300 characters:

<textarea
  class="large required maxlength=300"
>
</textarea>

Not only does this example mix presentation and two separate sets of behavior, it also becomes tricky to read out the actual maximum length of the textarea:

var max = <strong>this.className.substring(
  this.className.indexOf('maxlength')+10
);
if (this.value.length > max)
 // notify user of error

Note that this bit of code works only when we put the maxlength trigger last in the class value. If we want to allow a maxlength trigger anywhere in the class value (because we want to add another trigger with a value, for instance) the code becomes even more complicated.

The problem

So this is our problem for today. How do we add good JavaScript triggers that allow us to pass both a general alert (“deploy behavior here”) and an element-specific value to the script?

Technically, adding this information to the class attribute is possible, but is it allowed to use this attribute for carrying information it was not designed to carry? Does this violate the separation of behavior and presentation? Even if you feel there is no theoretical obstacle, it remains a complicated solution that requires complicated JavaScript code.

It is also possible to add the trigger to other existing attributes like lang or dir, but here, again, you’d use these attributes to carry information they aren’t designed for.

Custom attributes

I opt for another solution. Let’s take a second look at the textarea maxlength example. We need two bits of information:

  1. Does this textarea have a maximum length?
  2. What is the maximum length?

The natural, semantic way to express this information is to add an attribute to the textarea:

<textarea
  class="large" maxlength="300"
>
</textarea>

The presence of the maxlength attribute alerts the script to check user input in this textarea, and it can find the maximum length of this specific textarea in the value of the attribute. As long as we’re at it we can port the “required” trigger to a custom attribute, too. required=“true”, for instance, though any value will do because this trigger just gives a general alert and doesn’t carry extra information.

<textarea
  class="large" maxlength="300" required="true"
>
</textarea>

Technically there’s no problem. The W3C DOM getAttribute() method allows us to read out any attribute from any tag. Only Opera up to version 7.54 doesn’t allow us to read out existing attributes (like src) on the wrong tag (like <h2>). Fortunately later versions of this browser support getAttribute() fully.

So this is my solution:

function validateForm()
{
 var x = document.forms[0].elements;
 for (var i=0;ix<i>.getAttribute('required') && !x<i>.value)
    // notify user of error
 }
}var x = document.getElementsByTagName('textarea');
for (var i=0;ix<i>.getAttribute('maxlength'))
  x<i>.onkeypress = checkLength;
}function checkLength()
{
 var max = <strong>this.getAttribute('maxlength');
 if (this.value.length > max)
  // notify user of error
}

In my opinion this solution is easy to implement and consistent with the form JavaScript triggers may take: a name/value pair where the presence of the name triggers the behavior and the value gives the script extra information, allowing you to customize the behavior for each individual element. Finally, adding these triggers to the XHTML would be extremely simple even for novice webmasters.

Custom DTDs

Anyone implementing this solution and running the resulting page through the validator will immediately note a problem. The validator protests against the presence of the required and maxlength attributes. It is of course completely correct: the first attribute is not a part of XHTML, while the second one is only valid on <input> elements.

The solution is to make these attributes valid; to create a custom Document Type Definition (DTD) that extends XHTML a bit to include our trigger attributes. This custom DTD defines our special attributes and their proper place in the document, and the validator obeys by checking the document structure against our special flavor of XHTML. If the DTD says the attributes are valid, they’re valid.

If you don’t know how to create a custom DTD, read J. David Eisenberg’s aptly named Creating Custom DTDs in this issue, in which he explains everything you need to know.

In my opinion, using custom attributes to trigger the behavior layer — and writing custom DTDs to define these custom attributes correctly — will help to separate behavior and structure and to write simple, efficient scripts. In addition, once the attributes have been defined and the scripts have been written even the newbiest of webmasters will be able to add these triggers to the XHTML document.

140 Reader Comments

Load Comments