Is invalid code created through JavaScript ok?
When developing web sites with heavy interactivity, your scripting skills are really put to the test. And, sooner or later, you will be put in a situation where it’s a fine line between following web standards and what’s best from a performance and structure perspective. One question that follows that is: is it ok to apply invalid attributes via script to elements?
Generally, I’ve had the stance that code should be valid, no matter if it’s in the initial code when the web page is loaded, or if it’s dynamically altered through JavaScript. By doing some DOM manipulation, you can create great interactivity for web sites which really helps the end user and improves the experience. I’m glad that most scripting is of that kind (or is moving in that direction, at least), as opposed to doing things just because they’re technically possible.
When invalid feels good
So, let me give you a very basic example of invalid code created through JavaScript that I find more structured and efficient than the valid equivalent. Let’s say that you have one or several links in your web page that will show/hide certain elements. First, let’s find all those links and apply the event (example code is written using DOMAssistant):
// ---
$(window).addEvent("load", findExpandCollapseLinks);
// ---
function findExpandCollapseLinks(){
var links = document.getElementsByClassName("expand-collapse", "a");
for(var i=0; i<links.length; i++){
/*
Note: always apply events to elements, where it
would work fine without JavaScript enabled
*/
links[i].addEvent("click", expandCollapse);
}
}
// ---
Now we have applied the events to all links. Let’s examine different ways to achieve the showing/hiding of them:
Option 1: Look up related elements each time
Let’s say that the function executed each time the link is clicked first gets some elements through a getElementsByClassname
function (try The Ultimate getElementsByClassName or DOMAssistant‘s core file for that), and loops through them.
// ---
function expandCollapse(evt){
var parent = this.parentNode();
var elementsToExpand = parent.getElementsByClassName("expanded", "div");
var elementsToCollapse = parent.getElementsByClassName("collapsed", "div");
// Loop through the elements, etc...
}
// ---
- Upside
- Little code overall, and easy to understand.
- Downside
- Unnecessary performance hit finding all the elements each time.
Option 2: Global variables
Another option is for each link to save its references to its elements into global variables once the click events are applied to them.
// ---
function findExpandCollapseLinks(){
var links = document.getElementsByClassName("expand-collapse", "a");
var linkElm;
var elmId;
var parent;
for(var i=0; i<links.length; i++){
/*
Note: always apply events to elements, where it
would work fine without JavaScript enabled
*/
linkElm = links[i];
elmId = linkElm.getAttribute("id");
parent = linkElm.parentNode();
window[elmId + "-" + elementsToExpand] = parent.getElementsByClassName("expanded", "div");
window[elmId + "-" + elementsToCollapse] = parent.getElementsByClassName("collapsed", "div");
linkElm.addEvent("click", expandCollapse);
}
}
// ---
function expandCollapse(evt){
var elmId = this.getAttribute("id");
var elementsToExpand = window[elmId + "-" + elementsToExpand];
var elementsToCollapse = window[elmId + "-" + elementsToCollapse];
// Loop through the elements, etc...
}
// ---
- Upside
- Little code in the
expandCollapse
function. - Downside
- Global variables are seldom good coding practice, because the risk of having them being overwritten. No direct reference between the element and the actual related elements exist either. Also, personally, I don’t think it’s that suitable for scalability.
Option 3: Object property
One way to do it is to save all the elements in an object’s property as an array, as soon as the click events are applied to them (an index class is used for each link, to avoid looping through the property array and comparing objects each time).
// ---
var linkObj = {
links : []
}
// ---
function findExpandCollapseLinks(){
var links = document.getElementsByClassName("expand-collapse", "a");
var linkElm;
var parent;
var elementsToExpand;
var elementsToCollapse;
for(var i=0; i<links.length; i++){
/*
Note: always apply events to elements, where it
would work fine without JavaScript enabled
*/
linkElm = links[i];
linkElm.addClass(("link-" + i));
parent = linkElm.parentNode();
elementsToExpand = parent.getElementsByClassName("expanded", "div");
elementsToCollapse = parent.getElementsByClassName("collapsed", "div");
linkObj.links.push([linkElm, elementsToExpand, elementsToCollapse]);
linkElm.addEvent("click", expandCollapse);
}
}
// ---
function expandCollapse(evt){
var linkIndex = this.className.replace(/.*link-(\d+).*/i, "$1");
var arrayPosition = linkObj[linkIndex];
if(arrayPosition){
var elementsToExpand = arrayPosition[arrayPosition][1];
var elementsToCollapse = arrayPosition[arrayPosition][2];
}
// Loop through the elements, etc...
}
// ---
- Upside
- Code hidden away in an object dedicated for the task.
- Downside
- Indexing links, and getting no direct reference between the element and the actual related elements, except through an external object.
Option 4: Invalid code
Let’s instead create two invalid attributes when applying the click event to the links that keep a direct and cached reference from the link direct to the elements.
// ---
$(window).addEvent("load", findExpandCollapseLinks);
// ---
function findExpandCollapseLinks(){
var links = document.getElementsByClassName("expand-collapse", "a");
for(var i=0; i<links.length; i++){
/*
Note: always apply events to elements, where it
would work fine without JavaScript enabled
*/
linkElm = links[i];
parent = linkElm.parentNode();
linkElm.elementsToExpand = parent.getElementsByClassName("expanded", "div");
linkElm.elementsToCollapse = parent.getElementsByClassName("collapsed", "div");
linkElm.addEvent("click", expandCollapse);
}
}
// ---
function expandCollapse(evt){
var elementsToExpand = this.elementsToExpand;
var elementsToCollapse = this.elementsToCollapse;
// Loop through the elements, etc...
}
// ---
- Upside
- Little code in the
expandCollapse
function. Good overview and direct reference between the link and the elements it will interact with. - Downside
- Invalid code.
Summary
In my opinion, the code in Option 4 seems to be the best for performance reasons, while at the same time establishing a direct relation between the link itself and its associated elements that will be affected. It is invalid, but only in the DOM and not from a general SEO or accessibility perspective.
Which option would you choose? And generally, is invalid code in the DOM ok if it’s justified from a performance or structure angle?
[…] JavaScript. By doing some DOM manipulation, you can create great … Original post by Robert Nyman and plugin by Elliott Back […]
There's nothing invalid about option 4. There is a difference between HTML attributes, which are strictly defined, and properties of the DOM node, which are not strictly defined.
Let's look at the three standards concerned: HTML, DOM (Core and HTML) and ECMAScript.
From the viewpoint of HTML, adding an attribute that is not specified is an error.
From the viewpoint of ECMAScript, an object is completely dynamic, and you can add whatever properties to that you want.
From the viewpoint of the DOM, there are some predefined properties that map directly to HTML attributes. Setting these properties also sets the corresponding attributes, and if the attribute changes through any other means, the property changes as well. However, the DOM makes no statement at all about properties that it doesn't define, which means that JavaScript should fall back to simple ECMAScript behaviour.
In other words, setting an arbitrary property of a DOM node does not mean that you set any attribute of the element. In particular, calling linkElm.getAttribute("elementsToExpand") in your code above should return null.
No setting of element attributes -> no possible violation of the HTML standard -> no invalid code. q.e.d.
The only exception is Internet Explorer, which doesn't understand the difference between element attributes and node properties. But worrying about code doing invalid things when processed under invalid rules is stupid.
On a side note, you have a lot of stray semicolons in your code.
See the way I look at it this is about extending a Javascript Object that just happens to be a DOM Node too. I'd go for your last option every time because it makes sense from a Javascript point of view. It has to be said I hadn't thought about the fact your new attributes aren't valid because if you were do look at the generated code they wouldn't display (being invalid).
I'm not explaining myself very well I guess but basically while I don't like new random attributes being added to HTML pages I see no problem in extending any arbitrary Javascript Object as required.
I would, and have been going with option 4.
My view is that I'm not touching the parts DOM that affect the rendered output of the page, or the HTML that's used to validate the page. I'm simply extending the DOM.
Similarly, browsers extend the DOM in their own way. If you take a look at any INPUT element using Firebug and Firefox 2 and you'll see the 'spellcheck' property attached.
Sorry – no, why should it? (It's yet one of the reasons why I dissuade from using sIFR.)
What others have already said: option 4 does not create invalid HTML. Had you used setAttribute(), however… ;).
Hmm, for the most part, i try to avoid relying on the DOM to do my bidding, preferring objects to package stuff.
<code>
myobject = {
myItems = [];
}
</code>
This does add more code, but allows me to keep cached references to nodes and collections, and wraps most of my functionality in a way that doesn't usually class with third-party code (or even code I write myself).
Inspired by the work of Christian Heilman (http://icant.co.uk/sandbox/eventdelegation/) , I've done some work on a recent project, using event delegates instead of event listeners. This works very well, especially for generated / ajaxed content, where you don't really want to clutter it up with inline event listeners, and we all know what a pain IE can be, when trying to manipulate DOM with content inserted with innerHTML (which is another MS innovation, that's off the plantation, but very handy).
Ironically enough, MS created innerHTML and yet IE has the poorest implementation.
So, I guess my approach goes kindof between the options outlined, since I try to keep references to ancestor elements, and have them deal with the events, instead of repeating the same event listeners over and over and over again. This scales really well, and Chris, if you're listening, GREAT WORK!
You really inspired me (see Mommy, I am not harsh all the time π )
As other people have pointed out already there is nothing 'invalid' about it. But if you were really concerned you could add the attributes into a custom DTD and reference it in your doctype.
Ok so it won't be valid XHTML 1.0 Strict, it'll be valid something else. But hey valid is valid π
Hi Robert,
Sebastian gava a correct reply, so I won't repeat that. On the other hand, I do firmly agree with your viewpoint that option 4 is the best practice. Altough I tend to use option 3 regulary too. But I can't say I have a fixed rule for this: it's more a decision based on what feels good depending on the situation.
About the semicolons: copy/paste can be a real bitch in cases like this, no ;)? Easely missed, and I've done far worse before…
I always use option 4. You are adding an object to the DOM element, not an attribute, therefore it is not invalid.
Wow, great responses!
Sebastian,
That's actually spot on. If we look beyond IE's implementation, it's two completely different actions. Very well put.
And, I'm nor sure if I have a seeing problem, but I can't find the stray semicolons. Would you be nice and give me a hint? π
Jens,
Well, to me, it's about structure and performance. Valid code should never hinder us, but help us write good code. If a solution is the best, but invalid, I'd go for it any day.
Morgan,
Oh, definitely. It depends on the context, and object packaging is very often a very nice and good way to go.
Ok, my old tired eyes got some help, and the wrong semicolons should have been removed. Please let me know if I missed some… π
Well, to me it's a question of compliance. I tend to follow the standards in first hand but in some cases other methods seem more feasible (I'm not necessarily speaking of HTML here). Depending on the task, i tend to take different actions. Say, inserting content from a remote webpage, .innerHTML is the way to go. When adding elements that's to be manipulated later, I'll use the DOM construction.
Watch out for illegal DOM manipulations though. From personal experience, invalid code can sometimes behave unexpectedly.
I once tried to append TRs to a TABLE unsuccessfully -because a TR only belongs in a THEAD, TFOOT or TBODY.
Just my 2c…
Andy,
Definitely, it's all about context.
Goulven,
Most definitely, if manipulating the DOM by adding new elements, make sure it's always properly structured.
I never use write(), innerHTML (even though that’s “OK” now) or hide invalid code in javascript.
Why write valid code? It’s easier to debug, cheaper, blah blah blah…… I’m not doing it for ideals, I’m doing it because I find my document easier to manage if it remains valid. I don’t hide invalid code in javascript.
I think that one of the reasons for invalid things to be hidden in javascript is the way people see documents such as web pages. You learn to edit it using a text editor, and you first learn to manipulate it with string manipulation functions.
People think: “HTML documents are strings.”
This is only superficially true!
In fact: HTML documents are serializations of objects – not strings.
When I saw the light, my skill doubled overnight. I’m still amateur and my work is still low quality and slow in appearing, but seeing things as they are does help me to improve.