Do you find yourself in an unmaintainable mess where there’s DOM manipulation calls in every part of the code? Would it not be easier to simply set an attribute f.ex. data-progress=”50″ and then see the elements change automatically (seemingly by magic)? This way you can seperate concerns (calculation vs presentation) and your code will be easier to maintain / less brain overhead.
First of all, you need a way to detect attribute changes. In this article I’m going to use a jQuery plugin for simplicity, but the concept is platform independent.
Prequisites for this example
Down to business
Consider the following example; here we want to register whenever a div item has been clicked. We also want to keep a tally of the number of clicked items and use HTML/CSS to show which and how many items have been clicked. A reset button resets all the clicks.
The HTML/CSS looks like this:
<style type="text/css"> /* We want to show a different background color on the container based on the number of clicked items (no clue why!) */ #container[data-clicked-items="1"] #list { background: grey; } #container[data-clicked-items="3"] #list { background: blue; } #container[data-clicked-items="5"] #list { background: green; } .clicked-marker { display: none; } [data-clicked] .clicked-marker { display: inline; } </style> <!-- Container --> <div id="container" data-clicked-items="0"> <!-- Counter --> <p> Clicked items <span class="clicked-item-counter">0</span> </p> <!-- Items --> <div id="list"> <div class="item"> Item #1 <span class="clicked-marker">- Clicked</span> </div> <div class="item"> Item #2 <span class="clicked-marker">- Clicked</span> </div> <div class="item"> Item #3 <span class="clicked-marker">- Clicked</span> </div> <div class="item"> Item #4 <span class="clicked-marker">- Clicked</span> </div> <div class="item"> Item #5 <span class="clicked-marker">- Clicked</span> </div> </div> <!-- Reset button --> <p> <button type="button" class="reset-clicked-items-button">Reset clicks</button> </p> </div>
Now we could go ahead and solve it straight on the nose. We would then do something like this:
jQuery(function($) { // All of this crap happens when you click an item $( '.item' ).click(function() { var $item = $( this ) // Only process if the item has not yet been clicked if ( $item.attr( 'data-clicked' ) === undefined ) { // Mark item as clicked $item.attr( 'data-clicked', 'true' ) // Calculate number of clicked items var numClickedSiblings = $item.siblings( '[data-clicked]' ).length var numClickedItems = numClickedSiblings + 1 // Update total clicked items in container var $container = $( '#container' ) $container.attr( 'data-clicked-items', numClickedItems ) // Update total items counter var $counter = $( '.clicked-item-counter' ) $counter.text( numClickedItems ) } }) // When the user clicks the reset button we'd want to revert the above changes.. $( '.reset-clicked-items-button' ).click(function() { // Oh shit. What do we do here? // Let's see. Remove all data-clicked attributes var $items = $( '.item' ) $items.removeAttr( 'data-clicked' ) // We done yet? Hm, not quite var $container = $( '#container' ) $container.attr( 'data-clicked-items', 0 ) // Now? Ah, the counter.. sec. var $counter = $( '.clicked-item-counter' ) $counter.text( 0 ) // Perfect! Or? // Look at all the duplicated code and all the dependencies. It's awful really. }) })
Now let’s solve it using attribute change events (again, here we’re using the attrchange jQuery plugin).
jQuery(function($) { // Notice how the "top level" events are much simpler now $( '.item' ).click(function() { var $item = $( this ) // Only process if the item has not yet been clicked if ( $item.attr( 'data-clicked' ) === undefined ) { // Mark item as clicked $item.attr( 'data-clicked', 'true' ) } }) $( '.reset-clicked-items-button' ).click(function() { // Remove all data-clicked attributes var $items = $( '.item' ) $items.removeAttr( 'data-clicked' ) }) // The magic. Register the attribute change event handlers $( '.item' ).attrchange({ trackValues: true, callback: function(ev) { // The attribute data-clicked has been changed if ( ev.attributeName === 'data-clicked' ) { var $item = $( this ) var numClickedSiblings = $item.siblings( '[data-clicked]' ).length var numClickedItems = numClickedSiblings if ( ev.newValue ) { // The data-clicked attribute has been set so increment the number of items clicked numClickedItems++ } // Update total clicked items in container var $container = $( '#container' ) $container.attr( 'data-clicked-items', numClickedItems ) } } }) $( '#container' ).attrchange({ trackValues: true, callback: function(ev) { // The data-clicked-items attribute has been changed if ( ev.attributeName === 'data-clicked-items' ) { // The data-clicked-items attribute has changed, so update the number in DOM var $counter = $( '.clicked-item-counter' ) $counter.text( ev.newValue ) } } }) })
So as you can see by listening to attribute changes we are able to connect the dependencies in a more logical way.
Instead of listening to a click event and doing everything in one function, we rather change attributes (which we have to do anyway in order for the application to remember the current state) that are attached event handlers. These event handlers has the responsibility of handling everything related to the respective attributes like updating the DOM and even updating other attributes.
In the second example on the top level we are simply modifying the data-clicked attribute on the div items. This makes sense as we have to know which items are clicked or not. When modifying the data-clicked attribute an event is triggered which calculates the new number of clicked items and sets it as an attribute on the container element.
This again triggers an attribute change event registered on the container element, more specifically via the data-clicked-items attribute. This event handler simply updates the total number of clicked items in the DOM. By doing it this way we always know the state (the value stored in the attribute) is in sync with the text content of the DOM element.
If you have any comments please feel free to state them in the comments field below.