UX Design and Development course

Manipulating the DOM

http://jsbin.com/wabaniza/6/edit

It's one thing to add some text and change colors, but let's get into the business of actually manipulating the DOM based on events in the UI. For example, let's have a button that when pressed displays a price.

To get this to work we need to append a new DOM node and remove the button. First our HTML:

<button>show me the money</button>

Then the jQuery:

$('button').click(function() {
  var price = "From $199";
  $('button').text(price);
});

What happened here?

  1. Grab the button element in the DOM
  2. Using the .click() method to pass the event into a function
  3. Set the content we want to display as a variable
  4. Using the button element in the DOM, we can append the .text() method and pass in the price var

This is all pretty cool, but what happens when we add more buttons? We are pretty much screwed.

To fix this, we need to use the this keyword. What is this? When using this in an event handler, you are targeting the context from the item that triggered the event.

$('button').click(function() {
  var price = "From $199";
  $(this).text(price);
});

BOOM! this fixed that ;)

Ok, so we have multiple buttons. But all the values are the same? To get the content that we will swap through in the DOM, we can use the data- attribute. The HTML could look something like the following:

<button data-money="199">show me the money</button>
<button data-money="599">show me the money</button>

Now we update our jQuery so that we set our price var from the data attribute using the .data() method. Last we can create our new text sting from that price var.

$('button').click(function() {
  var price = $(this).data('money');
  $(this).text('$' + price + '.00');
});

Now we have another problem. The way this is written, every button in the site will look for a data- attribute and will fire this function. Let's update our HTML to the following so that there are wrapping parent containers and add a third button.

<div class="pay-me">
  <button data-money="199">show me the money</button>
</div>

<div class="pay-you">
  <button data-money="599">show me the money</button>
</div>

<button>submit</button>

To update our jQuery we are going to change things a little bit.

Using event delegation, we are going to update our event handler so that it only looks for a button within a specified selector, like .pay-me. I also removed the click() event with the on() event and passed in click as an argument for the button selector.

$('.pay-me').on('click', 'button', function() {
  var price = $(this).data('money');
  $(this).text('$' + price + '.00');
});

Now, only the button within the .pay-me selector will fire. But we have another problem. This will only work with the .pay-me button? Hmmmmmm. To fix this, I vote for using a better selector, like so:

$('[class^="pay"]').on('click', 'button', function() {
  var price = $(this).data('money');
  var totalPrice = '$' + price + '.00';
  $(this).text(totalPrice);
});

Now, any button with the the class of .pay-[anything] will work.

Expanding on on()

The previous example will work, but using attribute selectors is a performance hit with CSS, I would also assume that's is not exactly the best solution. Going back to the original solution, we get:

$('.pay-me').on('click', 'button', function() {
  var price = $(this).data('money');
  var totalPrice = '$' + price + '.00';
  $(this).text(totalPrice);
});

To get this to work with .pay-you we have to duplicate code? Hmmmm, nope. jQuery gives us the option to wrap code into reusable functions. Let's create a new function called payMe using the code we want to reuse:

function payMe () {
  var price = $(this).data('money');
  var totalPrice = '$' + price + '.00';
  $(this).text(totalPrice);
}

Ok cool. Now to use this function, pass this into the third argument of the .on() event handler.

$('.pay-me').on('click', 'button', payMe);
$('.pay-you').on('click', 'button', payMe);

Notice how we didn't use a () with the payMe function. This is because we don't want the function to fire immediately, but only when the click event is fired.

This only goes one way

The above code is good and will work, but it only goes one way. Once you click the button, the text() function will blow away the text node in the DOM and replace with the new text. How can we do this better?

To make this better, we are going to include a few if statements and make good use of the data() method in jQuery.

In the DOM we have a node with the class of .pay-me. We are going to use that for starters. Grab .pay-me and assign the click event to the button. Use the same vars for price and totalPrice, but move all the text() methods into the if statements.

$('.pay-me').on('click', 'button', function() {
  var price = $(this).data('money');     // same var
  var totalPrice = '$' + price + '.00';  // same var

  // Get the current text object and compare to the totalPrice
  if ($(this).text() === totalPrice) {
    // if they are equal, then replace with the text stored
    // into the data() object
    $(this).text($(this).data('text'));
  } else {
    // else, store the current text into the text() object
    // and swap with totalPrice
    $(this).data('text', $(this).text());
    $(this).text(totalPrice);
  }
});

Running this code, the first button with the class of .pay-me will fire and the text should swap between the original text and the string created from the data attribute.

But ... man, this is filled with a lot of $(this) object calls. We can make this better. The key is to trap the $(this) object as a var like so:

var el = $(this);
Now, the rest of the code looks like this:

$('.pay-me').on('click', 'button', function() {
  var el = $(this);
  var price = el.data('money');
  var totalPrice = '$' + price + '.00';

  if (el.text() == totalPrice) {
    el.text(el.data("text"));
  } else {
    el.data("text", el.text());
    el.text(totalPrice);
  }
});

Doesn't that look better? Remember, the var el is just a convention. You could use var foo if you want.

We are still stuck with another problem. This only works for the .pay-me node. We need to do what we did before, wrap this into a function and then make it reusable like so:

function payMe() {
  var elem = $(this);
  var price = elem.data('money');
  var totalPrice = '$' + price + '.00';

  if (elem.text() == totalPrice) {
    elem.text(elem.data("og-text"));
  } else {
    elem.data("og-text", elem.text());
    elem.text(totalPrice);
  }
}

Beautiful. Now, let's use it:

$('.pay-me').on('click', 'button', payMe);
$('.pay-you').on('click', 'button', payMe);

drop the mic

Play with code

JS Bin