Welcome to the Treehouse Community

Want to collaborate on code errors? Have bugs you need feedback on? Looking for an extra set of eyes on your latest project? Get support with fellow developers, designers, and programmers of all backgrounds and skill levels here with the Treehouse Community! While you're at it, check out some resources Treehouse students have shared here.

Looking to learn something new?

Treehouse offers a seven day free trial for new students. Get access to thousands of hours of content and join thousands of Treehouse students and alumni in the community today.

Start your free trial

JavaScript jQuery Basics (2014) Creating a Spoiler Revealer Perfect

Tomas Pavlik
Tomas Pavlik
26,726 Points

Playing with variables - something's wrong

Hi all, I was trying to change the original code using variables. Everything works fine, well, almost fine, except one thing..

I hope it is understandable, English is not my first language:

If a create a variable called $button and assign new button element to it:

var $button = $("<button>Reveal it!</button>");

and then try to use this variable instead of accesing the button:

$button.click(function() {
   $(this).prev().show().click(function() {
 etc etc
});

only the second button works fine (it disappears when i click on it and appeals again when I click on the spoiler text). There is no reactin when I click on the first button.

Here is the complete code:

var $spoilerSpan = $(".spoiler span"); 
var $spoiler = $(".spoiler");
var $button = $("<button>Reveal it!</button>");

$spoilerSpan.hide();
$spoiler.append($button);

$button.click(function() {
   $(this).prev().show().click(function() {
     $(this).hide();
     $(this).next().show();
  });
  $(this).hide();
});

What's wrong with the firstt button, why it does not react when I'm clcking on it? I appreciate any advice.

Thanks for help.

Are you able to post your HTML code as well so it gives a much wider context please? And your complete JavaScript code please?

Tomas Pavlik
Tomas Pavlik
26,726 Points

Hi Andrews, here´s the complete HTML, JS file is just above.

<!DOCTYPE html>
<html>
<head>
    <title>Star Wars Spoilers</title>
    <link rel="stylesheet" href="css/style.css" type="text/css" media="screen" title="no title" charset="utf-8">
</head>
<body>
    <img src="img/deathstar.png" />
    <p class="spoiler">
        <!--Spoiler:-->
        <span>Darth Vader is Luke Skywalker's Father! Noooooooooooo!</span>
    </p>

  <p class="spoiler">
        <!--Spoiler:-->
        <span>Luke Skywalker and Leia are siblings!</span>
    </p>

    <script src="http://code.jquery.com/jquery-1.11.0.min.js" type="text/javascript" charset="utf-8"></script>
    <script src="js/app.js" type="text/javascript" charset="utf-8"></script>
</body>
</html>

4 Answers

Sean T. Unwin
Sean T. Unwin
28,690 Points

Hi Tomas Pavlik,

The issue is with multiple instances where only the last button appended is getting the click listener.

This is caused because $button is only one element. In order to get all instances of $button we can create a new variable that will have the array of $buttons after they have been added to the DOM.

We can accomplish this by adding a line after $button is appended:

var  $buttons = $spoiler.children($button);

/* OR ( each do the same thing )*/

var $buttons = $('.spoiler > button');

I would declare $buttons with the other variables at the beginning, but don't set it yet.

Using your original code, we would now have:

  var $spoilerSpan = $(".spoiler span");
  var $spoiler = $(".spoiler");
  var $button = $("<button>Reveal it!</button>");
  var $buttons;

  $spoilerSpan.hide();
  $spoiler.append($button);

  $buttons = $spoiler.children($button);

  // Notice we changed from $button to $buttons
  $buttons.click(function() {
    $(this).prev().show().click(function() {
      $(this).hide();
      $(this).next().show();
    });
    $(this).hide();
  });
Sean T. Unwin
Sean T. Unwin
28,690 Points

Nesting Event Listeners (declaring a listener within another) could be considered bad form, I would think. A concept to consider could be to separate the logic so that component functionality is not intertwined. Even though $spoilerSpan and $button are parts of the $spoiler component, they are distinct entities.

Having this in mind could lead us to create an individual click listener for each:

$button.click(function() {
  $(this).hide();
  $(this).prev().show();
});
$button.prev().click(function() {
   $(this).hide();
   $(this).next().show();
});

You will notice the code is little more spread out now. The benefits, however, are that not only is it easier to read and understand, but the Event Listeners of each object (or entity) are their own. They are not bound to one another this way. Sure, they are related and the Listeners do similar tasks, yet they are different and, more importantly, we have removed any scope confusion with nested use of $(this), and also they don't need each other to survive. The $spoiler component needs them but they don't need each other. This line of thinking is at the core of modular development and Object Oriented Programming.

Another concept that could be touched on here is DRY (Don't Repeat Yourself). You will notice that the click listeners are fairly similar to each other (which is likely one of the reasons you grouped them together initially). So with DRY in mind, when we repeat lines of code, especially multiple lines, it is time to think about if a function can be created to help with typing the same thing more than once. This can also help reduce typos as well.

So for this function we definately want to use $(this).hide() and we also want to show() a sibling. Since the value of $(this) can be different we need to have our function be able to take two arguments - an object to hide and an object to show. This could be written similar to the following:

  function toggleSpoiler($hide, $reveal) {
       $hide.hide();
       $reveal.show();
  }

Then we can refactor the click Listeners to include the function call:

  $button.click(function() {
    toggleSpoiler($(this), $(this).prev());
  });
  $button.prev().click(function() {
    toggleSpoiler($(this), $(this).next());
  });

So now our tidy, easy to read code could be similar to:

$(function() {

  var $spoilerSpan = $('.spoiler span');
  var $spoiler = $('.spoiler');
  var $button = $('<button>Reveal it!</button');

  function toggleSpoiler($hide, $reveal) {
       $hide.hide();
       $reveal.show();
  }

  $spoilerSpan.hide();
  $spoiler.append($button);

  $button.click(function() {
    toggleSpoiler($(this), $(this).prev());
  });
  $button.prev().click(function() {
    toggleSpoiler($(this), $(this).next());
  });

});

Workspace Snapshot

Tomas Pavlik
Tomas Pavlik
26,726 Points

Hi Sean, thank you for your explanation. Your code works perfectly and I understand the concept you mentioned. In my code, I followed Andrew and it also works fine, except when I tried to create $button. So I still do not know the answer, why the first button does not react when I click on it...

THX

Tomas

Sean T. Unwin
Sean T. Unwin
28,690 Points

I just tried your original code again and it worked for me.

Could you specify the buttons when you say 'first' and 'second' so that it's clear?

Perhaps let us know which browser you are using as well.

why the first button does not react when I click on it

Does this means that you are getting a button that is labeled 'Reveal it!' and when you press the button then there is no hide/show or am I misunderstanding?

Hi Sean, the 'first' button is the top-most one; which does not respond to the click event (hide button, show spoiler text when clicked) and then the 'second' button respectively - which responds to the click event.

Instead of all the buttons behaving accordingly with the click event, only the last button of the two responds to the click event. The same undesired results is seen when you add more of the same elements to the page - only the last one behaves accordingly.

So his question I presume is, why is it only the last button responding to the click event and not both buttons as expected?

Sean T. Unwin
Sean T. Unwin
28,690 Points

Gotcha. I wasn't considering multiple $spoilers.

Yep Sean, it's all good! :)

Cheers!

Hi Tomas, there is one subtle error which is causing only the last button to respond to the click event handler.

So you declared:

var $button = $("<button>Reveal it!</button>"); 

which you used to dynamically create a new button here:

$spoiler.append($button); 

However, to apply the click event to the buttons, you need to use a valid and appropriate JQuery element | CSS selector as per the code below:

var $button = $("<button>Reveal it!</button>");  
var $spoilerSpan = $(".spoiler span"); 
var $spoiler = $(".spoiler");
//var $button = $("Reveal it!");

$spoilerSpan.hide();
$spoiler.append($button);

$("button").click(function() {
  console.log("I have been clicked"); 
   $(this).prev().show().click(function() {
     $(this).hide();
     $(this).next().show();
  });
  $(this).hide(); 
});

It works correctly!

Cheers!

Sean T. Unwin
Sean T. Unwin
28,690 Points

Your example will select all buttons in the DOM.

Once $button is appended it becomes a valid jQuery element and can be accessed via the DOM, provided $button was defined with valid HTML.

Yes, the selector would select all buttons in the DOM which I believe is not much of a problem in this context - it was the same selector the instructor (Andrew) used in the video actually. I believe the degree of specificity of the selector is primarily based on the developer's requirements as well as other factors such as DOM structure, etc. :)

Cheers!

Sean T. Unwin
Sean T. Unwin
28,690 Points

Ah. I see now.

Cheers. :)

Tomas Pavlik
Tomas Pavlik
26,726 Points

Hi guys, thank you so much for your help :-) I think I am gettint it. Will need a bit more time and a bit more practice, but your explanations helped a lot. Thanks again for you help and for your patience, I know I am not the best in written english communication :-) Tomas