Matt Andrews
Software engineer making apps – that aren’t apps – and more at the FT. 会说汉语.
DOM Event Delegation without jQuery

When building the FT’s and Economist’s HTML5 apps we felt that as we were targeting only the latest browsers shipping the entirety of jQuery would be a bit – well – wasteful. What we wanted were small focused components that could be swapped in and out that we could pull in (initially) via npm, bower or (later) our build service. This thinking has since spread to the rest of the FT, who are now also moving away from jQuery.

So what is jQuery?

According the documentation, it’s quite a lot of things – as a very crude measure, its API docs have 593 articles. I don’t think I’m unusual in thinking that the vast majority of that I’ve never used and probably would never use.

For me what has made jQuery so helpful are its wrappers that make Ajax, DOM manipulation, DOM transversal and listening to events simple to do.

Since the ‘invention’ of jQuery the browser has completely changed. Whilst we sometimes need to maintain backwards compatibility, for example our stubborn friends in China who just refuse to let go of IE6, the browser now provides a huge amount of what jQuery gave us natively. (Specifically I’m thinking of things like querySelectorAll, classList, the new array methods)

Also after using TJ Holowaychuk’s SuperAgent I can’t look at jQuery’s Ajax API without seeing its idiosyncrasies. (Why is type not method!?)

What to do about event delegation?

But there was a piece of jQuery we needed that was missing in the component world and that is a nice library to help with event delegation (read more about JavaScript event delegation and why you might use this pattern on Site Point). So we built one and called it FT DOM Delegate (or dom-delegate on the npm or bower registries).

Without the baggage of an old API that everyone already knows we were able to start from scratch. So this is what we did:-

You decide which DOM element to listen to events on

1
2
var bodyListener = new Delegate(document.body);
var targetedListener = new Delegate(document.getElementById('my-el'));

Rather than listening to all events on the same place (usually document.body) FT DOM Delegate allows you to create more focused DOM Delegates that only listen to events within a specific element. This is really helpful for creating self-contained widgets or in single page applications like our’s where we dynamically load pages without refreshing the page (where each page might require a different set of event listeners).

Delegates can be killed

1
targetedListener.destroy();

Just one call to destroy and all events will be unbound, event listeners removed. On single page apps with views being rapidly created and destroyed – this is essential to prevent memory leaks.

We actually went a step further to make delegates recyclable. Via the delegate’s root method you can trivially attach and detach delegates to DOM nodes. This is useful as it allows you to completely re-render the pages’ HTML in javascript without having to re-attach all the event listeners individually.

1
2
3
4
5
6
7
8
<body>
<section id="pane-1">
<button>Click me</button>
</section>
<section id="pane-2">
<button>No, Click me</button>
</section>
</body>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var pane1 = document.getElementById('pane-1');
var pane2 = document.getElementById('pane-1');

var dd = new Delegate();
dd.on('click', 'button', function() {
console.log("button clicked");
});

dd.root(pane1);
// Clicking 'Click me' => console.log
// Clicking 'No, click me' => nothing

dd.root(pane2);
// Clicking 'Click me' => nothing
// Clicking 'No, click me' => console.log

Delegates can be created without the DOM

Because Delegates can be detached from the DOM we realised that we didn’t actually need any DOM at all to be able to set up event listeners.

You can set up a delegate’s event listeners whenever you like, and when you are ready to actually start receiving those events, simply attach the delegate:-

1
2
3
4
5
6
7
8
var dd = new Delegate();
dd.on('click', '.close', function() {
closeOverlay();
});

// ** some time later **
var overlay = document.getElementById('overlay');
dd.root(overlay);

Use capture for pros

nother area we felt was missing from other event libraries was that whilst they were extremely helpful in basic cases – because of the need to support legacy IE they didn’t give you access to decide whether you wanted your event listeners to be capturing or not. (For more detail on how DOM events and useCapture work read my former colleague Wilson Page’s article on Smashing Mag).

Basically all events start at the document body then step through the DOM until they hit the element where the event (for example a click) was triggered. Then, if the event can bubble, it reverses back through the DOM until it hits the document body again. (Not all events bubble – for example error and blur events)

As the event moves from the document body towards the target element it is said to be in its capturing phase, when it reaches the target it is at target and is in its bubbling phase when it reverses back up through the document.

