Share via


Enforcing Unobtrusive JavaScript

Unobtrusive JavaScript is a concept that’s been around for quite a while, but it’s now finally starting to make its way into the mainstream. The basic idea is that you should separate your JavaScript from your HTML – so instead of;

<a href="Default.aspx" onclick="alert('Sorry, that is unavailable'); return false;" >Click Here</a>

You should have something like this;

<a id="ClickHereLink" href="Default.aspx">Click Here</a>

... with some matching JavaScript in another file somewhere;

function pageLoad() {

    var link = $get('ClickHereLink');

    $addHandler(link, 'click', preventClick);

}

function preventClick() {

    alert('Sorry, that is unavailable');

    return false;

}

I’m not here to tell you why this is so great, but the best discussion and walkthrough of issues I’ve found is this article on onlinetools.org, so check it out if you’re interested.

How do I enforce it?

Recently I found myself cursing some code I’d written a long time ago, when I found it littered with onclick and onload handlers. It was frankly a pain to work out how it works. It was at this point that I thought “I should write something to shout at me when I write don’t write unobtrusive JavaScript”.

Coincidentally I have also been doing some work on HTML parsing, and I have to say I’ve found the XPath-like features in the HTML Agility Pack to be incredibly useful. If you’ve never used it I’d recommend taking a look.

It occurred to me that this was the perfect tool to do what I wanted – so I set about creating a simple MSBuild task that uses the HTML Agility Pack to scan ASPX and ASCX files for Obtrusive JavaScript – basically a form of static code analysis. If you’re interested in customising MSBuild, check this out.

The implementation is very simple;

· It is derived from Microsoft.Build.Utilities.Task.

· There is a property that is an array of ITaskItems, flagged with a [Required] attribute, as a way to pass in a list of files to check.

· There’s a hard-coded (for now) list of validation XPath expressions, with matching explanations.

· In the Execute method override, it loops through all the files.

· For each file, it opens the HTML and applies every XPath to it.

· If there are any matches for the XPath, it creates a build failure.

The key to success here is the simplicity with which the HTML Agility Pack lets me use XPath to find elements. The following is the basics of my code;

HtmlDocument doc = new HtmlDocument();

doc.Load(path);

HtmlNodeCollection results =

    doc.DocumentNode.SelectNodes(

        validator.SearchXPath);

Easy huh? The XPath expressions themselves mostly look like this;

//*[@onkeypress]

This will match any HTML element that has an onkeypress attribute... which of course means that the developer has specified a JavaScript event handler in the HTML, and broken the rules of unobtrusivitiy (hey, if that’s not a word I’m having first dibs on inventing it J).

Remembering that we’re actually scanning ASP.NET mark-up, not pure HTML, we can also check for ASP.NET attributes that actually create JavaScript event handlers in HTML too;

//*[@onclientclick]

Again, this is not difficult. The tricky one appears when you realise that it is valid to have OnClick in ASP.NET code! Of course, an <asp:button> can have an OnClick attribute referring to the server-side event handler. I wondered about saying “match any with an onclick that don’t have a runat attribute”, but this isn’t quite accurate (HtmlControls can have runat=”server” too remember).

In the end I went for explicitly excluding the ASP.NET controls that I knew were OK to use OnClick;

//*[@onclick and not(

name()='asp:button' or name()='asp:bulletedlist' or

name()='asp:imagebutton' or name()='asp:imagemap' or

name()='asp:linkbutton')]

Finally, I wanted to check that there was no JavaScript inside the page HTML itself... even between <script> tags. In other words, the only valid <script> tags should reference an external file, as follows;

<script language="javascript" type="text/javascript" src="Default.js"></script>

[Of course, you could also use the <asp:ScriptManager> control if you wanted to; in fact I’d prefer it]

To achieve this I check that there is nothing other than whitespace between the start and end script tags;

//script[string-length(normalize-space(text()))>1]

... and that’s it! As usual, the source code for this MSBuild Task is attached to this post, along with a demonstration Web App.

