IAmTheRockstar

Yes. Yes I am.
YUI and You: How Ubuntu One Built Their Webapps
August 18 2011

Note: this post was meant to be all-encompassing. After writing and proofing it, it's clear that this subject will take a few posts to really be descriptive. This post accidentally became a crash-course in making YUI widgets, which is still helpful though, so I'm posting it anyway.

Ubuntu One's website uses YUI 3 for its interactions. The site provides webapps for interacting with the data you've synced to the service from your desktop/mobile phone/various web services/etc. YUI 3.4 was released today, and it has an App framework in it. When I joined the Ubuntu One team in November, I didn't have that luxury.

So I endeavored to build my own...

In each case, I had a pretty self contained application already. They were synchronous, but they were self contained. I started out by creating a Manager widget that would handle all the interactions, etc. The YUI widget provides most of what I needed by default, so I'll start there and leave the more complicated stuff for followup posts.

var Manager = Y.Base.create(
    'manager', Y.Widget, [], {}, {});
new Manager().render('.main');

This creates a Manager object that inherits from Y.Widget. I've given it a name of 'manager'. This helps YUI assign the class 'yui3-manager' to the DOM node that Manager will attach to (called a srcNode). I specify this node when I call .render() on a new Manager object. Now, the first DOM element with the class 'main' is the node that the Manager has attached to. jslint might complain about that second line, since it doesn't like us using 'new' as a command, but we shouldn't need the reference to Manager (it's self contained, right), so just ignore jslint (just this one time). Rendering the Manager doesn't do anything you'd notice without inspecting the DOM, but that's okay. We'll have to teach it about the DOM and what we want it to do as we go.

YUI Widgets go through three different phases when .render() is called: render, bind, and sync. The render phase is for making changes to the DOM. The bind phase is for hooking up event listeners to the DOM. The sync phase is for changing Widget state based on items in the DOM (data that you probably generated and rendered server-side). You can hook into these phase with the following member attributes: renderUI, bindUI, and syncUI.

Let's start out by implementing the render phase and adding a class to all A links, so we can tell that the Manager has loaded.

var Manager = Y.Base.create('manager', Y.Widget, [], {
    renderUI: function() {
        var srcNode = this.get('srcNode');
        srcNode.all('a').addClass('javascript');
    }
}, {});

What we've done here is implemented a .renderUI() function that will be called when .render() is called. The first thing we do is get the widget's srcNode, which was specified by selector when we call .render(). YUI uses getters and setters A LOT (sometimes to a fault). There are lots of clever little things we can do because we're using getters/setters, but we won't go into that now. Basically, understand that srcNode is now the Node that matches '.main' (the first argument to .render()). I then query the node for all A links, and add a class called 'javascript' to all of them. My CSS might define that all a.javascript links are a different color than other links, or have different effects. The sky is the limit; go with what you feel.

There's a much easier way to run DOM queries on the srcNode, however. Widgets are built with progressive enhancement in mind, and so there is a clever way to define things that might already be in the DOM that you want to have access to. We should change our code to look like this:

var Manager = Y.Base.create('manager', Y.Widget, [], {
    renderUI: function() {
        var links = this.get('links');
        links.addClass('javascript');
    }
}, {
    ATTRS: {
        links: { value: [] }
    },
    HTML_PARSER: {
        links: ['a']
    }
});

What we've done here is created two static attributes called ATTRS and HTML_PARSER. ATTRS is where you define attributes for your widget (extra things you can get with <Object>.get() and set with <Object>.set()), along with default values and getter/setter functions themselves. In this case, we default it to an empty list. We then use the HTML_PARSER attribute to specify a selector with which to populate the attribute we specified in ATTRS. In this case, we want a list of all the nodes matching selector 'a'. Now, when we get to the .renderUI() call, we can request the attribute called "links" and the operate on the entire NodeList.