Sometimes when you are adding listeners you will want to specify which stage of the event flow you are interested in. Our DOM Delegate library allows you to do this via its fourth parameter:-

1
2
3
4
5
6
delegate.on('click', '.js-btn', function() {
console.log("Caught event during capturing phase!");
}, true);
delegate.on('click', '.js-btn', function() {
console.log("Caught event during bubbling phase!");
}, false);

Sensible defaults for capture phases

Some events don’t bubble – e.g. error, blur, focus, scroll, and resize – so for these (unless you specify otherwise) we set useCapture to be true by default.

This is handy as we like to handle all image load failures by hiding them, which we can do with just a few lines of code:-

1
2
3
4
var dd = new Delegate(document.body);
dd.on('error', 'img', function() {
this.style.display = 'none';
});

Events for the future

With this library we believe we’ve made a really nice and absolutely tiny event delegation library that gives you as much power as the browser native methods – with some helpful methods that allow to you to easily tidy up after yourself. And we’re one step closer to kicking our jQuery addiction.

And, of course, it’s open source: https://github.com/ftlabs/ftdomdelegate.

Playing with Channel messaging

Whilst building some recent experiments with ServiceWorkers I’ve discovered a whole new API that I never knew existed: Channel messaging. Paper-clipped onto the end of the HTML5 Web Messaging specification, Channel messaging enables:

…independent pieces of code (e.g. running in different browsing contexts) to communicate directly
http://www.w3.org/TR/webmessaging/#channel-messaging

As far as I can see, they’re basically JavaScript wormholes between different tabs and windows.

How do they work?

te a new channel you call the MessageChannel constructor in the normal way:

1
var wormhole = new MessageChannel();

The wormhole has two portals, which are wormhole.port1 and wormhole.port2 and to send objects between one and the other you can postMessage the data on the sending port and listen to message message events on the receiving port.

One small complexity is that you won’t be able to listen to any of the incoming messages until start has been called on the receiving port.

Note: any data sent before the port has been opened will be lost – and there’s no way to interrogate the MessageChannel to find out whether a port is open or not.

Also note: as postMessage is asynchronous you can actually swap the wormhole.port2.start() and wormhole.port1.postMessage('HELLO'); around and it will still work.

1
2
3
4
5
6
var wormhole = new MessageChannel();
wormhole.port2.addEventListener('message', function(event) {
console.log('port2 received:'+event.data);
});
wormhole.port2.start();
wormhole.port1.postMessage('HELLO');

See this for yourself on JSBin

It’s no fun to talk to yourself

Let’s now see if we can use a Shared Worker to wire two browser windows up with each other and see what we are able to send, window to window, tab to tab. The full code is up on GitHub and you can try it out there.

For this we’ll need two files: index.html and agent.js.

/agent.js

1
2
3
4
5
6
7
8
9
10
11
var mc;
onconnect = function(e) {
var port = e.ports[0];
if (mc) {
port.postMessage({ port: mc.port2 }, [m c.port2 ]);
mc = undefined;
} else {
mc = new MessageChannel();
port.postMessage({ port: mc.port1 }, [ mc.port1 ]);
}
};

This is the SharedWorker. Every odd browser window that connects to it (ie. the 1st, 3rd, 5th, etc), it creates a new MessageChannel and passes one of the ports of that MessageChannel object to that browser window. It will also keep hold of a reference to the most recently created MessageChannel so that it can give the other port of it to the ‘even’ connecting browser windows (the 2nd, 3rd, 4th, …).

This allows the SharedWorker to hook up the browser windows, after which it can simply get out of the way – allowing the browser windows to talk to each other directly.

/index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE HTML>
<title>MessageChannel Demo</title>
<pre id="log">Log:</pre>
<script>
var worker = new SharedWorker('agent.js');
var log = document.getElementById('log');
worker.port.onmessage = function(e) {
window.portal = e.data.port;
window.portal.start();
window.portal.addEventListener('message', function(e) {
log.innerText += '\n'+ (typeof e.data) + ' : ' + e.data;
});
}
</script>

<button onclick="window.portal.postMessage('hi');">Send 'hi'</button>
<button onclick="var now = new Date();window.portal.postMessage(now);">Send a date object</button>
<button onclick="var node = document.createElement('div');window.portal.postMessage(node);">Send a dom node</button>