Wiring up the Task

Now we have a simple MSBuild task, all that is left to do is add it to a build. I created a new ASP.NET Web Application, right clicked the project file and chose “unload”, and then edited the XML manually.

The only bit I needed to add was as follows;

<UsingTask

   AssemblyFile="..\JsBuildTasks.dll"

    TaskName="ValidateUnobtrusive" />

<Target Name="BeforeBuild">

   <ValidateUnobtrusive

        SourceFiles="@(Content)" />

</Target>

First I import my task (I’ve copied a Release build of the assembly to the parent directory above my Web App project). Next, I override the BeforeBuild target, and add an instance of my ValidateUnobtrusive task. I’ve passed in an expansion of all the Content files in the project. The task filters for *.aspx and *.ascx at the moment – if you wanted to add *.html that would be trivial.

Save the file, and right click it to choose Reload Project. At this point Visual Studio 2008 warns you about a customised MSBuild project file – just choose “Load Project Normally” and you’re away.

From this point on, whenever you build a project containing Obtrusive JavaScript you’ll see errors like these;

Error Use of the 'onclick' JavaScript event on 'a' tag in markup causes Obtrusive Javascript. You should attach this handler in script instead.

Error Failed on 'script' tag . A JavaScript region containing something other than white space was found. You should link to related JavaScript resources files rather than embed script in a page. Conclusion

I think this works really well so far (although I’m at the early stages of trialling it); if you try it do let me know how you get on. One of the particularly cool things (that I haven’t tried yet as I’m working isolated on my laptop most of the time right now, and I’m short of time) is that this could be wired up into a Continuous Integration build, under Team Build – I guess that would be a good plan if you didn’t like running the validation as part of your local build, or if it starts to perform badly due to the complexity or size of your pages.

I also think it raises other questions and possibilities. For example; how about some static analysis of “.js” files themselves? Could this help check for Directly Accessible Scripts, for example?

Originally posted by Simon Ince on 10 July 2009 here.

Comments

  • Anonymous
    December 21, 2009
    hmm.... I find that onclick event on button or link is much more clear than using attach event. Since you might attach multiple events to a button or link and might not realize till it's too late. A better approach would be to use lots of tools like IE tool bar, fire bug and visual studio debugging tools to debug java script. And I have observed that using Microsoft Ajax tool kit, jquery others will introduce unnecessary dependency and unexpected errors. But  Html Agility Pack looks like very useful tool. In my previous project I required a HTML parser and could not find a useful, finally had to use IE dom object to parse the html which is really a pain since you have to do a lot of looping casing and casting particularly if you write in C#.

  • Anonymous
    January 03, 2010
    Thanks for the comments – interesting thoughts and I definitely agree that tooling is essential to write and debug JavaScript effectively. I think the issue of clarity for events is one of convention – if you’re used to seeing event handlers on the button then you’ll look there, if you’re used to them being attached in code you’ll look there. I initially shared some of your concerns but having adopted this approach I now find it much much better to work with (e.g. all my handlers tend to be attached in logical places, instead of scattered through HTML). It also encourages me to write pages that work without script, and to “progressively enhance” them (check out the p&p guidance on this here; http://webclientguidance.codeplex.com/). As for using frameworks, your reservations are common – but I would very strongly recommend reconsidering; to write any serious volume of JavaScript nowadays that is compatible across current and future browsers means a “compatibility layer” of some form (that both jQuery and Microsoft Ajax include) is essential. Also, managing complex script dependencies, performing common scripting tasks, and componentising JavaScript code can hugely benefit from using a framework. If, on the other hand, you’re using very little JavaScript (and by this I mean in the order of 100 lines of script on your site in total, to pluck a figure from nowhere!) then you’re right that the trade off of using a library versus the download size of that library  might not make sense... but it could still make sense from a development effort point of view. I was really impressed by the HTML Agility pack too... I love things that you can just plug in and work with immediately. So useful! Hope that helps! Simon