Preamble: This write-up is inspired by the talk I gave at BackboneConf 2013.
For the last year, I worked as a frontend engineer at Coursera. We constantly found ourselves needing UI widgets to decorate our interfaces, like modals, popups, tooltips, uploaders. You know, the same widgets that 99% of websites need, plus a few niche widgets.
We wanted to use the same UI widget libraries both in the legacy global-variables-roam-free codebase and the shiny-requirified-backboney codebase. We started with what most developers start with, jQuery plugins, but then we ended up coming up with our own way of architecting UI plugins to meet our particular constraints and satisfy our particular desires.
On jQuery Plugins
Now, don't get me wrong; I'm forever grateful for jQuery and its plugins ecosystem. John Resig released jQuery in 2006 with a plugin mechanism from day 1, and the first third-party jQuery plugin came out only a few weeks later. A year later, the community launched the plugin repository, further encouraging developers to create and share their plugins. They also launched jQuery UI soon after, which gave developer more of an architecture and base for UI plugin development. jQuery plugins became the defacto standard for UI library development and thanks to jQuery encouraging developers to share their creations, there are now thousands of plugins for developers to pick from.
But, that doesn't mean that jQuery plugins are perfect. To begin with, many jQuery plugins lack an architecture or
vary wildly in their internal architecture. The original jQuery plugin "architecture" was
as simple as attaching a method to $.fn
and processing the passed in element. jQuery UI
eventually introduced
a generic widget factory with more of an architecture, and developers like Addy Osmani
wrotes articles on best practices for plugin design. But it's hard to know how many plugins actually follow those best practices, particularly the older ones, and from a purely anecdotal perspective, I've gone through the source of enough plugins to suspect that the majority do not. Now, the internal architecture wouldn't matter if plugins were just blackboxes that did what you wanted them to do. But, they're not - they are chunks of JS code that your code depends on, code that you will likely find yourself debugging and patching to better suit your needs. If they follow a set of best practices and standard architecture,then they're more likely to work well and be easier for developers to dive into.
Besides that, jQuery plugins are inherently dependent on jQuery. Yes, most websites use jQuery these days (including all of Coursera), but it'd be better if there was a standard way to write UI libraries that did not depend on any libraries, and the UI library would only bring the libraries in that it truly needed. Plus, some of the best plugins are dependent on jQuery UI, since it includes the widget factory and offers features like draggable/sortable, and jQuery UI is a heavy addition to an app that isn't otherwise using it. It's also often difficult to use the jQuery UI JS without the CSS and its subsequent look & feel.
Rethinking UI Libraries
After trying to write some custom Coursera UI widgets as jQuery plugins, we decided that it might be more useful for us to start over and figure out how to architect our UI libraries for everything that we wanted out of them. We wanted many of the things that developers want in jQuery plugins, like customization and events, but we also wanted zero dependencies, AMD-compatibility, designer-friendly declarative usage, and more. As an example, I'll step through the creation of a library that emulates <marquee> functionality, because, seriously, there are so many times in life that I need a marquee.
The Basic Marquee Library
We'll start off with the basic library. The library user will write this HTML:
<div id="marquee-me">Yo Wassssup!</div>
And write this JavaScript:
var marquee = new Marquee(document.getElementById("marquee-me"));
window.setTimeout(function() { marquee.stop(); }, 5000);
The code that makes it work is in
this JSBin. I've used standard OO JS to create a Marquee
object, setInterval
to start the
element moving via CSS positioning, and clearInterval
in the stop
method.
Hidden Private Functionality
As I learned from my many years on the Google Maps API, developers using a JS API *will* find every single possible un-documented method and use them in their production code, and they'll get mad once that method stops working. After that experience, my approach now with libraries and APIs is that you should try to keep everything private until absolutely necessary; until developers are groveling at your doorstep for the functionality and you feel confident enough that you can test and maintain that functionality. Hiding functionality isn't as crucial for internal libraries, but I still think it's a useful to make it clear which methods a library author originally intended to be used by the outside.
In the original code, library users could easily call marquee.moveItMoveIt()
,
a method that I intended to be private (I'd never expose such a silly name!), so I want to restrict that. In the new code, I've wrapped everything in a MarqueeModule
that includes a _private
object with the moveItMoveIt
method as a member as well as the Marquee
object which now only has the stop
method. Then MarqueeModule
returns the Marquee
object, and window.Marquee
is set to that return value. There's no way now for library authors to access _private
, since that's a local variable that was never exposed or returned.
Idempotent Constructor
We developers are forgetful. Okay, at least I am, and I want to make sure that if I call a constructor twice on the same element, I only really construct the UI component once. That way, I don't have to worry about checking everywhere that a UI component may have been constructed and I can just call it again for good measure. This is the fancy concept known as "idempotence".
In the previous code, if a library user called Marquee
twice on #marquee-me
and tried to stop()
it, the marquee would go on forever and ever, because it started two setInterval
's. Now, we wouldn't want that.
In the new code, I've added a private
getOrMakeMarquee
method that checks if there's already a Marquee
object associated with the element, returns that if so, and constructs a new one if not.
Customization and Defaults
When a developer can customize their usage of a UI library by just specifying a few options, it gives them a way to change the library without having to go into the code or write their own. That's why many popular libraries boast many options (just see the Select2 demo page for an example). Of course, more options means more to maintain and test, so we don't want to add options just because, we want to add them because we have some inkling of how usage will vary of the library.
For Marquee
, we might realize that library users will want to vary the distance and direction:
var marquee = new Marquee(document.getElementById("marquee-me"),
{direction: 'forwards', distance: 100});
They also might want to re-specify options later, and have the library pick up the new options:
var marquee2 = new Marquee(document.getElementById("marquee-me"),
{direction: 'backwards', distance: 1});
To support options in the new code,
I added a defaults
object at the top that clearly documents every option, its default value,
and its possible values. I then added the customizeMarquee
method that merges the passed in options
with the default values, and I call that both from the constructor and the getOrMakeMarquee
method.
Declarative Customization
The Coursera designers like to work in HTML and CSS to craft the interfaces, and they even have Github accounts and local servers running on their machines to make it possible for them to actually check out branches, tweak HTML, and send changes for review. They're not quite at the stage yet of JS, and given the non-linearity of our JS, it's not so easy to poke around for newbies, so the frontend engineers try to make it possible for them to edit as many things about the look and feel as possible in HTML and CSS alone. That means that when it comes to UI libraries, they should be able to customize the widgets without diving into the JS. And hey, as it turns out, developers like being able to customize in the HTML as well.
For example, for the Marquee
library, it should be possible to use data attributes to customize it like so:
<div id="marquee-me"
data-marquee-direction="backwards"
data-marquee-distance="5">
Yo Wassssup!
</div>
To support that in the new code, I modified the customizeMarquee
method to check the data attributes for each option, and use that if none were specified in the JS.
Declarative Construction
Why should developers and designers have to use JS to initialize a UI widget at all? We could just have them add a specific data attribute that indicates an element should be transformed, and as long as the page included the JavaScript, we'd transform all the matching elements.
For the Marquee
library, we could add an attribute like data-marquee
:
<div id="marquee-me"
data-marquee
data-marquee-direction="backwards"
data-marquee-distance="5">
Yo Wassssup!
</div>
To support that in the new code, I added a new method
called start
to the public object. That method takes in an element, searches for any elements
with the data-marquee
attribute, and calls getOrMakeMarquee
on them. We call that method on document.body
when the library is loaded, so it can pick up anything that exists already. If we're using this library in a Backbone app where we often construct the HTML after the fact, then we'll still need to call that
method on the view element once everything's rendered. Theoretically, we could use Mutation Observers to find out
about DOM changes that introduce new data-marquee elements, but that's a new and not widely supported browser feature.
Observable
Another way to let developers customize their use of a library is to let them specify callback functions
to happen at particular times in the lifecycle of a UI widget. Many libraries still do this via options, letting
developers specify things like an onClick
callback. That approach isn't the best, though, since it means
only one callback can be assigned, and it clutters up the options hash. The preferred approach is to let developers
add event listeners to the widget for whatever event they're interested in. Just like with options, events should be tested for and maintained, so we only want to fire those events that we are prepared to support.
In the Marquee
library, we might realize we want library users to find out when the marquee reverses direction, like so:
var marquee = new Marquee(document.getElementById('marquee-me'));
marquee.on('reverse', function() {
document.getElementById('marquee-me').innerHTML = marquee.direction;
});
To support that in the new code, I added an EventTarget
object (copied from this article).
and made the Marquee
object extend it. Then I can call this.fire('reverse')
at the appropriate time.
AMD-Compatible
At Coursera, there are some codebases that use RequireJS to manage dependencies, and some codebases that just shove everything into the global namespace and hope for the best. Not that I'm advocating for the latter, but hey, I bet there a lot of codebases out there that do that, especially smaller ones. We want our UI libraries to work equally well in each codebase, and not have to use separate copies for each.
In an AMD environment, we should be able to use Marquee
like so:
define("lib/marquee", function(Marquee) {
var marquee = new Marquee(document.getElementById('marquee-me'));
});
To support that in the new code, we added a conditional at
the end which checks if define
is defined
in the AMD way,
and if so, it wraps the module in define
. If not, then it attaches it to window.Marquee
like before.
Declared Dependencies
Most UI libraries will indeed depend on additional libraries - and that's understandable. The code in a UI library should be focused on the unique functionality of that library, not on re-inventing the code of libraries past. So we want a way to bring in those dependencies that works in both the AMD and non-AMD environments.
For the Marquee
library, I would likely bring in jQuery for my DOM manipulation and data attributes,
as my current code is nowhere near being cross-browser compatible. You can see that
in this code; all I do is change the define block at the end and pass the $ to MarqueeModule at the top.
I would also use LucidJS for the event emission, as it's a great little library that's easy to mix-in to an object in a non-obtrusive way. You can see that in this code.
Many of the Coursera UI libraries also bring in Underscore for data manipulation, extend
to process options, debounce
, and some bring in Q for promises.
Testable
As I've mentioned in many of the above points, we should have tests for all publicly exposed functionality, options, and events. In fact, we should probably test UI libraries even more than our normal application JS, because we will be using these libraries multiple times, and it is more likely that a developer from another part of the codebase will accidentally "abuse" library, using it in a way that the author never expected. If there are tests for the library, then it's easy to add regression tests for the new and interesting ways that the libraries gets used, or add a test that ensures it can't get used that way.
For the Marquee library, I wrote a small suite of 7 tests that check construction, options, declarative usage, and events. That suite is built on Mocha and Chai, but the tests could just as easily be written in QUnit, Buster, or Jasmine. The typical Coursera testing stack is Mocha and Chai with JSDom, so that the UI library tests can be quickly run outside of a browser environment as well.
I did run into a problem testing Marquee that I also ran into with Coursera libraries: now that we've hidden the private functionality, how can we test them? We don't usually care about calling them directly (though there are cases where testing that could be useful), but there are times when it would be useful if we could use Sinon to stub out private functions and simply verify that they were called during the execution of a public function. There are various posts like this one the discuss testing private functions in JS, and I don't think there's one technique that's clearly the best yet.
Documented
Developers should not have to read through a library's code to understand how to use it. Even if you've written the most beautifully usable code in the world (at this point, I will remind you how most parents think their babies are beautiful, when in fact they're screaming trolls). At the minimum, add a comment at the top which shows an example of using the library, both the HTML and the JS needed. If you want users of the library to appreciate you even more, add more examples of advanced configuration, write up an explanation of why the library exists and what future work remains, and point to examples of real world usage of the codebase. Remember, you want your colleague to look over at you with an adoring look on their face, not a begrudged glare.
For the Marquee
library, I added a readme with a short history
and usage examples.
Remember: when in doubt, document.
Real World Examples
Now that we've walked through the Marquee library together, I've shown you many of the design patterns that power the Coursera UI libraries. You might be wondering what these look like in a "real library", as you likely suspect that the Coursera designers never let me get away with Marquee-ing everything. You'd be right...so here are some actual Coursera UI libraries.
ReadMe
The ReadMe library displays a banner at the top of a page, which will be displayed until a particular date or until users close it a certain number of times. The Coursera frontend lead built this after constantly wanting banners like this to announce new functionality to students and admins, and it's now in heavy use.
To use this library, we'd write HTML like this - notice how we can stick
data-readme-close
on any element to tell the library that
clicking it should close the banner:
<div data-readme="watchlist-announcement" data-readme-show-count="1" data-readme-show-until-closed="data-readme-show-until-closed" data-readme-show-expires="Jun 15, 2013" class="hide readme">
We now give students the ability to "watch" classes they're interested in, which replaces the need for TBA sessions. <a href="https://class.coursera.org/mooc/forum/thread?thread_id=472" target="_blank" data-readme-close="data-readme-close">Read more here.</a>
<div data-readme-close="data-readme-close" class="readme-close-icon"><span class="icon-remove"></span></div>
</div>
Then we'd call ReadMe on the element:
new Readme(this.$('.readme'));
You can see the full code here.
Modals
Like any proper webapp, Coursera uses a lot of modals. The modals library was built as a replacement for the Bootstrap modals library (which didn't do enough) and the fancybox library (which did too much and was heavy), so it was designed to let developers use the Bootstrap CSS if desired, but not force it.
To use it with Bootstrap CSS, we'd write HTML like so:
<div data-modal-overlay-class="coursera-overlay-dark" class="modal coursera-course-selfstudy-modal hide">
<div class="modal-header"><h2>What is "self study"?</h2></div>
<div class="modal-body"><p>Self-Study bla bla bla....</p></div>
<div class="modal-footer"><button data-modal-close class="btn btn-primary">OK, I got it!</button></div>
</div>
We could then trigger it via an HTML anchor:
<a data-modal=".coursera-course-selfstudy-modal" role="button">?</a>
Or we could programmatically open it:
Modal(this.$('.coursera-course-self-study-modal')).open();
You can see the
full code here. The start
function is a bit more interesting in the Modals library,
because there should only ever be one Modal open at once, so it takes care of closing previous modals
and enforcing the singleton nature of this UI widget.
PopUps
The Popups library is similar to the Bootstrap popovers library, a UI component that pops up next to an anchor, and remains there until the user moves away. Coursera uses it for dropdown menus, hover cards, and more.
To use it, we'd write HTML like this for the anchor. Yes, it's a lot of HTML, and that's because we need our UI to be accessible, so we must add the appropriate ARIA attributes that signal both that the anchor is a button and that it is associated with an expanded menu.
<li class="course-topbar-nav-list-item"
tabindex="0" role="button" aria-haspopup="true"
aria-expanded="false" aria-owns="course-topbar-aboutus"
data-popup="#course-topbar-aboutus"
data-popup-bind-open="mouseenter" data-popup-direction="se">
<a>About <i class="icon-caret-down"></i></a>
</li>
We'd write this HTML for the actual popup content - no ARIA required here, as the library itself adds what's necessary. We wrote the ARIA roles manually in the anchor HTML, as we don't necessarily run the JS until the anchor is interacted with, and it needs to be accessible from the beginning.
<div id="course-topbar-aboutus" class="course-topbar-sublist">
<a class="course-topbar-sublist-item" href="/about/jobs">Jobs</a>
<a class="course-topbar-sublist-item" href="/about/team">Team</a>
</div>
You can see the full code here. A few things to notice: 1) it documents the accessibility requirements clearly, 2) it enforces that only one popup is open at once, like modals, 3) if the specified activation event is mouseenter but the current browser is a touch device, it uses click instead.
And Many More
Those are some of the most frequently used UI libraries at Coursera, but as you can imagine, there are many more: a custom A/B testing framework, a media uploader using Transloadit as a backend, a rich text area with Markdown and HTML support, tooltips, calendar date picker, affix, draggable, sortable, etc. They vary in how much they adhere to the design patterns I've laid out here, but going forward, the frontend lead tries to enforce them in reviews of new libraries and backport them to old libraries. It's important that new developers that join the engineering team are presented with a consistent architecture, because they'll naturally follow that architecture when building new libraries.
Wrapping It Up
You might be looking at everything I just showed and thinking, "wait, couldn't we do all that with jQuery plugins?" I bet that you could, and I bet there are jQuery plugins out there that do all of that and more. We just went down the route of starting from scratch to see what we'd end up with at the end, given our particular constraints and desires, and I'm sharing what we came up with. I encourage you to share your own best practices for UI library design in the comments, or try out some of the ideas here in your own projects and report back.
No comments:
Post a Comment