diff --git a/page/events/introduction-to-custom-events.md b/page/events/introduction-to-custom-events.md index 72287e43..c05ff497 100644 --- a/page/events/introduction-to-custom-events.md +++ b/page/events/introduction-to-custom-events.md @@ -7,7 +7,7 @@ ## Custom Events -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 chapter, we'll use jQuery's custom events system to make a simple Twitter search application. +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. It can be difficult at first to understand why you'd want to use custom events, when the built-in events seem to suit your needs just fine. 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: @@ -32,8 +32,8 @@ Without custom events, you might write some code like this: ``` $( ".switch, .clapper" ).click(function() { - var light = $( this ).parent().find( ".lightbulb" ); - if ( light.hasClass( "on" ) ) { + var light = $( this ).closest( ".room" ).find( ".lightbulb" ); + if ( light.is( ".on" ) ) { light.removeClass( "on" ).addClass( "off" ); } else { light.removeClass( "off" ).addClass( "on" ); @@ -44,9 +44,9 @@ $( ".switch, .clapper" ).click(function() { With custom events, your code might look more like this: ``` -$( ".lightbulb" ).on( "changeState", function( e ) { +$( ".lightbulb" ).on( "light:toggle", function( event ) { var light = $( this ); - if ( light.hasClass( "on" ) ) { + if ( light.is( ".on" ) ) { light.removeClass( "on" ).addClass( "off" ); } else { light.removeClass( "off" ).addClass( "on" ); @@ -54,7 +54,8 @@ $( ".lightbulb" ).on( "changeState", function( e ) { }); $( ".switch, .clapper" ).click(function() { - $( this ).parent().find( ".lightbulb" ).trigger( "changeState" ); + var room = $( this ).closest( ".room" ); + room.find( ".lightbulb" ).trigger( "light:toggle" ); }); ``` @@ -78,38 +79,44 @@ Let's make our example a little more interesting. We'll add another room to our
``` -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: +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: `light:on` and `light:off`. We'll make use of them in the `light:toggle` custom event, and use some logic to decide which one the master switch should trigger: ``` -$( ".lightbulb" ).on( "changeState", function( e ) { +$( ".lightbulb" ).on( "light:toggle", function( event ) { var light = $( this ); - if ( light.hasClass( "on" ) ) { - light.trigger( "turnOff" ); + if ( light.is( ".on" ) ) { + light.trigger( "light:off" ); } else { - light.trigger( "turnOn" ); + light.trigger( "light:on" ); } -}).on( "turnOn", function( e ) { +}).on( "light:on", function( event ) { $( this ).removeClass( "off" ).addClass( "on" ); -}).on( "turnOff", function( e ) { +}).on( "light:off", function( event ) { $( this ).removeClass( "on" ).addClass( "off" ); }); $( ".switch, .clapper" ).click(function() { - $( this ).parent().find( ".lightbulb" ).trigger( "changeState" ); + var room = $( this ).closest( ".room" ); + room.find( ".lightbulb" ).trigger( "light:toggle" ); }); $( "#master_switch" ).click(function() { - if ( $( ".lightbulb.on" ).length ) { - $( ".lightbulb" ).trigger( "turnOff" ); + var lightbulbs = $( ".lightbulb" ); + + // Check if any lightbulbs are on + if ( lightbulbs.is( ".on" ) ) { + lightbulbs.trigger( "light:off" ); } else { - $( ".lightbulb" ).trigger( "turnOn" ); + lightbulbs.trigger( "light:on" ); } }); ``` Note how the behavior of the master switch is attached to the master switch; the behavior of a lightbulb belongs to the lightbulbs. -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`. +### Naming Custom Events + +You can use any name for a custom event, however you should beware of creating new events with names that might be used by future DOM events. For this reason, in this article we have chosen to use `light:` for all of our event names, as events with colons are unlikely to be used by a future DOM spec. ### Recap: `.on()` and `.trigger()` @@ -133,231 +140,6 @@ $( document ).on( "myCustomEvent", { $( document ).trigger( "myCustomEvent", [ "bim", "baz" ] ); ``` -### A Sample Application - -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: - - - -``` -Loading...
" ); - - // Get the twitter data using jsonp - $.getJSON( "http://search.twitter.com/search.json?q=" + escape( e.data.term ) + "&rpp=5&callback=?", function( json ) { - elem.trigger( "populate", [ json ] ); - }); - }, - - populate: function( e, json ) { - var results = json.results; - var elem = $( this ); - - elem.find( "p.loading" ).remove(); - $.each( results, function( i, result ) { - var tweet = "" + - "" + - result.from_user + - ": " + - result.text + - " " + - result.created_at + - "" + - "
"; - - elem.append( tweet ); - }); - - // Indicate that the results are done refreshing - elem.removeClass("refreshing"); - }, - - remove: function( e, force ) { - if ( !force && !confirm( "Remove panel for term " + e.data.term + "?" ) ) { - return; - } - $( this ).remove(); - - // Indicate that we no longer have a panel for the term - search_terms[ e.data.term ] = 0; - }, - - collapse: function( e ) { - $( this ).find( "li.collapse" ) - .removeClass( "collapse" ) - .addClass( "expand" ) - .text( "Expand" ); - - $( this ).addClass( "collapsed" ); - }, - - expand: function( e ) { - $( this ).find( "li.expand" ) - .removeClass( "expand" ) - .addClass( "collapse" ) - .text( "Collapse" ); - - $( this ).removeClass( "collapsed" ); - } -}; -``` - -The Twitter container itself will have just two custom events: - -* `getResults` — Receives a search term and checks to determine whether there's already a results container for the term; if not, adds a results container using the results template, set up the results container using the `$.fn.twitterResult` plugin discussed above, and then triggers the `refresh` event on the results container in order to actually load the results. Finally, it will store the search term so the application knows not to re-fetch the term. - -* `getTrends` — Queries Twitter for the top 10 trending terms, then iterates over them and triggers the `getResults` event for each of them, thereby adding a results container for each term. - -Here's how the Twitter container bindings look: - -``` -$( "#twitter" ).on( "getResults", function( e, term ) { - - // Make sure we don't have a box for this term already - if ( !search_terms[ term ] ) { - var elem = $( this ); - var template = elem.find( "div.template" ); - - // Make a copy of the template div - // and insert it as the first results box - results = template.clone() - .removeClass( "template" ) - .insertBefore( elem.find( "div:first" ) ) - .twitterResult({ - "term": term - }); - - // Load the content using the "refresh" - // custom event that we bound to the results container - results.trigger( "refresh" ); - - search_terms[ term ] = 1; - } -}).on( "getTrends", function( e ) { - var elem = $( this ); - - $.getJSON( "http://search.twitter.com/trends.json?callback=?", function( json ) { - var trends = json.trends; - $.each( trends, function( i, trend ) { - elem.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 capture the term that was entered in the input and pass it as we trigger the Twitter container's `getResults` event. Clicking the "Load Trending Terms" will trigger the Twitter container's `getTrends` event: - -``` -$( "form" ).submit(function( event ) { - var term = $( "#search_term" ).val(); - $( "#twitter" ).trigger( "getResults", [ term ] ); - event.preventDefault(); -}); - -$( "#get_trends" ).click(function() { - $( "#twitter" ).trigger( "getTrends" ); -}); -``` - -By adding a few buttons with the appropriate ID's, 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. - -``` -$.each([ "refresh", "expand", "collapse" ], function( i, ev ) { - $( "#" + ev ).click( function( e ) { - $( "#twitter div.results" ).trigger( ev ); - }); -}); - -$( "#remove" ).click(function( e ) { - if ( confirm( "Remove all results?" ) ) { - $( "#twitter div.results" ).trigger( "remove", [ true ] ); - } -}); -``` - ### 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.