This code will connect to the SharedWorker, wait for the SharedWorker to send it one of the ports of the MessageChannel (which the SharedWorker will create) and when it gets one, it will start listening to message events and print out the data it receives onto the web page.

I’ve also added some buttons so that it’s easy to test sending bits of data between the two browser windows. (Remember, you need to have two browser windows open for this to work)

Uncaught DataCloneError: Failed to execute ‘postMessage’ on ‘MessagePort’: An object could not be cloned.

Not every kind of JavaScript object can be sent in this way (which is why DOM nodes fail). According to the specification:

Posts a message to the given window. Messages can be structured objects, e.g. nested objects and arrays, can contain JavaScript values (strings, numbers, Dates, etc), and can contain certain data objects such as File Blob, FileList, and ArrayBuffer objects.
http://www.w3.org/TR/2012/WD-webmessaging-20120313/#posting-messages

Beware of embedding tweets in full screen single page apps

Using components built by other people is fundamental to the success of any piece of technology. The more high quality physical and virtual components you can pull together, the less you need to build from scratch and the faster you can build things. We’ve been sharing and reusing code since the beginning of the web – and almost every web company that I can think of offers some way to embed their content on your site.

That’s all fine until you find the component does something that you don’t expect it to. For example, if the creator of the component made an assumption that is not true for your application, instead of saving you time it can cause problems for your application or the component itself. This happened to us when we tried to embed Tweets in the FT Web App.

This is an embedded Tweet:

This is an embedded Tweet:

One of the features the JavaScript Twitter use for embedded tweets on external websites has is that if you click reply or retweet instead of taking your user away from your website to Twitter, it will helpfully open a new, smaller window in which the user can use to post Tweets from, like this:

The problem is that the way this is implemented is that it doesn’t just affect the behaviour for links within the <blockquote class="twitter-tweet"> elements, it will listen to clicks on all links anywhere anywhere on your web page – and if the link is to a URL containing twitter.com/intent it will open a small new window.

To see this behaviour click here.

Interestingly it’ll also match links to others domains, as long as they contain the pattern twitter.com/intent/. Eg. http://mattandre.ws/twitter.com/intent/tweet. Play around with this on JSBin.

