Going Simple with JavaScript

I was making a change to a page that needed to pull from a remote API and make changes to various parts of the page. Sounds like the time to pull out jQuery and Ajax, doesn't it? Instead, I just used old fashioned JavaScript. Actually, I used new fashioned JavaScript.

Browsers haven't stood still in the advent of libraries and frameworks. As a result, we can take advantage of those features when we need to bake in a little extra.

Some JSONP

The first step was to get the JSONP call executing. This is generally straightforward: embed a script tag into the page. The script will run a function that you've defined on your page.

var scr = document.createElement('script');
scr.src = 'http://openexchangerates.org/latest.json?callback=formatCurrency';
document.body.appendChild(scr);

When the script runs, it'll pass the data into the formatCurrency function. Excellent.

Once the data is in the function, I needed to grab all the elements of a particular type and make changes based on those.

querySelectorAll

The querySelectorAll method will grab all elements that match a particular selector—similar to jQuery. It's limited to selectors that the browser understands, which is definitely less than what jQuery can do. Sometimes a chisel will do in place of a sledgehammer.

The querySelectorAll method works in IE8 and up and all of the other popular browsers, too. I also wrapped my entire block of code to check if the browser supports this method before doing anything.

if (document.querySelectorAll) {
    function formatCurrency (data) {

        var els = document.querySelectorAll('.price');
        /* do stuff with the elements */
    }

    var scr = document.createElement('script');
    scr.src = 'http://openexchangerates.org/latest.json?callback=formatCurrency'; 
    document.body.appendChild(scr);
}

As you can see in the example, all I'm looking for is every element that has a class of price. I could've used getElementsByClassName but that's not supported in IE8, whereas querySelectorAll is. So far, so good.

After that, it was just a matter of getting an attribute, making some changes and then re-inserting it into the DOM using innerHTML. Easy peasy.

Progressive Enhancement

But what of users who don't support this new fandangled functionality? It'll be the same as those who don't support JavaScript. In my case, it meant that users will see just Canadian pricing instead of converting it into their local currency.

Here's the final script in its entirety.

if (document.querySelectorAll) {
    var currencyLookup = {
        EUR:'€', USD:'US$', CAD:'CDN$'
    }
    function formatCurrency (data) {
        // format germany price
        var els = document.querySelectorAll('.price');
        for (var i=0; i<els.length; i++) {
            var price = parseInt(els[i].innerHTML.replace(/[^0-9]*/,''));    
            var curr = els[i].getAttribute('data-currency');
            var newPrice = price / data.rates.CAD * data.rates[curr];
            els[i].innerHTML = '<small>' + currencyLookup[curr] + '</small>' + Math.round(newPrice); 
        }
    }

    var scr = document.createElement('script');
    scr.src = 'http://openexchangerates.org/latest.json?callback=formatCurrency';
    document.body.appendChild(scr);
}

That's a total of 628 bytes. No JavaScript libraries or frameworks required. That could be minified to 469 bytes. A far cry from the 91,000 bytes needed just to get jQuery on the page.

Take it easy

"Just use jQuery" might be the go-to phrase but, thankfully, you don't always need that much code to solve a simple problem. Pages are getting bigger and bigger. It's nice to know that we don't always have to use large resources to accomplish a small goal.

Published March 05, 2012
Categorized as JavaScript
Short URL: http://snook.ca/s/1014

Conversation

21 Comments · RSS feed
Bramus! said on March 05, 2012

This overview's also handy towards stepping back from jQuery:
Vanilla JavaScript FTW – http://sharedfil.es/js-48hIfQE4XK.html

KMB said on March 05, 2012

The final script got mangled in your code.

Does the script mean, that even though no one wants to convert prizes you still pull the json-data on every page-load? This would be the kind of functionality I would do on the server-side with caching and stuff. Especially if I want to sell products outside Canada.

Last but not least: I agree with your last two paragraphs but would add that even though jQuery is a heavy stone to kill a small bird, it usually kills a lot of birds at the same time.

Jonathan Snook said on March 05, 2012

@KMB: I want the prices to show the approximate price in the local currency. I surveyed people and most said they'd prefer to see it in their local currency. I could do it server-side but then that meant I had to figure out a caching scheme and write more code than I wanted to. It would be kind of me to implement it server-side with some caching anyways, so that I don't hit the API too often but this page isn't trafficked heavily (less than a hundred hits a day).

Airman said on March 05, 2012

for (var i=0; i' + Math.round(newPrice);
}

it's an error of syntax or a syntax I didn't know ?

Jonathan Snook said on March 05, 2012

@airman: oops. Encoding issue. Fixed. Thank you. :)

Joan Piedra said on March 05, 2012

I can express how strongly I agree with this, while being a jQuery fan from its beginning I do encourage developers to learn and use real Javascript.

And if you'd like to use even fancier Javascript, you could of course use els.forEach(), since you querySelectorAll and Array.forEach are only supported from IE8 onward.

Jonathan Snook said on March 05, 2012

@Joan Piedra: Indeed, forEach is available in newer browsers but could not be used directly with the NodeList returned by querySelectorAll. The NodeList looks like an Array but isn't and therefore, the forEach method would fail (hence the traditional for loop in the example).

Bruno Carriere said on March 05, 2012

On forEach; To change the NodeList into an Array:

Array.prototype.slice.call(nodelist, 0)

Should do the trick. But cross-browser support should be ensured, because I have a feeling it might fail in IE.

It's nice to see how little code is needed for JSONP.

Mathias Bynens said on March 05, 2012

@Bruno, no need, you could just do it like this:

[].forEach.call(document.querySelectorAll('.price'), function(el) {
  // do stuff with `el` here…
};

@Jonathan, note that if (condition) { function fn() {} } is not allowed in ES5 strict mode, and will declare fn even if condition is falsy. Function expressions are allowed, though, so you could do something like:


var fn;
if (condition) {
  fn = function() {};
}

In this case I think the ideal solution would be to use an IIFE:


condition && (function() {
  function fn() {}
  // do other stuff here that will only be executed if `condition` is truthy
}());
Xander M said on March 05, 2012

Love going simple with JS if I can. Definitely the way forward for simple sites.
That said if you use a cdn for popular libraries then generally it doesn't slow down for the end user, as its a fair bet that they already have the latest version cached.

Not@applicable.com said on March 05, 2012

Please, for the sake of the children; always specify a base for your integer parsing. If you are making judgement on currency exchange rates and parse that leading zero... well lets just say it could be a very very bad day.

Adam said on March 05, 2012

It is important to know how to use JavaScript, but I would still use jQuery for this task. Because jQuery is so ubiquitous I assume the Google CDN hosted copy is cached on most machines so the is no serious delay to loading it. jQuery code to do this task would be simpler and easier to read and maintain and requires no code to account for browser compatibility issues.

Jonathan Snook said on March 05, 2012

@NotApplicable.com: Please, for the sake of the children, I wrote the HTML and the JavaScript, so I'm rest assured my prices will not have leading zeros. :) (but point taken)

@Adam: cache miss ratios and browser performance still put a burden on users and browsers. The browser still has to parse 91k of JavaScript just to do something that will take approximately the same amount of code. Why plan for the best case scenario?

Joss said on March 05, 2012

Great article, nice to see somebody using open exchange rates! Would be awesome to get a clicky link to it in the post - it's a free service so relies on people spreading to word for it to be maintained.

Joss

Oliver said on March 05, 2012

JSONP is risky -- if you ever plan on offering private (or potentially confidential) data secured through session cookies and the like, JSONP opens your site to XSS attacks, in a way that real JSON and XML don't.

Nikke said on March 05, 2012

Just lovely.

<nitpick>
But you left out the radix in parseInt... ;-)
</nitpick>

Ward said on March 08, 2012

I think I see one more bug - You only define the formatCurrency function in IE8 and up, not IE7, but IE7 will insert the script tag and fire the callback won't it? Eees bad, no?

Jonathan Snook said on March 08, 2012

@Ward: Because the entire script is in a check for querySelectorAll, nothing will run in IE7, including the insertion of the script tag.

Graham B said on March 09, 2012

I really do try to avoid jQuery wherever I can (I wrote something to this effect in a reply to one of your posts on animations). The short version; if you know jQuery but can't understand the underlying code then you're just building on sand (and I simply don't buy the 'its cached from a CDN anyway' argument).

Scott said on March 09, 2012

For an extra couple hundred bytes or so you could have probably extended support down to IE 6 by using getElementsByClassName and implementing a custom function for browsers that don't support it--you know, like this http://snook.ca/archives/javascript/your_favourite_1 ;-)

Like you said, it's not important in this case. But if someone else had a more critical reason to support more browsers, there are still lots of options outside of jQuery.

Nancy said on March 26, 2012

I would like to note that this was the third SCR that I tried, and of the three this one had the worst burn. I am really hipong with some more aging these will turn out to be a decent mild smoke. I like the flavors associated with the Habano wrapper but this one didn't utilize them well. A great Habano to try is the Padilla Habano. The wrapper is a lighter shade but the flavor profile is much better.

Sorry, comments are closed for this post. If you have any further questions or comments, feel free to send them to me directly.