So if we add this to our DOM, we can see the classes for the links change (and apply the CSS you've decided to apply there). This is boring though. How can we start hooking event handlers up to the DOM? That's the real interesting part, amirite?

Let's say our DOM has a link with id 'delete' that should remove a div with id 'item' in it. How would we teach our Manager object how to do that? Like this:

var Manager = Y.Base.create('manager', Y.Widget, [], {
    renderUI: function() {
        var links = this.get('links');
        links.addClass('javascript');
    },
    bindUI: function() {
        var delete = this.get('delete');
        delete.on('click', this.onClickDelete, this);
    },
    onClickDelete: function(e) {
        e.preventDefault();
        var item = this.get('item');
        item.remove();
    }
}, {
    ATTRS: {
        links: { value: [] },
        delete: { value: null },
        item: { value: null }
    },
    HTML_PARSER: {
        links: ['a'],
        delete: '#delete',
        item: '#item'
    }
});

Oh wow. There's a lot of stuff going on here. Let's break it up into a few different pieces. The first thing I did was add the delete and item attributes to the Manager, with the accompanying entries in HTML_PARSER. This makes it much easier to get to the items that we need to work with. The second thing I did was implement the .bindUI() function. Remember, that's the second phase of a widget rendering. In .bindUI(), I get the delete link, and then add a click event handler to it. I've specified this handler as a function called onClickDelete. You can name it whatever you want, but trust me when I say you should name it pretty clearly to show that it's a click event handler for delete. Otherwise, when you come back in 6 months, you'll have to keep scrolling up to .bindUI to see what functions are called when (not fun). The last argument to .on() is the context with which the function should execute. Basically, we're saying "anytime inside the function where we reference 'this', that object should be the object we pass in as the third argument".

The last thing we do is implement the click handler itself, in .onClickDelete(). The call to e.preventDefault() keeps the browser from executing your javascript and then doing its own default behavior. In this case, we're clicking a link, and the browser says "Oh, the link has an href! When we're done with this javascript function, we should navigate to the url in the href." We don't want that, so we call e.preventDefault() to keep the browser from doing that. Then we move on to the nitty gritty of the handler, i.e. getting the item div and deleting it.

The last thing we might want to do is grab data already in the DOM and make sure the widget is aware of that. Let's say that there is a span with the id "name", and its contents has the user's name in it. We might want to store that name for later. We'll just make these last changes here:

 var Manager = Y.Base.create('manager', Y.Widget, [], {
    renderUI: function() {
        var links = this.get('links');
        links.addClass('javascript');
    },
    bindUI: function() {
        var delete = this.get('delete');
        delete.on('click', this.onClickDelete, this);
    },
    syncUI: function() {
        var nameNode = this.get('nameNode');
        this.set('name', nameNode.getContent());
    },
    onClickDelete: function(e) {
        e.preventDefault();
        var item = this.get('item');
        item.remove();
    }
}, {
    ATTRS: {
        links: { value: [] },
        delete: { value: null },
        item: { value: null },
        name: { value: '' },
        nameNode: { value: null }
    },
    HTML_PARSER: {
        links: ['a'],
        delete: '#delete',
        item: '#item',
        nameNode: '#name'
    }
});

So once again we start by adding some attributes to Manager. We add the name attribute, which we default to an empty string. Then we add a nameNode attribute, and specify it's selector in HTML_PARSER. In .syncUI(), we then get the nameNode, and set the Manager's name attribute to the content of nameNode.

The actual Manager widgets on Ubuntu One are a bit more complicated than this, but this should provide a basic understanding of how we've done it so far. Recently, I've learned that this approach doesn't scale very well with the sorts of complicated things we're doing now (and I will blog about that), but it does cover most of the things we have done up until this point.

All opinions expressed here constitute my personal opinion, and do not necessarily represent the opinion of any other organization or person, including, but not limited to, my fellow employees, my employer, its clients or their agents.