Simon Willison - http://simonwillison.net/
Partial notes made in preparation for a 45 minute talk at XTech in Amsterdam, May 16th 2006. Entry on my blog.
I'm going to be talking today about the Yahoo! User Interface Library, Yahoo!'s open source collection of JavaScript components, developed for internal use but now made available to the world under a liberal BSD license.
The library was developed by Yahoo!'s client-side platform engineering team in the US; the project lead was Thomas Sha. Thomas also drove the efforts to release the code under a BSD license.
The library itself can be divided in to two logical sections: utilities, and controls. Utilities are reusable infrastructure libraries that handle things like events, Ajax calls, animation and drag and drop. Controls are reusable interactive components - things like sliders, menus and calendars, built using the utility libraries. I guess you could call them widgets, but that part of the Yahoo! corporate namespace has already been accounted for by the Yahoo! Widget Engine.
I'm going to discuss the utilities in detail, and then give an overview of the controls. There's a lot of stuff in this library (well over 10,000 lines of code) and a detailed tutorial could fill the best part of a day, but since we have 45 minutes I'll try to stick to the highlights.
The dom component provides the foundation for the rest of the libraries. As with any JavaScript library, it defines its own abstraction for the venerable document.getElementById:
YAHOO.util.Dom.get(element or string or array)
The popular prototype library popularised the "dollar function" as a shortcut for this operation. The Yahoo library is good about keeping out of your global namespace, but one of the first things I do in my own code is set up my own dollar function like this:
var $ = YAHOO.util.Dom.get;
The next problem it solves relates to coordinate systems. When building advanced interfaces with JavaScript the ability to position things using a direct x, y coordinate system is extremely useful - especially for things like animation and drag-n-drop. IE and Gecko provide differing mechanisms for doing this, while Opera and Safari require you to do some recursive legwork over the parent elements.
YAHOO.util.Dom.getXY(el) does all that work for you.
The reverse operation is to move an element to a specific coordinate. getXY has a sister function called setXY which can do that, regardless of how the element is positioned on the page (static / relative / absolute).
Further shortcuts include getElementsByClassName which does exactly what you would expect, and getElementsBy which is more general, taking an acceptance function that is passed each node in turn and must return true or false:
YAHOO.util.Dom.getElementsBy(function(el) {
return (/^http:\/\/www\.yahoo\.com/.test(el.getAttribute('href')));
}));
Both getElementsByClassName and getElementsBy by default traverse every element in the DOM. This can lead to poor performance, so the functions take optional 2nd and 3rd arguments for specifying the type of elements to be checked and the root node to start checking from. Here's how you run the above query only against link elements within a div of ID content:
YAHOO.util.Dom.getElementsBy(function(el) {
return (/^http:\/\/www\.yahoo\.com/.test(el.getAttribute('href')));
}, 'a', 'content'));
Speaking of classes, the dom utility provides tools for easily adding, removing and replacing classes:
hasClass(el, className)
addClass(el, className)
removeClass(el, className)
replaceClass(el, className)
In addition to modifying the CSS classes applied to an element, the styles of the element can be change directly:
setStyle(el, property)
getStyle(el, property)
Both of these methods include workarounds for getting the CSS 3 opacity property to work cross-browser. The getStyle method does a bunch of work to figure out the runtime style of an element - a very useful ability!
Each of the dom methods described so far can take either an element reference, an element ID or an array of the same. If an array is provided, the function will return an array of corresponding results.
Other methods:
Finally, the dom library includes Region and Point classes for representing square regions and single pixel points on the page.
region = YAHOO.util.Region.getRegion(el) - returns a region representing the space occupied by the element
region.contains(otherRegion)region.intersect(otherRegion) - returns region representing intersection or nullregion.union(otherRegion)
These are used by the dragdrop utility, described later.
Event handling is the most notoriously area of browser incompatibilities. IE 4 and Netscape 4 were both released before the W3C standards covering events had been written, and came up with dramatically different models. The effects of these differences persist to today.
On top of that, event handling is a frequent target for browser bugs. Safari has a number of obscure bugs in this area, and Internet Explorer's memory leak problem is frequently tripped by event handling code.
The event component tackles these problems head-on, abstracting away many of the differences across browsers and cleaning up memory leaks. It also provides support for custom events, enabling easy support for the listener pattern in JavaScript applications. It includes support for legacy events, and knows when to fall back to them.
Basic usage of the event library looks like this.
function myCallback(e) {
alert('Something was clicked');
}
YAHOO.util.Event.addListener(el, 'click', myCallback);
You can combine the above using an anonymous function:
YAHOO.util.Event.addListener(el, 'click', function(e) {
alert('Something was clicked');
});
YAHOO.util.Event.on is an alias for addListener; we'll use that from now on.
You can also pass the ID of the element:
YAHOO.util.Event.on('mylink', 'click', function(e) {
alert('Element was clicked!');
});
The callback function is executed in the scope of the element the event was added to, so 'this' refers to the original element:
YAHOO.util.Event.on('mydiv', 'click', function(e) {
this.style.backgroundColor = 'red';
});
You can pass an object (or number or whatever) as the third argument. It will be passed to your event handling function when it is called.
function msgAlert(e, msg) {
alert(msg);
}
YAHOO.util.Event.on('mydiv', 'click', msgAlert, "My div was clicked");
You can also optionally override the object used as the scope, if you want 'this' to refer to something else. To do that, pass the new scope object as the fourth argument and 'true' as the fifth argument:
YAHOO.util.Event.on('mydiv', 'click', myCallback, scopeObject, true);
A common problem with applying unobtrusive JavaScript (where scripts are treated like stylesheets and kept completely separate from the rest of the page) is that it involves adding event handlers from code, but you can't add an event handler to an element until that element has been loaded in to the DOM. The standard solution is to have event handlers added in a piece of code that is itself called when the window.onload event fires. If a page takes a while to load (due to large images for example) UI elements won't respond as they should until the load has completed.
The event component lets you assign event handlers by ID before the element is available:
YAHOO.util.Event.on('mydiv', 'click', myCallback);
If 'mydiv' is not yet available in the DOM, a delayed listener is automatically created. This polls the DOM until the element becomes available or 10 seconds after the window.onload event fires.
You can also add delayed listeners yourself:
YAHOO.util.Event.onAvailable('mydiv', function() {
alert('mydiv has become available');
});
As with addListener, you can pass in an optional argument to be passed to your callback, and an optional flag to use that argument as the execution scope.
The first parameter passed to the event handling function is the browser event object. The event library provides a number of utility functions for cross-browser event manipulation:
YAHOO.util.Event.getCharCode(ev)
YAHOO.util.Event.getPageX(ev)
YAHOO.util.Event.getPageY(ev)
YAHOO.util.Event.getXY(ev)
YAHOO.util.Event.getTarget(ev)
YAHOO.util.Event.getRelatedTarget(ev)
YAHOO.util.Event.stopPropagation(ev)
YAHOO.util.Event.preventDefault(ev)
YAHOO.util.Event.stopEvent(ev)
YAHOO.util.Event.getTime(ev)
Custom events can also be created. These can then have other code subscribe to them, just like real browser events.
var myEvent = new YAHOO.util.CustomEvent('myEvent');
You can then subscribe from other parts of the code like this:
myEvent.subscribe(function() {
alert('event fired');
});
And here's how you fire the custom event itself:
myEvent.fire();
That callback function will be passed the event description string ('myEvent' here) as the first argument. Arguments passed to the fire method will be passed as an array as the second argument. The third argument can be set by passing in something when you first subscribe the event. This provides a great deal of flexibility in passing data around the event system.
These days you can't have a JavaScript Library without some kind of Ajax handling, and connection is the component for providing exactly that. It abstracts away the differences between various browser implementations, and includes a clever defence against potential memory leaks in Internet Explorer where the readyState of the XMLHttpRequest object is polled rather than assigning a potentially leaky event handler.
Here's the code for making a call:
YAHOO.util.Connect.asyncRequest('GET', '/ajaxy-goodness', {
success: function(o) {
alert(o.responseText);
},
failure: function(o) {
alert('Request failed: ' + o.statusText);
}
});
As you can see, you pass in an object containing success and failure callback functions. The connection object can also handle scope correction, for example if you want to call methods on a JavaScript object:
YAHOO.util.Connect.asyncRequest('GET', '/ajaxy-goodness', {
success: myObject.onSuccess,
failure: myObject.onFailure,
scope: myObject
});
Another useful trick is the ability to include an extra argument to the callback. This argument will be made available as a field of the object passed to the callback functions:
function onSuccess(o) {
alert(o.argument);
}
YAHOO.util.Connect.asyncRequest('GET', '/ajaxy-goodness', {
success: onSuccess,
argument: 'some extra data'
});
The animation utility benefits from some truly inspired API design by Matt Sweeney. Here's how to animate an element to resize itself to 400 by 400 pixels (no matter what it's current size is) over a one second period:
var anim = new YAHOO.util.Anim(el, {
width: {to: 400},
height: {to: 400}
}, 1);
anim.animate();
Let's have it fade in at the same time:
var anim = new YAHOO.util.Anim(el, {
opacity: {from: 0, to: 1}
width: {to: 400},
height: {to: 400}
}, 1);
You can increase by a specified amount rather than setting a finish goal:
var anim = new YAHOO.util.Anim(el, {
width: {by: 100}
}, 1);
Default units are pixels, but you can use other units as well:
var anim = new YAHOO.util.Anim(el, {
width: {from: 1, to: 10, unit: 'em'}
}, 1);
This animation mini-language can be applied to any CSS property that takes a numerical argument. It's surprisingly powerful.
While you can animate movement of absolutely positioned elements by animating their top and left properties directly, the YAHOO.util.Motion class gives you more flexibility:
var anim = new YAHOO.util.Motion(el, {
to: [100, 100]
}, 1);
anim.animate();
Even better, if you understand the principles behind bezier curves you can add bezier control points for smoothly curved movements.
var anim = new YAHOO.util.Motion(el, {
to: [100, 100],
control: [[50, 50], [150, 150]]
}, 1);
All animations can have an 'easing' function specified as the fourth argument. Easing functions can be used to cause the animation to speed up or slow down towards the beginning and end.
var anim = new YAHOO.util.Motion(el, {
to: [100, 100]
}, 1, YAHOO.util.Easing.easeOut);
anim.animate();
Since animations take time to complete, the ability to subscribe to custom events is supported (using the CustomEvent library). Available events are onStart, onComplete and onTween (which fires for every frame).
var anim = new YAHOO.util.Anim(el, {
opacity: {to: 0}
} 1, YAHOO.util.Easing.easeOut);
anim.onComplete.subscribe(function() {
var el = this.getEl();
el.parentNode.removeChild(el);
});
anim.animate();
One of the neatest parts of the animation system is also one of the most subtle. As the animation progresses, an animation manager object keeps track of how much time has elapsed. If performance is poor (there are many things being animated at once, for example) the framerate is dynamically reduced to ensure that everything completes in the time specified.
Good drag and drop is a surprisingly hard problem. The actual mechanics of getting an element to move with the mouse are pretty simple, but that's only 80% of the solution. Getting the other 20% is remarkable hard work.
Thankfully, Adam Moore has done all of that work for you. The dragdrop utility incorporates a bunch of theory from the masterminds behind the Yahoo! Design Pattern library and effectively covers the subtleties of drag and drop.
For example: when should a drag start? The answer isn't as obvious as you would think. You can't start a drag when the user hits the mouse button while over your object, because that might just be the start of a regular click or double click. What if they click and move? If they only moved a pixel, it might just have been an accidental move during a regular click.
dragdrop solves this with two settings: click pixel threshold (how far the mouse has to move during a click for a drag to start) and click time threshold (how long you have to hold the mouse button without moving to start a drag). These have sensible defaults.
My written notes end here. The content of the rest of the talk (including code examples of dragdrop and more) can be partially derived from the slides.