Showing posts with label zepto. Show all posts
Showing posts with label zepto. Show all posts

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. :)