Showing posts with label jquery. Show all posts
Showing posts with label jquery. Show all posts

Friday, August 2, 2013

JavaScript UI Library Design

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.

Saturday, March 17, 2012

Porting jQuery Plugins to Zepto: Tips & Tricks

As I've written about it here in the past I've spent a fair amount of time porting jQuery plugins to work with Zepto, a lightweight alternative with a smaller API surface. You can read those posts here and here to see how I ported specific plugins over, but today I thought I'd give a general guide for people taking on the task of porting other plugins over. There are an approximate shit ton of jQuery plugins out there (it was basically the defacto way of creating Javascript UI widgets for the last few years), so I see a lot of porting in our future. Here's the strategy I employ:

  1. Start by setting up your page so you can easily switch between serving jQuery and serving Zepto, and modify the last line of the plugin code to work with either:
    })(window.jQuery || window.Zepto);
  2. Test your code with Zepto, and look for errors in the logs about undefined methods. That means the plugin is using a method that is not in your Zepto.js, and you need to either not use that method or more likely, find a way to define it in your Zepto copy:
    1. Do a CTRL+f on the Zepto docs to see if the method is described there. Sometimes it's defined in the optional files, and you just have to bring those into your copy (like the data() and animate() methods).
    2. Look up the method in the jQuery docs and read the description. Some jQuery methods are very similar to other methods, and can be safely substituted in some situations (like contents() and children().)
    3. Search the Zepto issue tracker to see if other developers have filed an issue about lacking functionality. If they have, they often include a patch or link to a gist which you can copy into your Zepto copy.
    4. Check the jQuery source code (either using this source browser or by CTRL+Fing in the uncompressed source code) to see how they accomplish the functionality, and bring that code into your Zepto copy. Just think carefully about how much of that code you actually need for the modern mobile browsers, and whether there are edge cases you can safely remove.
  3. Once you've gone through the undefined errors, you might think your plugin is good to go — but then you try to use it and it just doesn't do what you expect it to. Don't worry, there's still hope yet! Sometimes a plugin uses an existing jQuery method but uses it with optional arguments or alternative constructors, and that can lead to code behaving differently when run on jQuery versus Zepto. To see if that's happening, place strategic console.log statements around the code, run it with both jQuery and Zepto, and see where variable values differ. When you find a misbehaving area of the code, look carefully at the arguments that the plugin is sending into a method, compare that to what Zepto is accepting (both type and number). If you discover a discrepancy, follow the steps above to supplement your Zepto copy to handle the other method usage.
  4. Now that you've hopefully successfully ported a plugin, contribute it back to the community. At the very least, stick it in a gist with "Zepto + PLUGIN-NAME" and tweet it, or even better, write a blog post explaining the changes, and contribute patches to Zepto (of additions) and/or the plugin (of subtractions/simplifications). You may find that the Zepto authors don't want to bloat the library with that functionality, and you might find that a plugin author doesn't care about making their code play nicer with lighter weight jQuery alternatives, but it doesn't hurt to put your patches out there.

And if all else fails (or if porting is just taking too damn long) — find a library that does what you want, but doesn't rely on jQuery. Or write it yourself. Happy porting!

Using FancyBox with Zepto

On the web version of the EatDifferent site, I use the jQuery Fancybox plugin to lightbox meal photos when you click on them. On the mobile version, I left them as clickable images. I've realized that's not a great experience because the user feels like they're leaving the app (because they are!) and on the iPhone, they can't simply press the back button to go back to the app (like you can on Android). So, for the next version of the mobile app, I decided to use lightboxes for the photos there as well.

Then I had to decide how to lightbox them on mobile. I could use FancyBox, use Twitter Bootstrap modals (which I already use on mobile), or find a mobile-optimized lightbox solution. After doing a quick check in the browser to see how FancyBox works at small screen sizes — and seeing it actually works pretty well — I decided to go for it.

But, there's a catch: I use jQuery on the web, and the more lightweight Zepto on mobile, and very often jQuery plugins use obscure jQuery functionality that Zepto does not support, like optional arguments, alternative constructors, and little known methods. So when I want a jQuery plugin to work with Zepto, I either have to modify the plugin code to use the more "standard" functionality or I have to add the extra functionality to my Zepto copy. My preference is not to mess with plugin code, as that makes it hard to upgrade to new plugin versions and its usually difficult to figure out what exactly the plugin author intended. But, sometimes it's easier. In the case of FancyBox, I did a little of both. Here's a run-down:

FancyBox Changes:

You can check out the diff or the full fancybox.js.

  • As usual, I changed the last line of the plugin file to say window.jQuery || window.Zepto, to make it work with either.
  • The plugin used contents(), and I changed it to use children() instead, which seemed to work.
  • The code used a concept called global event triggers in jQuery which lets you trigger an event on no element in particular ($.event.trigger('fancybox-cleanup'). Zepto doesn't have that, so I changed the code to simply trigger on the two elements that listened to that global event.

Zepto Changes:

You can check out the diff or the my full zepto.js. My Zepto is fairly customized for my use (beyond just FancyBox), so you probably don't want to just grab it all.

  • The code used $.support to detect opacity support on browsers. Since I only use Zepto on mobile and it doesn't have the support module, I hard-coded support for it ($.support = {opacity: true};)
  • The code called the append() method with multiple arguments, which Zepto doesn't support out of the box. I found this gist that adds support for it and merged that into my copy.
  • The code used "is:visible" to detect DOM element visibility. Unfortunately, that pseudo-selector was completely made up by jQuery and it won't work in Zepto since it uses the browser's built-in query selector engine. There's a long discussion about how to best generally handle these fake selectors, but in the meantime, I just added a special check to my filter function to just handle is:visible, based on this gist.
  • The code used fadeTo(), fadeIn() and fadeIn(), which aren't in the core Zepto files but are available if you include the fx.js and fx_methods.js files.
  • Unfortunately, the code also used stop() to stop fading animations, which isn't in those optional Zepto files, so I then had to copy the files from this Zepto fork instead.

If this post was of interest to you, you may also be interested in reading my first post on porting most of my plugins to Zepto.

Tuesday, February 7, 2012

Datepicker for Twitter Bootstrap & Zepto/jQuery

When I ported my Phonegap code over to using Zepto instead of jQuery, I struggled the most with porting the jQuery tools DateInput plugin. I eventually got it to work, but man, it required a lot of little hacks and just didn't feel so great. I ran with it for a little bit, but then decided to see if there was a better alternative standalone datepicker, and I found this pull request to Twitter Bootstrap for a input-triggered datepicker. I took the code attached to the request and modified it to be standalone, and it totally worked, both with Zepto and jQuery. Here's what it looks like:

You can play with a demo of my modified standalone version here. That demo also shows using the date.format.js library for pretty printing the date. If you want to use it yourself, you can grab the JS/CSS from that demo, or the JS from this gist and the SaSS from this gist. Or if you don't need a standalone version, check out the demo for the original datepicker and grab his code from the pull request.

Hopefully the datepicker gets pulled into Bootstrap and becomes an official plugin. It's something that many websites need and the HTML5 input type="data" isn't giving us what we need yet, not even in the modern browsers.

Monday, December 5, 2011

Upgrading from jQuery Templates to jsRender

In making my recent port from jQuery to Zepto, I was the most worried about porting over from jQuery templates - I knew there were other templating engines out there (like mustache.js, which comes highly recommended) but I also know templating engines can be vastly different and I didn't want to spend a lot of time porting.

Fortunately, I found out that jQuery templates have actually been deprecated in favor of jsRender, a revision of the library that isn't dependent on jQuery. Perfect!

The templating language for jsRender is largely the same as jQuery templates, with a few purely aesthetic syntax changes and one logical difference — atleast from the perspective of the parts that I was using. The jsRender library is still in development, so the differences might change in the future, but in the meantime, I thought I'd write up a quick upgrade guide for anyone else porting over.

The operators are now prefixed with a "#". You can pretty much do a search and replace to port them over - {{if}} to {{#if}}, {{each}} to {{#each}}.

Previously, variables were outputted with ${var} - now, variables are outputted using the same double brackets as operators, like so: {{=var}} and {{=var.property}}. If you're using a server-side templating engine that also uses that bracket notation for templating, you will need to instruct the engine to ignore the jsRender templates. For Jinja2 templates, that means surrounding the script tags with {% raw %}{% endraw %}.

In addition, the syntax for specifying you don't want HTML escaping changed from {{html var}} to {{var!}} (notice the exclamation mark at the end). Finally, if you want to access the value of the currently referenced variable, the syntax changed from ${$value} to {{=$data}}.

To give you an idea of what an upgraded template would look like, here's some example data and templates:

var templateData = {
  label: 'Comments on post for Wednesday, Dec. 5th',
  comments: [
     type: 'freetext',
     creator: {fullName: 'Pamela Fox', profileUrl: 'http://everyday.io/user/1'},
     textHtml: 'Have you tried macademia nut oil instead? You can get it from <a href="http://www.amazon.com">.'
   ]
}

Using jQuery templates:

var dom = $('#stream-comment-tmpl').tmpl(templateData);
<script id="stream-comment-tmpl" type="text/x-jquery-tmpl">
  <span>${label}</span>
  {{each comments}}
  <div class="stream-comment">
    {{if type == 'highfive'}}
     <div>
      <span class="icon-highfive"></span>
      High five from <a href="${creator.profileUrl}">${creator.fullName}</a>!
     </div>
    {{else}}
     <a href="${creator.profileUrl}">${creator.fullName}</a>:
     {{html textHtml}}
    </div>
    {{/if}}
  </div>
  {{/each}}
</script>

Using jsRender templates:

var template = $.template('stream-comment-tmpl', document.getElementById('stream-comment-tmpl').innerHTML);
var html = $.render(templateData, template);
<script id="stream-comment-tmpl" type="text/jsrender-tmpl">
  <span>{{=label}}</span>
  {{#each comments}}
  <div class="stream-comment">
    {{#if type == 'highfive'}}
     <div>
      <span class="icon-highfive"></span>
      High five from <a href="{{=creator.profileUrl}}">{{=creator.fullName}}</a>!
     </div>
    {{else}}
     <a href="{{=creator.profileUrl}}">{{=creator.fullName}}"</a>:
     <div>{{=textHtml!}}</div>
    {{/if}}
  </div>
  {{/each}}
</script>

In jQuery templates, you could pass null as a value in an array and use the if operator to test whether it was defined. In jsRender, the template will not be called at all for a null value. Instead, you need to define objects in the array, set an object property to null, and check to see if that object property is defined. The reasoning behind the change is discussed more in this issue.

To give you an idea of the change, here's a before and after - notice I had to change the data format itself.

Using jQuery templates:

var templateData = ['Walked to work', null, null, 'Biked to work'];
<script id="notes-mini-tmpl" type="text/x-jquery-tmpl">
<div>
  {{each dates}}
    {{if $value}}
     ${value}
    {{else}}
     No notes for this date.
    {{/if}}
  {{/each}}
</div>
</script>

Using jsRender templates:

var templateData = [{notes: 'Walked to work.', {notes: null}, {notes: null}, {notes: 'Biked to work'}];
<script id="notes-mini-tmpl" type="text/jsrender-tmpl">
<div>
  {{#each dates}}
    {{#if notes}}
     {{=notes}}  
    {{else}}
     No notes for this date.
    {{/if}}
  {{/each}}
</div>
</script>

Saturday, November 19, 2011

Porting from jQuery to Zepto

In my quest for better performance in my PhoneGap Android app, I finally decided to switch from using jQuery to Zepto, a framework that boasts a similar API with a much smaller footprint and additionally offers mobile specific touch events. Of course, since Zepto is designed to be lighter weight than jQuery, it does not offer all the same methods — and the methods that it does offer sometimes differ in their arguments. This makes sense but it means that porting from jQuery to Zepto requires learning how their differences affect your code, and how you can workaround them.

Since other developers might start going down the porting path too, I thought I'd write a post on the differences I encountered while porting. Because I reuse much of my application code between my Phonegap app and desktop web app and I'm not yet ready to eliminate jQuery in my desktop app, I wanted to port my code over in a way that would mean it would work with either jQuery or Zepto as a base framework. That effectively meant that I tried to keep my code the same, and simply add to the Zepto JS file where needed. I'll explain my ports below and then show my Zepto additions at the end.

This post got a bit long, so here are quick links to the sections:

I spent most of my time porting 3rd party jQuery plugin code, as they tend to use the more esoteric jQuery features, but I also found a few places in my own application code where I was using jQuery functionality that's missing from Zepto:

  • CORS:

    I use CORS (cross-domain XMLHttpRequests) to communicate from my mobile app to my server, and that means setting xhrFields in jQuery.ajax(), something that isn't yet supported in Zepto. Luckily, there's a patch for it, and I applied that. I also discovered a bug with sending empty strings in CORS requests, and I work around that by sending null instead.

  • scrollTop:

    I use jQuery.scrollTop() to reset the scroll in my mobile app on page transition, so I took and Zepto-ified the jQuery implementation.

  • position:

    I use jQuery.position() to calculate where to scroll to in some cases, and Zepto only supports offset(). I checked out the jQuery source code for calculating position and stuck that inside Zepto.

  • non-standard selectors:

    jQuery supports several convenience selectors that aren't defined in the w3 spec and simply special cases them in their code, and these won't work as selectors in Zepto. There are some that correspond to methods - ":first" is .first() in Zepto, and ":last" is last(). There are others with no equivalents, like ":hidden", and ":visible", and I just removed my usage of them or checked display values manually. (I didn't feel like checking the jQuery source for them.)

  • valueless attributes:

    There are times when I set and remove valueless attributes on form elements, like "checked", "readonly", and "disabled". In jQuery, I used .attr('disabled', false). That's apparently been replaced in jQuery 1.7 by .prop('disabled', false). Neither will work in Zepto, however, but removeAttr('disabled') will. Since I want my code working with both, I now have .attr('disabled', false).removeAttr('disabled'), just to be sure.

Porting plugins

I use five jQuery plugins in my mobile app, only one of which I wrote myself, and they took the longest to port. For all of these plugins, I started off by changing the final line in the file from '})(window.jQuery);' to '})(window.jQuery || window.Zepto);' and then hoped for the best.

  • ColorSlider:

    I wrote this plugin to give users a colorful slider (red to green), and the only jQuery function it was missing was outerWidth(), so I wrote one based on the jQuery implementation. In doing so, I discovered that the Zepto width() function includes the padding and border, which jQuery.width() doesn't, so I submitted a patch clarifying that in the Zepto docs.

  • jQuery Templates:

    I was pretty worried about this port, but then found out that the jQuery templates was being deprecated in favor of a non-jQuery dependent library, jsRender/jsViews. That library is in beta and the docs are minimal, but the templating syntax is largely the same, and the author responded quickly to my upgrade question. One thing to note, though: the jsViews library tries to take the global $ variable if it doesn't see jQuery defined, so you have to call $.noConflict() after loading it if you want to use $ with Zepto instead — but you don't want to call that if you are actually using jQuery. Here's what I have after I load my scripts: if (window.Zepto) $.noConflict();

  • timeago:

    This handy library turns timestamps into pretty times like "1 day ago" and also auto refreshes elements with timestamps on an interval. It relies on jQuery.trim, which I just copied from their codebase. Along with the DateInput plugin, it relies on the ability to store objects as data attributes (instead of just strings), and thankfully, there's a data.js in the Zepto codebase that I copied into my Zepto JS to handle that.

  • Twitter Bootstrap modal:

    This is a simple modal library that's designed to work with either jQuery or Ender(a lightweight package library), so it already relied on minimal jQuery features. It does attach functions to $.support, which Zepto doesn't define, so I added a one-liner to define it. It also uses $.proxy, which I copied from the jQuery codebase.

  • jQuery Tools DateInput:

    This was by far the hardest plugin to port, and I did look around briefly to see if I should just switch to a completely different pure JS datepicker library, but I quite like this one so I stuck with with it. First, it uses $.expr, a jQuery object that isn't actually documented and (from what rumors tell me) might actually go away soon. I wasn't actually using the results of that function, so I simply defined the object so that the code would not error out. It also uses jQuery.clone and Event.isDefaultPrevented, which were simple enough to write my own versions of.

    Now we get to the tricky parts - methods that exist in both frameworks but don't have the same interface or behavior. I filed bugs on most of these, but the Zepto team could decide to keep their interface the same for simplicity: Zepto doesn't handle multiple self-closing tags in a creation string the same way as jQuery (Issue 322), Zepto does not assign this to the current iterated object in each() (Issue 295), and Zepto does not include an option for deep copy in the extend() function, and Zepto doesn't handle non-DOM objects in expressions/events (Issue 321, That last one unfortunately required a bit of dirty finagling in the DateInput code itself, as its non obvious how to extend Zepto to support binding and firing events on non-DOM objects.

Summary: What's different

To summarize, the following jQuery functionality is missing from Zepto (and much of it is in the gist below):

  • scrollTop()
  • position()
  • ":first", ":last", ":hidden", ":visible"
  • prop()
  • outerWidth()
  • trim()
  • support()
  • proxy()
  • expr()
  • isDefaultPrevented()

And this functionality differs in behavior/arguments:

  • attr('disabled', false);
  • ajax() CORS
  • width()
  • data()
  • each()
  • extend()

My Zepto modifications

Most of the above issues were resolved by defining functions or objects in Zepto, and this gist shows all of those:

I also made a few tweaks to the core Zepto code, and you can see those in this diff (but not all of them are necessarily successful tweaks, nor are they tested.)

Performance comparison

Since my main point in porting over to Zepto was to improve my loading performance, I celebrated the successful porting by measuring the loading times for my page. I used this timing code and compared the differences between including a minified jQuery 1.7 script tag and a minified Zepto script tag, both for a fresh app install and an app re-launch. On average, switching to Zepto shaved 22% off my total loading time. For the detailed results, check out this spreadsheet.

Was it worth it?

I'm happy that I ported, but it's not a decision to be taken lightly. jQuery alternatives are still in early stages, so if you use them, you have to think of yourself as a beta user, be prepared for issues, be prepared for lack of documentation, and do the responsible thing — report whatever issues or workarounds you find. And on that note, thanks to the Zepto authors for bearing with my barrage of issues over the last few days. :)