After a bit of digging, hidden in the minified code Twitter encourage you to use, are these few lines that are responsible for this:-

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function m(e) {
var t, r, i, s;
e = e || window.event, t = e.target || e.srcElement;
if (e.altKey || e.metaKey || e.shiftKey) return;
while (t) {
if (~n.indexOf(["A", "AREA"], t.nodeName))
break;
t = t.parentNode
}
t && t.href && (r = t.href.match(o), r && (s = v(t.href), s = s.replace(/^http[:]/, "https:"), s = s.replace(/^\/\//, "https://"), g(s, t), e.returnValue = !1, e.preventDefault && e.preventDefault()))
}

[...]

var o = /twitter\.com(\:\d{2,4})?\/intent\/(\w+)/, u = "scrollbars=yes,resizable=yes,toolbar=no,location=yes", a = 550, f = 520, l = screen.height, c = screen.width, h;
b.prototype = new t, n.aug(b.prototype, {render: function(e) {
return h = this, window.__twitterIntentHandler || (document.addEventListener ? document.addEventListener("click", m, !1) : document.attachEvent && document.attachEvent("onclick", m), window.__twitterIntentHandler = !0), s.fulfill(document.body)
}}), b.open = g, e(b)

For most ordinary websites this behaviour wouldn’t be surprising – and probably even desired.

But our site ain’t no ordinary website. It’s one of those modern new fangled offline-first single page apps called the FT Web app.

Most of our users use our application full screen after it has been added to their (typically) iOS home screen. The problem is that we need to be in complete control (within javascript) of what happens when the user clicks any link because the default behaviour is fairly ugly (the application will suddenly close and the link will be opened in Safari). In order to make that experience a little less awful, in order to support external links like we first show the user a popup warning them that they’re about to leave the app like this:-

I’d be the first to admit that this isn’t exactly the pinnacle of user experience – it reminds me of the Microsoft Office paperclip helpfully double checking that you’re absolutely “sure you wanna exit?” but it’s the best we can do for now.

When we tried to start using Twitter’s embedded Tweet functionality we found that the code we’d carefully crafted to stop web links from inadvertently closing our full screen web app was being completely bypassed. In the end decided not to use Twitter’s javascript library.

It’s a little bit unfair that I’ve singled out Twitter, especially as they do provide the raw CSS to style Tweets without the JavaScript that does all the weird stuff. In fact we’ve ended up shunning lots of different libraries for similar reasons (eg. jQuery and numerous advertising libraries) and every now and again one of our advertisers creates an advert that breaks critical features of our web application, which never fails to create a little excitement in the office. For being so adverse to externally written code, we’ve gained something of a reputation internally.

The fundamental problem is that unless you use an iframe to embed content (like YouTube does) – which causes numerous other problems for our web app so we don’t support either :( – the web is not encapsulated. If you add a 3rd party library to your web page, that library can do what it wants to your page and, short of just removing it, there isn’t always much you can do about it if it does do something you don’t agree with.

Guidance

If you’re building websites in non-standard ways (full screen ‘web apps’; packaged/hybrid apps; single page apps and/or offline first apps) don’t automatically assume that because you’re using ‘web technologies’ you will be able to use every existing library that was built for the web. All libraries – even modern, well written ones like the one Twitter use for embedding Tweets – are built with certain assumptions that may not be true for your product.

In the future Web components (via Shadow DOM) will finally bring the encapsulation that the web needs that will help us address some of these problems.

Hopefully iOS will also make the way it handles links in full screen web apps a little better too.

HTML5 Offline Workshop this Autumn in Freiberg

I’m going to teaching a workshop on offline technologies in Freiberg at Smashing Conference this Autumn covering:-

  • A brief history of the offline web
  • Patterns for offline web applications
  • Cookies and Local Storage
  • IndexedDB and WebSQL
  • AppCache and ServiceWorker
  • Offline data sync strategies
  • Open-source libraries that can help us
  • Fallback techniques for older browsers, search-engine crawlers and users that do not need an offline experience

Come along, if you like :-)

Automate Sass Testing (with Travis CI)

View my demo project on GitHub

Having a large amount of CSS is unavoidable in a modern web application. Using preprocessors such as Sass help us manage that CSS but as we write more @mixin‘s, @function‘s and adopt techniques such as object orientated css the complexity grows. At FT Labs we even use (or perhaps abuse) npm as a package manager for Sass only repositories for various projects, including the FT Web app, so that those styles can be shared across projects.

With this ever increasing complexity, the differences between writing CSS and any other programming language are eroding.

All this complexity adds risk

In other programming languages we mitigate this kind of risk with automated testing. It’s time to start testing our Sass.

Testing Sass with Travis CI

Sass isn’t a language that Travis CI currently has first class support for but we can get it working with just a small number of hacks to the .travis.yml file.

Apart from some Sass (which I’m assuming you have already) you will need a .travis.yml file that looks something like this:

1
2
3
4
5
6
script:
- "test/travis.rb"

language: sass
before_install:
- gem install sass

View source file

Here I’m telling Travis to first install Sass then execute the file located at test/travis.rb.

If you use a task runner such as make, Rake, Grunt or another you’ll probably want to use it rather than a script like this but I wanted to keep things as simple and technology agnostic as possible.

Interestingly the language: option is actually optional and it even allows for invalid values – helpfully it will default to Ruby (the language Sass is written in). Optimistically I’ve set it to sass but it may be more robust to set this to ruby.

The test

The next step will be to tell Travis exactly what to build.

Here are the contents of my test/travis.rb script:

1
2
3
4
5
#!/usr/bin/env ruby
result = `sass main.scss built.css`
raise result unless $?.to_i == 0
raise "When compiled the module should output some CSS" unless File.exists?('built.css')
puts "Regular compile worked successfully"

View source file

I’m using back ticks rather than any of the other other ways to run shell commands in ruby so I can easily check the status code and output any errors thrown by Sass (which come through via stdout). I then check to see if the built files exists – and raise an error if it does not.

An error thrown at either step will stop the script executing and cause the build to fail.

Protection against bad PR

From now on any Pull Request that causes our Sass not to compile will come with a bright yellow red ‘The Travis CI build failed’ warning.

What can we actually test?

Compiling is a good first step but that offers little more than a linter) and will only catch the most basic of regressions. Here are some other ideas and examples of what could be tested: