When you're building a non-trivial JS application ...

Posted

I sense another round of discussion of this is about to begin, and 140 characters isn't quite enough to say what I want to say, so:

When you're building a non-trivial JS application, you don't want a jQuery developer, or a Dojo developer, or a YUI developer, or, frankly, any developer who chooses their tool before they evaluate the problem. For god's sake, you want a JavaScript developer. Can you roll your own solution with jQuery as the base? Yes! Should you? I don't think so, and I advise my clients against it for reasons I've written about at length, but I'm open to hearing compelling, articulate, fact-based arguments in favor of it!

But do me a favor, OK? Don't base your arguments solely on the winner of a popularity contest. Don't tell me how easy it is to find developers familiar with one library or another, because I'll come right back and ask you just how good those developers will be at solving problems that aren't addressed by said library. And please tell me you've at least explored some of the other options besides [insert the library you're advocating here]. 

People read what I write about JavaScript libraries and they write me heartfelt tweets and e-mails saying OMG YOU HATE JQUERY NOW WHAT HAPPENEDDDDD? I don't hate jQuery! It is a perfectly viable and valuable tool for so many things! But when people argue not just its viability but its absolute supremacy, when people get defensive and possibly even angry that I suggest there are solutions that are vastly better suited to a certain set of problems, when people contort themselves into pretzels to make their case and their case is "well, it's not that bad" ... well, that smacks of blind loyalty, not a thoughtful weighing of the tradeoffs and challenges we face as developers, and I question how those people would fare if actually confronted with the needs of a non-trivial application. 

So, please: Tell me what solutions you've looked at for non-trivial application development. Tell me where they work, tell me where they fall short. Tell me what you're working on and how you chose the tools. Don't tell me why I'm wrong -- tell me why you're right. Deal? Discuss.

Filed under  //  javascript  
Comments (15)
Posted

In Search of JavaScript Developers: A Gist

Posted

I posted a gist the other day in an attempt to locate some JavaScript help. I’m finding, lately, that I’m being asked to do more work than I can comfortably take on without having some overflow capacity, but I’ve been a little lax at actually identifying people who can provide that capacity up until now. That meant I was turning down work, and that’s not a good thing.

I had a great time the last couple of days watching people come up with solutions, and was especially gratified that so many people who weren’t looking for work thought that answering JavaScript questions sounded like fun.

A lot of people have asked if I’d be providing “answers” to the questions I posed in the gist, so I wanted to try to do that, mixed in with a bit of commentary about what I was looking for.

Question 1: Writing Readable but Terse Code

// 1: how could you rewrite the following to make it shorter?
if (foo) {
  bar.doSomething(el);
} else {
  bar.doSomethingElse(el);
}

Answers to this question told me whether to keep reading; I was looking for something like:

bar[foo ? 'doSomething' : 'doSomethingElse'](el);

// OR

bar['doSomething' + (foo ? '' : 'Else')](el);

To be honest, the second one is maybe a little too clever. But generally, I wanted to see submissions that understood using the ternary operator instead of an if statement to create shorter but still-readable code when all that is different is a method name.

Some people submitted an answer that used a ternary operator, but didn’t take advantage of it to just switch the method name:

foo ? bar.doSomething(el) : bar.doSomethingElse(el);

This is an improvement, definitely, but there’s room for more.

Question 2: Understanding Variable Scope

var foo = 'hello';

(function() {
  var foo = foo || 'world';
  console.log(foo);
})();

This was a question where, in hindsight, what I was looking for may not have been super-clear. I got a lot of right-enough answers, but really what I wanted to hear was that the || statement was absurd, because foo would always evaluate to 'world' due to variable hoisting.

This can be sort of a sort of crazy thing to wrap your head around, but basically JavaScript scans the contents of a function for var statements before it runs the function. Any variable initialized with a var statement inside the function will become local to the function, even if the variable is “used” before it is initialized. Changing the order of the two lines inside the function shows this readily:

var foo = 'hello';

(function() {
  console.log(foo); // undefined!
  var foo = foo || 'world';
})();

Some submissions thought I wanted access to the external foo inside the closure — not an unreasonable interpretation of the question.

var foo = 'hello';

(function(f) {
  var foo = f || 'world';
  console.log(foo);
})(foo);

Anyway, there were lots of right-enough answers, but anyone who talked about hoisting definitely caught my eye.

Question 3: Working with Objects and Prototypes

This question was looking for really basic understanding of prototypes. It also was written to be a tad hard to follow, lumping all the questions into a single paragraph, to simulate a not-atypical client request.

// 3: given the following code, how would you override the value of the 
// bar property for the variable foo without affecting the value of the 
// bar property for the variable bim? how would you affect the value of 
// the bar property for both foo and bim? how would you add a method to 
// foo and bim to console.log the value of each object's bar property? how 
// would you tell if the object's bar property had been overridden for the 
// particular object?
var Thinger = function() {
  return this;
};

Thinger.prototype = {
  bar : 'baz'
};

var foo = new Thinger(),
    bim = new Thinger();

The good submissions broke the question down into separate comments, and then showed the answers:

// override the bar prop for foo w/o affecting bim
foo.bar = 'new value';

// change the bar prop for both foo and bim
// (*if* it hasn't been overridden locally!)
Thinger.prototype.bar = 'another new value';

// we could delete foo.bar now and it would get
// the prototype value instead
// delete foo.bar;

// add a method to foo and bim to log bar
Thinger.prototype.logger = function() {
  console.log(this.bar);
};

// check if bar has been overridden
foo.hasOwnProperty('bar'); // true
bim.hasOwnProperty('bar'); // false

Question 4: Iterating over Objects

This one was pretty basic:

// 4: given the following code, and assuming that each defined object has 
// a 'destroy' method, how would you destroy all of the objects contained 
// in the myObjects object?
var myObjects = {
  thinger : new myApp.Thinger(),
  gizmo : new myApp.Gizmo(),
  widget : new myApp.Widget()
};

Really I just wanted to see people iterate over an object without the use of a helper like jQuery.each. The hasOwnProperty check may seem like overkill, but I was glad when people didn’t leave it out. Adding in the delete statement was another nice touch, though not strictly required by the question.

for (var obj in myObjects) {
  if (myObjects.hasOwnProperty(obj)) {
    myObjects[obj].destroy();
    delete myObjects[obj];
  }
}

Question 5: Solving Deceptively Simple Problems

This question was probably the most fun, because even though it was a dead-simple task, the answers were all over the map. This was the question:

// 5: given the following array, create an array that contains the 
// contents of each array item repeated three times, with a space between 
// each item. so, for example, if an array item is 'foo' then the new 
// array should contain an array item 'foo foo foo'. (you can assume the 
// library of your choice is available)
var myArray = [ 'foo', 'bar', 'baz' ];

Rather than going through the different answers one at a time, I’m just going to tell you to visit this JSPerf test page to see some of the variations, and their relative performance.

This brings up a good question tweeted by Ryan Florence:

T | F – In #JavaScript, 90% of the time we do stuff only a few times, maybe hundreds. Therefore, 90% of the time Readability > Performance.

I tend to come down on the side of readability and compression over straight-up perf, precisely because we’re rarely doing anything that’s actually that intensive. On the other hand, we should avoid doing things that are outright stupid; where the line gets drawn depends a lot, I think, on experience.

Here’s the thing, though: Something that seems like a gratuitous and obscure optimization to a less experienced developer might seem completely readable and obvious to a more experienced developer. How to balance this? Can comments bridge the gap? Should the gap be bridged? I dunno.

Question 6: Basic jQuery Best Practices and DRY

I see way too much code in real life that looks like this question.

// 6: how could you improve the following code?
$(document).ready(function() {
  $('.foo #bar').css('color', 'red');
  $('.foo #bar').css('border', '1px solid blue');
  $('.foo #bar').text('new text!');
  $('.foo #bar').click(function() {
    $(this).attr('title', 'new title');
    $(this).width('100px');
  });

  $('.foo #bar').click();
});

There are a slew of things wrong in this tiny snippet. First and foremost, making the same selection repeatedly suggests that the author fundamentally doesn’t understand what their code is doing, or the expense they’re incurring in doing it; the selection should be made once, and then the selection should be cached and/or the methods should be chained.

While it wasn’t imperative, submitters did well to point out that CSS changes should be made via class names instead of hard-coded CSS in JavaScript; they also did well to put the click handler in a named function. Finally, while there may be cases where an ID selector needs to be prefixed by a class, i.e. .foo #bar, I appreciated it if people questioned this.

$(document).ready(function() {
  var handleClick = function(el) {
        el.attr('title', 'new title')
          .width('100px');
      },

      bar = $('#bar')
        // ideally: use a class for this
        .css({
          color : 'red',
          border : '1px solid blue'
        })
        .text('new text!')
        .click(function(e) {
          handleClick($(e.target));
        });

  handleClick(bar);
});

Question 7: Asynchronicity

This is a pretty newby thing, but I wanted to make sure people understood the basic concept of async requests — that is, you can’t set the value of a variable inside an XHR’s callback and expect that value to be available immediately.

(function() {
  var foo;

  dojo.xhrGet({
    url : 'foo.php',
    load : function(resp) {
      foo = resp.foo;
    }
  });

  if (foo) {
    // run this important code
  }
})();

Fixing this just involves waiting for the XHR to complete before running the code that expects foo to be set. (Alternately, you could make the request run synchronously by setting sync : true in the XHR config object.)

There was one other issue with this code as well: Dojo needs to know the response should be handled as JSON, else it will handle it as text. If a submitter missed this, I didn’t hold it against them — Pete Higgins actually had to point it out to me :) That said, it would become pretty obvious pretty quickly in real code.

(function() {
  dojo.xhrGet({
    url : 'foo.php',
    handleAs : 'json'
  })
  .addCallback(function(resp) {
    if (resp && resp.foo) {
      // do stuff
    }
  });
})();

Note that the callback function could also be specified in the XHR config object using the load property; Dojo’s XHRs are great in that while you can specify everything in a config object, you can also attach callbacks to the return value of the XHR methods. You should read more about this because it is very pleasant.

Question 8: DRY

Repetitive code is dumb.

// 8: how could you rewrite the following code to make it shorter?
(function(d, $){
  $('li.foo a').attr('title', 'i am foo');
  $('li.bar a').attr('title', 'i am bar');
  $('li.baz a').attr('title', 'i am baz');
  $('li.bop a').attr('title', 'i am bop');
})(dojo, dojo.query);

How far you want to go with DRYing this out is debatable, but to me this cries out for improvement. Here’s what I’d do:

(function(d, $){
  d.forEach(['foo', 'bar', 'baz', 'bop'], function(c) {
    $('li.' + c + ' a').attr('title', 'i am ' + c);
  });
})(dojo, dojo.query);

I’d be lying if I didn’t mention that I also wanted to show here how easy it is to make Dojo look like jQuery. After all, it’s just JavaScript, right?

Question 9: DOM Manipulation Best Practices & DRY

I thought it was well-known that we don’t append 202 things to the DOM one at a time; the good news is, most people did know this. The bad news is, some people did not.

In addition to doing 202 appends, this code also does 202 selections. To top it off, the iterator i is global because we didn’t prefix it with var.

// 9: how would you improve the following code?
for (i = 0; i <= 100; i++) {
  $('#thinger').append(
    '<p><span class="thinger">i am thinger ' + i + '</span></p>'
  );
  $('#gizmo').append(
    '<p><span class="gizmo">i am gizmo ' + i + '</span></p>'
  );
}

Here’s a fix:

var thingerDom = [], gizmoDom = [],
    tpl = '<p><span class="%s">i am %s %i</span></p>',
    tplFn = function(str, i) {
      return tpl.replace(/%s/g, str).replace(/%i/g, i);
    },
    i;

for (i = 0; i <= 100; i++) {
  thingerDom.push(tplFn('thinger', i));
  gizmoDom.push(tplFn('gizmo', i));
}

$('#thinger').append(thingerDom.join(''));
$('#gizmo').append(gizmoDom.join(''));

There’s more that could be done here to DRY this out a bit more, but the fix addresses the main problem of excessive DOM manipulation.

Question 10: Loose Typing

Numbers in JavaScript suck, especially when the user enters them.

// 10: a user enters their desired tip into a text box; the baseTotal, 
// tax, and fee values are provided by the application. what are some 
// potential issues with the following function for calculating the total?
function calculateTotal(baseTotal, tip, tax, fee) {
  return baseTotal + tip + tax + fee;
}

How you’d actually deal with this problem would probably depend on the business logic of your application; you may be well-advised to convert everything to integers instead of trying to deal with decimals, because math with floats in JavaScript can have issues.

What I wanted to see in submissions, though, was an awareness that the tip would come to us as a string, and we couldn’t just add it to the other arguments and expect a useful result. I was more interested in the discussion of this problem, and other problems that could arise, but here’s at least the beginning of a solution:

function calculateTotal(baseTotal, tip, tax, fee) {
  // convert the tip to a number using base 10;
  // allow for a NaN result from parseFloat
  tip = parseFloat(tip) || 0;

  // don't allow a negative tip
  if (tip < 0) { tip = 0; }

  return baseTotal + tip + tax + fee;
}

Question 11: Array Mapping

For this question, I was looking for code that used a map method to return an array by running a function on each item in the array. Some people used a forEach method to iterate over the array instead, and then push the results to a new array they created. I guess this is OK, but it’s not my preference.

// 11: given the following data, write code that returns an array 
// containing the name of each item, followed by a comma-separated list of
// the item's extras, if it has any. e.g. 
//
//    [ "Salad (Chicken, Steak, Shrimp)", ... ]
//
// (you can assume the library of your choice is available)
var menuItems = [
  {
    id : 1,
    name : 'Salad',
    extras : [
      'Chicken', 'Steak', 'Shrimp'
    ]
  },
  {
    id : 2,
    name : 'Potato',
    extras : [
      'Bacon', 'Sour Cream', 'Shrimp'
    ]
  },
  {
    id : 3,
    name : 'Sandwich',
    extras : [
      'Turkey', 'Bacon'
    ]
  },
  {
    id : 4,
    name : 'Bread'
  }
];

Here’s an answer:

var newArray = dojo.map(menuItems, function(item) {
  var ret = item.name;
  if (item.extras && item.extras.length) {
    ret += '(' + item.extras.join(', ') + ')';
  }
  return ret;
});

Bonus 1: Functional Programming 101

This was a late addition, courtesy of Andrew Hedges, so not everyone saw it:

// BONUS: write code such that the following alerts "Hello World"
say('Hello')('World');

I wanted to see people understand that functions could return other functions, and that the returned function has access to the scope of the wrapper function:

var say = function(first) {
  return function(second) {
    alert(first + ' ' + second);
  }
};

Some people got pleasantly carried away with this question; check out this JSFiddle from Colin Snover.

Bonus 2: Attention to Detail

This last bonus was riddled with errors, including some that I made accidentally when I wrote it at 2 a.m.

// BONUS: what is the faulty logic in the following code? 
// how would you fix it?
var date = new Date(),
    day = date.getDate(),
    month = date.getMonth(),
    dates = [];

for (var i = 0; i <= 5; i++) {
  dates.push(month + '/' + (day + i));
}

console.log('The next five days are ', dates.join(', '));

Here’s what you should see:

  • The for loop will return 6 dates, not 5.
  • The method for calculating the date for each successive date could end up with nonexistent dates (32, 33, etc.), and it doesn’t change the month when it should.
  • The getMonth method on the date object returns a zero-indexed month.

Here’s a fix:

(function() {
  var date = new Date(),
      otherDate = new Date(),
      day = date.getDate(),
      future = 5,
      dates = [],
      i;

  for (i = 1; i <= future; i++) {
    otherDate.setDate(day + i);
    newMonth = otherDate.getMonth() + 1;
    newDay = otherDate.getDate();
    dates.push(newMonth + '/' + newDay);
  }
})();

Postscript

I want to be really clear that I’m not some super-awesome and infallible JavaScript developer, and more to the point, there was a time in the not-too-distant past where I would have failed my own quiz miserably. While my main goal in putting this together was to find some skilled developers to help me out, I also wanted to provide a tool for exposing up-and-coming developers to some slightly more advanced concepts of JavaScript. I hope that, whatever your skill level, you found it to be at least entertaining, and at best, useful. I also hope you’ll forgive any gross errors I’ve made in the answers above, though I tried really hard to test them all.

Post-Postscript

  • To the person who complained that I included code from a library other than jQuery, and who helpfully illustrated their point with the graph that shows jQuery is obviously better than anything that ever was: I’m not sure whether to laugh or cry or just be really snarky. I’m looking for JavaScript developers. If you don’t see the value in knowing more than just jQuery, and if you can’t find your way through relatively trivial non-jQuery code, you need not apply.
  • If you found the questions vague and requiring a lot of assumptions or guesswork, well, welcome to consulting — if such things make you uncomfortable, we’re not going to be a good fit. I was looking for people to tell me what they know, to impress me, to point out where they saw holes in the questions, to take initiative. This was not a multiple choice test; it was an interview.

License

If you think the quiz would be useful to you as you’re looking for a JavaScript developer to call your own, it’s licensed under the WTFPL, which you should read just because it’s funny.

Filed under  //  javascript   performance   refactoring  
Comments (29)
Posted

Code Org, Take 2: Structuring JavaScript Applications

Posted

It’s hard to believe it was not even a year ago that I spoke at the jQuery Conference in Boston about code organization. At the time, I’d been thinking for a few months about how to improve my own code and give it more structure, and how to give advice to others about how to do the same.

My talk was about code organization, but really I was talking about how you might organize a single piece of functionality; I didn’t even begin to answer the larger question of how to structure a bona fide JavaScript application. Really, that question is almost perpendicular to the one I was tackling at the time; it’s a question of strategy, not tactics.

A year later, I’d like to share my thoughts on how I’m answering it these days.

First Things First: What’s a JavaScript Application?

GMail is a JavaScript application; this Posterous blog, while it makes use of JavaScript a bit, is probably not, but the admin interface most certainly is. The line can be frustratingly fuzzy, but at the same time it’s pretty important to realize when you’ve crossed it.

To me, the defining characteristic of a JavaScript application is that the heavy lifting of manipulating and displaying data falls to the browser, with most communication with the server happening via XHR.

If you find yourself in application land, welcome. Now what?

My Building Blocks

My approach to organizing an application is really just an MVC variant, so I don’t want to sound as though I’ve discovered something novel or new. However, there are a couple of things to note: For one, the term “controller” has a couple of different meanings to me, as explained below; for two, there are two distinct flavors of “views,” though I’m not sure exactly how important the distinction is.

I also want to be super-clear that I don’t tout this as The One and Only Way; it is just a way that has worked for me, a way that I evolve and adapt with every project I work on, and a way that I’ve run by a few people and they haven’t laughed at me. My point in dissecting it isn’t that you’ll try to follow it word-for-word; rather, I hope it might get you thinking about JavaScript applications beyond the DOM.

Models

There’s nothing particularly unique to models in a JavaScript application. They are responsible for fetching and storing application data and maintaining its integrity in the browser. They fetch data, store data, and provide an API for other application components to get access to that data. There may be more than one destination for the data: memory, the server, or some type of local storage. But if it has to do with managing data, it’s the model’s job.

Models stay out of the way when it comes to displaying data or responding (at least directly) to user interaction. Those tasks are left up to other pieces of the application, as we’ll see below.

Example

A simple search application would likely have a search results model, responsible for receiving the current search term, fetching the data for the term, and broadcasting it to the rest of the application. It might also allow for manipulating individual search results, such as indicating that a particular result was a favorite or a dud, though that task might also fall to an individual search result model depending on the needs.

Widgets and Data Views

Views comprise HTML (generally in the form of client-side templates) and CSS for a component, and are generally accompanied by a view controller (the JavaScript related to interacting with the vew; see below for an explanation). The HTML for a view consists of a single parent node with an arbitrary internal structure; the parent node will optionally have one or more classes on it that can be used to target CSS.

There are two flavors of views, in my mind: widgets, which are responsible for supporting user interaction with the application but don’t render any application data; and data views that are responsible for displaying and allowing interaction with application data.

Data views are instantiated with the initial data required to populate them; then, their view controllers listen for messages from other pieces of the application to tell them when new or updated data needs to be rendered.

Examples

A basic search input box would be considered a widget — when it is created, it doesn’t need any application data in order to render properly. The widget is strictly responsible for allowing the basic interaction of typing a search term and hitting enter; that is, it’s not responsible for actually performing the search.

A search results list is an example of a data view; it renders application data and, potentially, allows for interaction with it. Again, though, it’s not responsible for performing the search; it just renders data and then allows for interaction with it.

View Controllers

View controllers manage interaction with a data view or widget — interaction by the user, and interaction with the rest of the application. They are responsible for binding and handling events, for broadcasting user interactions with the widget to the rest of the application, and for listening to other pieces of the application to tell them they have new data to render.

View controllers never handle server communication directly; their role is solely to provide a user interface to the application. When something interesting happens to a view or widget, the view controller announces it. When new data is available for a view, the view controller should know how to handle it. But, again, the view controller itself should focus on providing a user interface, not on interfacing with the server.

Examples

The view controller for a search box might listen for the user to focus on the search box, hiding placeholder text for the input. Then, it might listen for the user to hit Enter inside the search box; when that happens, it would broadcast to the rest of the application that the user had submitted a search, along with the term that was searched.

The view controller for a search results list might listen for another piece of the application to announce that new search results are available to be displayed. If the results were for the currently displayed search term, it could add them to the list; if they were for a new term, the results list could empty itself and display the new results.

Application-Level Controllers

Application-level controllers are the glue of an application. Loosely, there may be one per “page” of the application, or one per feature. For example, an application that includes a search feature and a checkout feature might have a controller for each feature, even though the checkout feature might spread across multiple pages.

These controllers are responsible for getting the models and views/widgets for a feature in place and talking to each other. So, a controller might first make sure the required models are in place, then tell them to fetch the appropriate data; once the data is available, the controller would instantiate the views for displaying the data. Finally, the controller would broker future interactions between the views and the models.

Examples

On a search results page, a user might click a Favorite button on a search result. The search results list’s view controller would handle the click, broadcasting a message about the user’s action to the rest of the application. The controller would observe this message and pass it, along with any other relevant information, to the search results model, which would in turn pass the information to the server, or store it locally.

Notes on Enablers

I’ve glossed over a few implementation details that are somewhat tangential to the organization question, but I want to touch on them briefly:

Pubsub and Friends

I didn’t want to get too specific about how all of this “announcing” and “broadcasting” and “listening” happens, because there are lots of ways to accomplish it. One could use pubsub, custom events, or any number of other solutions. I don’t think the actual implementation is important, though personally I lean heavily on pubsub — what is important is the notion of broadcasting and listening for announcements that something has happened, allowing other components of the application to react appropriately.

Templating

If you aren’t using a toolkit with templating built in (or, heck, even if you are), I’ve kind of fallen in love with mustache.js lately. It’s a great client-side templating companion, making it dead-simple to turn data into markup without ending up with templates that look more like JavaScript than HTML.

Figuring out how to maintain templates can be tricky — do you store them in your page’s markup? Do you maintain them as separate files requested via XHR and then cached? Or do you put them in your JavaScript? Dojo’s dojo.cache() method provides a handy way to keep your templates in separate files and load them via XHR, while interning them into your JavaScript for you if you use Dojo’s build tool. I like this.

Attaching Events to Views

Another shameless Dojo plug: dijit.\_Templated provides some serious hotness when it comes to attaching events to views. Read up on dojoAttachPoint and dojoAttachEvent; together with dijit._Widget’s connect and subscribe methods, which provide automatic cleanup for you, there’s some real power here, which has me writing hardly any selector-based code these days.

File Structure

I hesitate to make any particular recommendations here, because the needs of an application can vary widely. However, I tend to have a directory each for models, views (for view controllers and templates), and controllers (for application-level controllers). Those directories — especially the views directory — may contain subdirectories, for instance if there’s more than one view for a certain type of data.

Why Go To All This Trouble, Again?

So this is the part where you might say “OMG, srsly, what happened to ‘get some elements, do something with them?!?’” Let me be clear, that approach may be entirely appropriate for your particular needs; I’m not here to convince you otherwise.

But: if your application is complex enough to warrant considering an approach like this, I’ve found that in the long run it actually simplifies my code by cleanly separating concerns and providing a decent roadmap for building new features. I can build and test a solid model for some Thinger, and then use that model throughout my code; I can build and test a user interface component for editing a Thinger long before the data exists to support it. I can map “pages” of my application to application-level controllers, providing a high-level view of what’s happening where.

Best of all, paths to code reuse become clear and entanglements become fewer when I keep this division of responsibilities in mind as I code. A search results data view, for example, can be made to accept search results from any model that provides them in the proper format; a search can be initiated and the results displayed without depending on a user entering text into a search input widget.

Dividing the responsibilities into well-defined sections leads to components that are truly pluggable, often in ways you may not have even imagined when you wrote them. In an application that evolves over time, it’s hard to overstate the benefits of this.

In Conclusion & A Plea

If you find yourself working on a JavaScript application, I can’t recommend enough that you consider, at length, what underlying structure makes the most sense; it’s almost inevitably more complex than you can manage via the DOM alone. Again, my answer isn’t the right answer, it’s just an answer, but I hope it helps you start thinking about what the right answer might be for your project.

If you’re interested in this stuff, I’d encourage you to check out JavaScriptMVC, if only to see how they approach these problems; Cujo.js is another framework, built on top of Dojo, that aims to enter this space, but you’ll have to wait until mid-September to see it.

Finally: If you have your own thoughts to share about how to approach these large application questions, I’m pleading with you to write your own blog post(s) about them. If you have other reference material on the topic, I beg you to share it. As more and more people transition from simple JavaScript enhancements to non-trivial applications, the need for education is huge.

Filed under  //  javascript   large-applications  
Comments (8)
Posted

Patterns for DRY-er JavaScript

Posted

I came across a little code the other day that reminded me I've been meaning to write about JavaScript patterns I take for granted. The code in question was intended to set the value of some fields in a form when a checkbox was selected; when it was deselected, the same fields were to be emptied. It looked not unlike this:

// config is defined outside of this snippet,
// and may contain more than the properties
// we care about 
$('#myCheckbox').click(function() {         
  if (this.checked) {                 
    $('#field_foo').val(config.foo);                 
    $('#field_bar').val(config.bar);                 
    $('#field_baz').val(config.baz);         
  } else {                 
    $('#field_foo').val('');                 
    $('#field_bar').val('');                 
    $('#field_baz').val('');         
  } 
});

This is a wholly readable bit of code -- there's almost no question what's going on here. On the other hand, it's pretty easy to see the rampant repetition; this code isn't interested in "don't repeat yourself" (DRY). We're calling the same method on every selection we make, and our selections are repeated in both the if and else block. When I saw this code, I had an immediate inclination to rewrite it. Here's what I came up with first:

// config is defined outside of this snippet,
// and may contain more than the properties
// we care about

$('#myCheckbox').click(function() {
        // note whether the checkbox is checked
        var checked = this.checked;

        // iterate over the keys we care about
        $.each(['foo', 'bar', 'baz'], function(i,v) {
                // find the field for the given key
                $('#field_' + v)
                        // and set its value either to the string
                        // stored for the key, or to an empty string,
                        // depending on whether the checkbox was checked
                        .val(checked ? config[v] : '');
        });
});

This looks approximately nothing like the initial code, and without the comments, the code itself would be substantially less readable than the original. The idealistic part of me -- the part that believes people who write JavaScript should understand JavaScript -- says this is an acceptable price to pay. And besides, there's something to be said for explaining the code in a comment that can be stripped by a minifier, rather than explaining the code via the code. In this iteration, we've introduced two patterns for DRY-er code: iterating over an array literal (or, alternately, an object) to achieve repetition without repeating ourselves, and using the ternary operator in place of an if/else statement when the simplicity of our logic allows it. The array literal serves as a list of the fields we care about. When our checkbox is clicked, we iterate over this list, build up a selector for each item in the list, make our selection, and then set the field value using a ternary operator. We've gone from 11 lines of code to six, with the added bonus that we have to do a lot less typing if we need our checkbox to affect more fields. (A side note: Is this premature optimization? I'd argue no, if you've learn to see these patterns before you start writing code. Once you learn how to spot these patterns in a requirement, writing code that embraces them can actually be easier than writing code that takes a more "literal" approach to the problem. For example, imagine if the checkbox affected 20 other fields instead of one? You'd undoubtedly find yourself copying and pasting code if you took the more "literal" approach to the problem, and that would be your first clue that you were doing something inefficiently.) The great thing about using a pattern like this is that it rapidly exposes the actual meat of what you're doing, and makes refactoring far less painful. I also find that it helps me see opportunities for reuse that I might not have spotted in the more literal version of the code. Let's say we're feeling all proud of ourselves for DRYing out our code using clever JavaScript that only super-smart people can read. Now there's another checkbox that needs similar behavior, but it's going to use a different config object and a different list of fields. No problem! You've already written this code, so you can just copy and paste it and then change what's different. Sweet. Er ... suddenly you're not looking so DRY after all. This is when another pattern comes into play: creating a function that returns another function with certain variables already baked in (that is, creating a closure). We'll execute this creator function and then use the function it returns in place of the anonymous function we were using previously when we bound to the click event.

// handleClick accepts a config object
// and a makeSelector function; it returns
// a function that can be bound to 
// a click event, using the config object
// and the makeSelector function to react
// appropriately to the click
var handleClick = function(fields, config, makeSelector) {
        return function() {
                var checked = this.checked;

                fields && $.each(fields, function(i, v) {
                        // build the selector using the provided
                        // makeSelector function
                        $(makeSelector(v))
                                // set the value using the
                                // config object, depending
                                // on whether the checkbox
                                // is checked
                                .val(checked ? config[v]: '');
                });
        };
};

$('#myCheckbox').click(
        // use handleClick to create a function
        // that has these variables baked in;
        // pass the created function as the
        // click handling function
        handleClick(
                ['foo','bar','baz'],
                myCheckboxConfig, 
                function(field) {
                        return '#field_' + field;
                }
        )
);

$('#myOtherCheckbox').click(
        handleClick(
                ['bim','bar','bop'],
                myOtherCheckboxConfig, 
                function(field) {
                        return 'input[name="' + field + '"]';
                }
        )
);

By creating a function that returns a function, we can isolate what's different about the two different handlers while centralizing the pieces that are the same. If your event handling function was slightly less trivial than this one, or if you were binding to five different checkboxes instead of two, the benefit of consolidating the code would be even more substantial. JavaScript offers plenty of patterns for writing DRY-er code; it's important to learn to both recognize and use them. It's also important to recognize when you're writing un-DRY code in the first place -- copying and pasting code is one crystal-clear indicator, but others are more subtle and you may not identify them on the first go-round. For example, take these two functions; each receives a list item as its only argument and returns either the next or previous list item, returning to the beginning or end of the list if there is no next or previous item.

var getNextItem = function($item) {
        return $item.next().length ? 
                $item.next() : $items.first();
};

var getPrevItem = function($item) {
        return $item.prev().length ?
                $item.prev() : $items.last();
};

This felt repetitive when I first wrote it, but I couldn't quickly come up with a single function that would work. A little thinking about it, though, led me to this single function which gets a second argument: direction. That argument is used to decide whether to run the item's next or previous method, and then whether to run the item's first or last method. Besides combining two functions into one, it also eliminates calling next or prev twice inside each function.

var getItem = function($item, direction) {
        var $returnItem = $item[direction]();
        return $returnItem.length ? 
                $returnItem : 
                $items[(direction == 'next') ? 'first' : 'last']();
};

Learning about patterns and then discovering opportunities to use them is one of the more pleasing parts of my job. I hope this helps you identify some of those opportunities for yourself :)

Filed under  //  front-end development   howto   javascript  
Comments (24)
Posted

Announcing jQuery Fundamentals: An Open-Source jQuery Training Curriculum

Posted

I've been leading jQuery trainings for more than a year now, from tiny gatherings that I organized myself at the local coworking space, to intensive two-day sessions at local web companies, to whirlwind one-day classes at governmental agencies. Over the course of those trainings, I've developed what I'd like to think is a decent curriculum -- training material that's the size of a small book, exercises that demonstrate core concepts, and solutions to those exercises that students can peek at later or when they get stuck. I decided recently that it was time for all of this material to see the light of day, so I spent the last several days converting it all to DocBook files that allow for easy publication to HTML and PDF (and other formats, if I'm later so inclined). I also fleshed out some topics that I'd given short shrift, and started planning sections covering advanced topics such as plugin authoring, code organization, best practices, and more. There's more to come in the next few days, but I think what I've done so far is worth sharing. I hope you'll agree.


Media_httpgyazocoma21_qfifz


My goals in releasing this are several. First and foremost, I want to see people writing better jQuery. The free resources for learning jQuery are scattered across the internets, and my personal experience of learning the library was haphazard — it was a long time before I learned some things I wish I'd known from the get-go. In addition, I want people who are writing jQuery to understand JavaScript. To that end, the book begins with a survey of JavaScript itself before jumping into jQuery. Finally, I want to enlist the bright minds of the jQuery community to help developing a robust, authoritative, in-depth jQuery curriculum, and in exchange it only seemed fair to make it available to everyone. I should mention that the goal of this material is to serve as a companion to a human instructor. That said, individuals may find it useful for self-study, especially if they're diligent about doing the exercises at the end of each chapter. If you're inclined to help -- by adding a chapter, a section, a paragraph, an exercise, or even just a correction -- fork the repo and send me a pull request. I look forward to seeing how this project might evolve with the community's help. Note: If you comment on this post pointing out an issue with the material, I will do my best to tend to the issue, but I probably won't publish your comment, as this post isn't the right place for reporting issues in the code. You can report issues at the repository, but if it's important to you, please fork the repository, make the change, and send me a pull request.

Filed under  //  front-end development   howto   javascript   jquery   jquery fundamentals   training  
Comments (37)
Posted

jQuery: Storing and retrieving data related to elements

Posted

It’s very common to need to get information about a DOM element when a user interacts with it — for example, perhaps you have an unordered list of names, and when a user clicks on a name, you want to show a picture of the person above the list. In order to do this, you need to figure out which person the clicked list item represents. Many beginning jQuery users will attempt to achieve this by putting ID attributes on each list item, such as id="rebecca". Then, they’ll read the ID attribute off the clicked element and use it to build a URL for the related image.

<ul class="people">
<li id="paul">Paul</li>
<li id="rebecca">Rebecca</li>
<li id="alex">Alex</li>
<li id="adam">Adam</li>
</ul>


var portrait = $('#portrait');

$('ul.people li').click(function() { 
    var name = $(this).attr('id');
    portrait.html('<img class="posterous_download_image" src="/images/people/'%20+%20name%20+%20'.jpg" alt="" />');
});

Strictly speaking, this will work. But is the ID really the right place to store this information? What if you need this behavior on other elements on the page too? You can’t have more than one element with the same ID on the page, so you might find yourself using funny prefixes in your IDs, like person_rebecca, and then stripping out the prefix. You could do it with classes, but then you’d have the opposite problem: you’re using (generally) unique classes like rebecca, but really classes are meant to indicate similarities among a set of elements. And what if you need to store more than one piece of information about an element on the element? Next thing you know you’ve got id="person_alex_red" and you’re jumping through all sorts of hoops to parse out the data you need.

Custom Data Attributes

HTML5 makes available [custom data attributes](http://dev.w3.org/html5/spec/Overview.html#custom-data-attribute, and they prove to be a much more elegant and robust solution to this problem. They’re custom, so they can contain pretty much anything you want, and each element can have as many of them as you want.

<ul class="people">
  <li>Paul</li>
  <li>Rebecca</li>
  <li>Alex</li>
  <li>Adam</li>
</ul>


var $portrait = $('#portrait');

$('ul.people li').click(function() { 
    var $li = $(this),
        name = $li.attr('data-name'),       
        color = $li.attr('data-hairColor'),
        $img = $('<img class="posterous_download_image" src="/images/people/'%20+%20name%20+%20'.jpg" alt="" />');

    $portrait.append($img).css('border', '5px solid ' + color);
});

$.fn.data

When you want to embed information about an element in the HTML you send down from your server, custom data attributes offer a clear and easy solution. But what if you want to attach data to elements that you’ve added to the page using JavaScript? For example, you might have some data you fetched from your server via an Ajax request:

{
    "items" : [
        { "name" : "Paul", "image" : "paul", "hairColor" : "black" },
        { "name" : "Rebecca", "image" : "rebecca", "hairColor" : "brown" },
        { "name" : "Alex", "image" : "alex", "hairColor" : "red" },
        { "name" : "Adam", "image" : "adam", "hairColor" : "red" }
    ]
}

You’re going to iterate over the data to produce a structure much like the one above, but in this case, it doesn’t make sense to store the related data in markup, because you’ll just have to extract it again later. Instead, you can use the $.fn.data() method in jQuery to store the data using JavaScript instead of markup:

var $target = $('ul.people');

$.each(response.items, function(i, data) {
    $('<li/>', { html : data.name })        
      .data({  
        name : data.image,              
        hairColor: data.hairColor       
      })        
      .appendTo($target); 
});

Later, you can read the data off the element using the $.fn.data() method again, this time passing just the name of the key you’re after:

var $portrait = $('#portrait');

$('ul.people li').click(function() { 
    var $li = $(this),
        name = $li.data('name'),
        color = $li.data('hairColor'),
        $img = $('<img class="posterous_download_image" src="/images/people/'%20+%20name%20+%20'.jpg" alt="" />');

    $portrait.append($img).css('border', '5px solid ' + color);
});

Mixing the two methods

This is all well and good, but what if you have a list of people that was sent from the server using HTML and custom data attributes, and then you add elements to it later using JavaScript and store data on them using $.fn.data()? Now your data is stored on elements in two different ways, so how do you extract it reliably? One option is to handle both cases. First, you’ll switch to using jQuery’s delegate method for the event binding, so you don’t have to keep binding click handlers as you add list items to your list. Then, inside of your click handler, you’ll figure out where you can get your data from:

var $portrait = $('#portrait');

$('ul.people').delegate('li', 'click', function() { 
    var $li = $(this), 
        name = $li.attr('data-name'), 
        color, $img;

    if (!name) { // did the li have custom data attributes?
        name = $li.data('name');
        color = $li.data('hairColor');
    } else {
        color = $li.attr('data-hairColor');
    }

    $img = $('<img class="posterous_download_image" src="/images/people/'%20+%20name%20+%20'.jpg" alt="" />');
    $portrait.append($img).css('border', '5px solid ' + color);
});

Another option is to iterate over the original elements, and store the data from the custom data attributes using the $.fn.data() method:

var $portrait = $('#portrait'), 
      $ul = $('ul.people');

$ul.find('li').each(function() {
    var $li = $(this);
    $li.data({
        name : $li.attr('data-name'),
        hairColor : $li.attr('data-hairColor')
    });
});

$('ul.people').delegate('li', 'click', function() { 
    var $li = $(this), 
        name = $li.data('name'),
        color = $li.data('hairColor'),
        $img = $('<img class="posterous_download_image" src="/images/people/'%20+%20name%20+%20'.jpg" alt="" />');

    $portrait.append($img).css('border', '5px solid ' + color);
});

// load more list items via ajax at some point
// to make that whole delegate thing worthwhile

Which option you use will depend on how large your original list is (and thus how long it will take to iterate over it), and how likely people are to click on a lot of items in the list (and thus whether the initial iteration is worth the time). I leave it as an exercise for the reader to decide which approach makes sense for you.

In Conclusion

When you need to attach data to elements and then extract that data later, there are options beyond classes and IDs, and in fact classes and IDs may be an especially poor way to approach the problem. Taking advantage of custom data attributes and the $.fn.data() method in jQuery can make it painless to store and retrieve data related to elements, and the metadata plugin can streamline the process for you even further.

Further Reading

Filed under  //  data   howto   html5   javascript   jquery  
Comments (8)
Posted

TXJS: JS + BBQ + You

Posted

If you follow me on Twitter, you've heard about TXJS by now, but let me tell you: now that we've announced Douglas Crockford and John Resig, there is officially no way you should miss this. TXJS is a full-day conference set for June 5 in Austin, Texas, hosted by the yayQuery team and featuring BBQ and a slew of awesome JavaScripters:

  • Brandon Aaron, jQuery team member
  • Tim Caswell, Node.js contributor and founder of howtonode.org
  • Douglas Crockford, JavaScript architect at Yahoo! and author of "JavaScript: The Good Parts"
  • Andrew Dupont, Prototype core team member
  • Peter Higgins, Dojo project lead
  • Paul Irish, jQuery team member and yayQuery podcast co-host
  • Brian LeRoux, software architect at Nitobi and PhoneGap hacker/contributor
  • Joe McCann, senior technologist at Frog Design
  • John Resig, creator of the jQuery JavaScript library
  • Alex Sexton, front-end developer and yayQuery podcast co-host
  • Kyle Simpson, Austin JS organizer and author of LABjs
  • Adam Sontag, jQuery UI developer relations team member and yayQuery podcast co-host
  • Mike Taylor, HTML5 aficionado and front-end developer at Tunecore
  • Juriy Zaytsev (aka @kangax), expert JavaScript wrangler and Prototype core developer
Earlybird tickets are just $49 while they last, and regular tickets are just $69, which is about as close to free as we could get. See you there!

Filed under  //  javascript   txjs  
Comment (1)
Posted

srchr: Crowdsourcing JavaScript wisdom

Posted

UPDATE: The deadline for completing your submission is April 16, the day before JSConf. If you're at the conference, join others in the hacker lounge to see what they did! I've been working on a blog post about using classes and pub/sub for structuring jQuery applications, and I had in mind a pretty simple demo app that I was going to build. I also wanted to show a version of the app that was built in a more traditional way, and I'd been pondering whether I should write that version myself, or see if I could cajole someone else into doing it. And then, a moment of inspiration: rather than a contrived counter-example, why not get a whole bunch of developers to show how they'd tackle the problem, so we can all gain from the exercise and learn from each other? I tweeted my idea, and five minutes later I had a dozen volunteers and counting, which is downright awesome and in hindsight shouldn't be surprising. It's so rare that we get to see multiple approaches to a moderately complex problem -- it's much more common to see horrendous code and bitch about it :)

The project

I've put together a mock/spec for a small, strictly client-side application that uses YQL to search for content and then displays it to the user. (Click on the image to see it full-size.)
Media_httpblogrebecca_cudcn
Think of this as an exercise in creating a product, not a site that you finish and walk away from -- the goal is to create an extensible, modular application. That said, there are no "right" answers here: the point is for you to demonstrate how you, personally, would approach the problem.

Presenting your solution

I've created a github repository for the project that contains nothing more than some documentation, the mock/spec, and a few stub files and directories. You should fork this repository to get started. If you create some CSS that you'd like to share, I'd encourage you to send a pull request so I can make it available to everyone; this isn't an CSS exercise, so no one should labor over that part if they don't want to. I may very well write some basic CSS myself in the next couple of days, but it's late :) Finally: please comment on this post if you have any questions!

Filed under  //  front-end development   javascript   jquery  
Comments (18)
Posted

Building a standalone JavaScriptMVC controller.js

Posted

I've been doing a lot of work lately with a client who's using just the controller portion of JavaScriptMVC. I plan to write a more in-depth post about this while I'm traveling this week, but in the meantime, I wanted to jot down the steps to create the standalone controller.js file for my future reference. These instructions work for me on OSX 10.6.

  1. Go to the JavaScriptMVC downloads page on Github
  2. Download the latest version (3.0.0a0 as of this writing) of the framework and unzip it
  3. Open a terminal window and cd to the directory created when you unzipped the file
  4. Run steal/js steal/compress/plugin.js jquery/controller
  5. You should now have a new controller.js file in the directory; you're all set!

Filed under  //  howto   javascript   javascriptmvc   jquery  
Comment (1)
Posted

Demystifying custom events in jQuery

Posted

This article originally appeared in the May 2009 issue of JSMag. We’re all familiar with the basic events — click, mouseover, focus, blur, submit, etc. — that we can latch on to as a user interacts with the browser. Custom events open up a whole new world of event-driven programming. In this article, we’ll use jQuery’s custom events system to make a simple Twitter search application, but the general concepts should apply to any framework that supports custom events. I confess: it took me a long time to decide to learn about custom events. The built-in events seemed to suit my needs just fine, and it was difficult to understand why I’d want to start adding my own. Boy, was I missing out. It turns out that custom events offer a whole new way of thinking about event-driven JavaScript. Instead of focusing on the element that triggers an action, custom events put the spotlight on the element being acted upon. This brings a bevy of benefits, including:

  • behaviors of the target element can easily be triggered by different elements using the same code;
  • behaviors can be triggered across multiple, similar, target elements at once; and
  • behaviors are more clearly associated with the target element in code, making code easier to read and maintain
Why should you care? An example is probably the best way to explain. Suppose you have a lightbulb in a room in a house. The lightbulb is currently turned on, and it’s controlled by two three-way switches and a clapper, as shown here:
Triggering the clapper or either of the switches will change the state of the lightbulb. The switches and the clapper don’t care what state the lightbulb is in; they just want to change the state. Without custom events, you might write some code like this:
$('.switch, .clapper').click(function() {
        var $light = $(this).parent().find('.lightbulb');
        if ($light.hasClass('on')) {
                $light.removeClass('on').addClass('off');
        } else {
                $light.removeClass('off').addClass('on');
        }
});
With custom events, your code might look more like this:
$('.lightbulb').bind('changeState', function(e) {
        var $light = $(this);
        if ($light.hasClass('on')) {
                $light.removeClass('on').addClass('off');
        } else {
                $light.removeClass('off').addClass('on');
        }
});
$('.switch, .clapper').click(function() { 
        $(this).parent().find('.lightbulb').trigger('changeState');
});
This last bit of code is not that exciting, but something important has happened: we’ve moved the behavior of the lightbulb to the lightbulb, and away from the switches and the clapper. Let’s make our example a little more interesting. We’ll add another room to our house, along with a master switch, as shown here:
If there are any lights on in the house, we want the master switch to turn all the lights off; otherwise, we want it to turn all lights on. To accomplish this, we’ll add two more custom events to the lightbulbs: turnOn and turnOff. We’ll make use of them in the changeState custom event, and use some logic to decide which one the master switch should trigger:
$('.lightbulb').
bind('changeState', function(e) {
        var $light = $(this);
        if ($light.hasClass('on')) {
                $light.trigger('turnOff');
        } else {
                $light.trigger('turnOn');
        }
}).
bind('turnOn', function(e) {
        $(this).removeClass('off').addClass('on');
}).
bind('turnOff', function(e) {
        $(this).removeClass('off').addClass('on');
});

$('.switch, .clapper').click(function() { 
        $(this).parent().find('.lightbulb').trigger('changeState');
});

$('#master_switch').click(function() {
        if ($('.lightbulb.on').length) {
                $('.lightbulb').trigger('turnOff');
        } else {
                $('.lightbulb').trigger('turnOn');
        }
});
A bit more interesting, huh? Note how the rules about what the master switch does belongs to the master switch; the rules about how a lightbulb turns on and off belong to the lightbulb. It’s also worth noting that the master switch is able to affect all of the lightbulbs without having to iterate over them — it just triggers an event on all elements that have the class “lightbulb”. This isn’t critical to the example, but if we were to perform more elaborate manipulations of each lightbulb than adding and removing a class, this would be an important benefit of custom events, as you’ll see in the rest of the article. (If you’re accustomed to object-oriented programming, you may find it useful to think of custom events as methods of objects. Loosely speaking, the object to which the method belongs is created via the jQuery selector. Binding the changeState custom event to all $('.light') elements is akin to having a class called Light with a method of changeState(), and then instantiating new Light objects for each element with a classname of light.)

jQuery Event Primer

Before we dive in, a couple of things we need to recap. In the world of custom events, there are two important jQuery methods: .bind() and .trigger(). I encourage you to read the jQuery docs for details, but basically:
  • The .bind() method takes an event type and an event handling function as arguments. Optionally, it can also receive data, which will be available to the event handling function in the data property of the event object. The event handling function always receives the event object as its first argument.
  • The .trigger() method takes an event type as its argument. Optionally, it can also take an array of values. The first item in the array will be the second argument passed to the event handling function (after the event object).
Confused? I don’t blame you. Read on and I’ll try to clear some things up.

Our Mission

To demonstrate the power of custom events, we’re going to create a simple tool for searching Twitter. The tool will offer several ways for a user to add search terms to the display: by entering a search term in a text box, by entering multiple search terms in the URL, and by querying Twitter for trending terms. The results for each term will be shown in a results container; these containers will be able to be expanded, collapsed, refreshed, and removed, either individually or all at once. When we’re done, it will look like this:

The Setup

We’ll start with some basic HTML:
Twitter Search

Search Results for

This gives us a container (#twitter) for our widget, a template for our results containers (hidden via CSS), and a simple form where users can input a search term. (For the sake of simplicity, we’re going to assume that our application is JavaScript-only and that our users will always have CSS.) First, the setup. We’ll build an $actions object that we’ll use later to create the buttons in each results container, and we’ll also create a global search_terms object so we can store a list of search terms that are being displayed on the page. Next, we’ll do our custom event binding. There are two types of objects we’ll want to act on: the results containers, and the Twitter container.

The Results Containers

The results containers are the heart of the application. We’ll create a setupResults() plugin that will prepare each results container once it’s added to the Twitter container. Among other things, it will bind the custom events for each container and add the action buttons at the top right of each container. Each results container will have the following custom events:
  • The refresh event will mark the container as being in the “refreshing” state, and fire the $.getJSON() request to fetch the data for the search term.
  • The populate event will receive the returned JSON data and use it to populate the container.
  • The remove event will remove the container from the page after the user verifies the request to do so. Verification can be bypassed by passing true as the second argument to the event handler. The remove event also removes the term associated with the results container from the global search_terms object.
  • The collapse event will add a class of collapsed to the container, which will hide the results via CSS. It will also turn the container’s “Collapse” button into an “Expand” button.
  • The expand event will remove the collapsed class from the container. It will also turn the container’s “Expand” button into a “Collapse” button.
The plugin is also responsible for adding the action buttons to the container by cloning the $actions object that was created earlier. It binds a click event to each action’s list item, and uses the list item’s class to determine which custom event will be triggered on the corresponding results container.
// we'll use this every time we add a new results panel,
// so let's build it once and cache it in $actions
var $actions = $('
    '); $('
  • Refresh
  • ').appendTo($actions); $('
  • Remove
  • ').appendTo($actions); $('
  • Collapse
  • ').appendTo($actions); // this is where we'll keep track of which search terms // are shown on the page already var search_terms = {}; Here's the setupResults plugin:
    $.fn.setupResults = function(settings) {
            return $(this).each(function() {
                    
                    var $results = $(this);
                    var $actions = settings.actions;
                    var term = settings.term;
    
                    // change the "Search results for" text
                    $results.find('span.search_term').text(term);
    
                    // bind custom events for results box
                    $results. 
            
                    // the "refresh" event fetches 
                    // the latest content for the term
                    bind('refresh', function(e) {
                            // indicate that the results are refreshing
                            var $this = $(this).addClass('refreshing');
    
                            $this.find('p.tweet').remove();
                            $results.append('Loading ...');
    
                            // get the twitter data using jsonp
                            $.getJSON(
                                    'http://search.twitter.com/search.json?q=' +                                         escape(term) + '&rpp=5&callback=?', 
                                    function(json) { 
                                            $this.trigger('populate', [ json ]); 
                                    }
                            );
                    }).
                    
                    // the "populate" event takes results 
                    // in json format 
                    // and populates the results container
                    bind('populate', function(e, json) {
                            var results = json.results;
                            var $this = $(this);
                            
                            $this.find('p.loading').remove();
    
                            $.each(results, function(i,result) {
                                    var tweet = '' + 
                                            '' +
                                            result.from_user + 
                                            ': ' +
                                            result.text + 
                                            ' ' + 
                                            result.created_at + 
                                            '' +
                                    '';
                                    $this.append(tweet);
                                            });
    
                                            // indicate that the results 
                                            // are done refreshing
                                            $this.removeClass('refreshing');
                                    }).
                                                    // the remove event removes 
                                                    // the results from the page
                                                    // after the user confirms the action
                                                    bind('remove', function(e, force) {
                                                            // allow forced removal without confirmation
                                                            if (
                                                                    !force &&         
                                                                    !confirm('Remove panel for term ' + term + '?')
                                                            ) {
                                                                    return;
                                                            }
                                                            $(this).remove();
    
                                                            // indicate that we no longer 
                                                            // have a panel for the term
                                                            search_terms[term] = 0;
                                                    }).
    
                                                    // the collapse event collapses the results so only the
                                                    // header of the results section is showing
                                                    bind('collapse', function(e) {
                                                            $(this).find('li.collapse').removeClass('collapse')
                                                                    .addClass('expand').text('Expand');
    
                                                            $(this).addClass('collapsed');
                                                    }).
    
                                                    // the expand event
                                                    bind('expand', function(e) {
                                                            $(this).find('li.expand').removeClass('expand')
                                                                    .addClass('collapse').text('Collapse');
    
                                                            $(this).removeClass('collapsed');
                                                    });
    
                                                    if ($actions && $actions.length) {
                                                            // add a clone of $actions to the results panel
                                                            var $a = $actions.clone().prependTo($results);
    
                                                            // use the class of each action to figure out 
                                                            // which event it will trigger on the results panel
                                                            $a.find('li').click(function() {
                                                                    // pass the li that was clicked to the function
                                                                    // so it can be manipulated if needed
                                                                    $results.trigger(
                                                                            $(this).attr('class'), [ $(this) ]
                                                                    );
                                                            });
                                                    }
                                            });
                                    };

    The Twitter Container

    The Twitter container itself will have just two custom events:
    • The getResults event will receive a search term. It will check the global search_terms object to determine whether there’s already a results container for the term; if not, it will add a results container using the results template (div.template), set up the results container using the setupResults() plugin discussed above, and then trigger the refresh event on the results container in order to actually load the results. Finally, it will store the search term in the global search_terms object, so the application knows not to re-fetch the term.
    • The getTrends event will query Twitter for the top 10 trending terms. It will iterate over them and trigger the widget’s getResults event for each of them, thereby adding a results container for each term. Here you can see how we go about passing data to a triggered event.
    The Twitter container bindings are shown here:
    $('#twitter').
    bind('getResults', function(e, term) {
            // make sure we don't have a box for this term already
            if (!search_terms[term]) { 
                    var $this = $(this);
                    var $template = $this.find('div.template');
    
                    // make a copy of the template div
                    // and insert it as the first results box
                    $results = $template.clone().
                            removeClass('template').
                            insertBefore($this.find('div:first')).
                            setupResults({
                                    'term' : term,
                                    'actions' : $actions
                            });
    
                    // load the content using the "refresh" 
                    // custom event that we bound to the results container
                    $results.trigger('refresh');
                    search_terms[term] = 1;
            }
    }).
    
    bind('getTrends', function(e) {
            var $this = $(this);
            $.getJSON('http://search.twitter.com/trends.json?callback=?',                 function(json) {
                            var trends = json.trends; 
                            $.each(trends, function(i, trend) {
                                    $this.trigger('getResults', [ trend.name ]);
                            });
                    });
    });
    So far, we’ve written a lot of code that does approximately nothing, but that’s OK. By specifying all the behaviors that we want our core objects to have, we’ve created a solid framework for rapidly building out the interface. Let’s start by hooking up our text input and the “Load Trending Terms” button. For the text input, we’ll make the form submission stop in its tracks using e.preventDefault(), then capture the term that was entered in the input and pass it to the Twitter widget’s getResults event. (Again, you can see how we go about passing data to a triggered event.) Clicking the “Load Trending Terms” will simply trigger the Twitter widget’s getTrends event:
    $(document).ready(function() {
    
    $('form').submit(function(e) {
            e.preventDefault();
            var term = $('#search_term').val();
            $('#twitter').trigger('getResults', [ term ]);
    });
    
    $('#get_trends').click(function() {
            $('#twitter').trigger('getTrends'); 
    });
    
    });
    Just entering a search term into a text box is boring, of course — the following code shows how we can capture search terms from the URL’s hash (e.g., http://foo.com/index.html#foo,bar):
    $(document).ready(function() {
    
    // pass search terms via URL hash 
    if (document.location.hash) {
            var terms = document.location.hash.split(',').reverse();
            $.each(terms, function(i,term) {
                    $('#twitter').trigger('getResults', [ term ]);
            });
    }
    
    });
    By adding a few buttons with the appropriate IDs, we can make it possible to remove, collapse, expand, and refresh all results containers at once, as shown below. For the remove button, note how we’re passing a value of true to the event handler as its second argument, telling the event handler that we don’t want to verify the removal of individual containers.
    $(document).ready(function() {
    
    $('#refresh').click(function(e) {
            $('#twitter div.results').trigger('refresh'); 
    });
    
    $('#expand').click(function(e) {
            $('#twitter div.results').trigger('expand'); 
    });
    
    $('#collapse').click(function(e) {
            $('#twitter div.results').trigger('collapse'); 
    });
    
    $('#remove').click(function(e) {
            if (confirm('Remove all results?')) {
                    $('#twitter div.results').
                            trigger('remove', [ true ]);
            }
    });
    });
    It should be noted that you don’t have to choose one or all results containers — really, you can choose any results containers you want to affect. For example, if for some (strange) reason you wanted to refresh only the first and last results containers, you could do:
    $('div.results:first, div.results:last').
            trigger('refresh');
    You can see the entire application, including the full HTML and CSS, at http://www.rebeccamurphey.com/jsmag/custom-events/.

    Conclusion

    Custom events offer a new way of thinking about your code: they put the emphasis on the target of a behavior, not on the element that triggers it. If you take the time at the outset to spell out the pieces of your application, as well as the behaviors those pieces need to exhibit, custom events can provide a powerful way for you to “talk” to those pieces, either one at a time or en masse. Once the behaviors of a piece have been described, it becomes trivial to trigger those behaviors from anywhere, allowing for rapid creation of and experimentation with interface options. Finally, custom events can enhance code readability and maintainability, by making clear the relationship between an element and its behaviors.

    Learn More

    Filed under  //  howto   javascript   jquery  
    Comments (7)
    Posted