views:

977

answers:

3

Hi, i'm creating an FAQ page where the answer is toggled by clicking on the question. The question is h3 and the answer is several "p" elements. Like this:

<h3>The First Question</h3>
<p>Answer Paragraph</p>
<p>Answer Paragraph</p>
<p>Answer Paragraph</p>

<h3>The Second Question</h3>
<p>Answer Paragraph</p>
<p>Answer Paragraph</p>

How can I toggle all p elements belonging to a certain question? My JS toggles ALL following p elements on the page:

$(document).ready(function(){
    $("p").hide();
    $("h3").click(function(){
     $(this).nextAll("p").toggle();
    });
});

(I cannot use div's or classes). Thank you for any help!

+8  A: 

I would do it this way:

$(function() {
  $("p").hide();
  $("h3").click(function() {
    $(this).nextAll().each(function() {
      if ($(this).is('h3')) {
        return false;
      }
      $(this).toggle();
    });
  });
});

Returning false from each() ends the chain.

I would also suggest, if possible, structuring your data better to handle this scenario. For example:

<h3 class="question">Why is there no soup for me?</h3>
<div class="answer">
<p>...</p>
<p>...</p>
<p>...</p>
</div>

and then the problem becomes trivial to solve:

$(function() {
  $("div.answer").hide();
  $("h3.question").click(function() {
    $(this).next().toggle();
  });
});
cletus
Checking the tagName is a good idea, didn't occur to me. Guess I was stuck in jQuery-mode. You might want to check it for the element being toggled as well in case there is something other than a paragraph that shouldn't be toggled.
tvanfosson
Would love to know the reason for the downvote...
cletus
cletus - it's probably for your typo with nextall instead of nextAll and the : instead of ;
ScottE
+1 for clean jQuery, HTML, CSS, and Seinfeld reference.
John Rasch
+6  A: 

The best way to do this is using each and iterating until you get to the next element that should stop the iteration. Returning false during an each stops the iteration. Using filter allows you to check the type of the element in the iteration and respond appropriately.

$(function() {
   $("p").hide();
   $("h3").click(function() {
       $(this).nextAll().each( function() {
           if ($(this).filter('h3').length) {
              return false;
           }
           $(this).filter('p').toggle();
       });
   });
});
tvanfosson
Thank you so much, works perfectly!
Christoph
I like your solution better as it is more jQueriesh.
SolutionYogi
I think it's probably important to note that this code will only hide paragraph tags. If you text without paragraph tags or some other tags after your h3 tag, it will not hide them.
SolutionYogi
@Yogi -- yep, that's by design. If you want to apply it to any child elements, simply remove the "filter('p')" preceding the toggle.
tvanfosson
A: 

Here is an interesting solution that doesn't use .each()

$("h3").click(function() {

    var idx = $("h3,p").index(this);
    var nextIdx = ($("h3,p").index($(this).nextAll("h3")));
    var nextPs = (nextIdx == -1) ? $("h3,p").length - idx : nextIdx - idx;
    $(this).nextAll("p:lt(" + (nextPs - 1) + ")").toggle();

});

I'm looking for the next Ps by index. Not sure how practical this is, but it was a good exercise.

ScottE
IMHO, this code is much harder to read. I prefer the accepted answer.
SolutionYogi
By my count this iterates through the DOM three times to avoid iterating through the paragraph elements following a single header once. I vote for not practical. :-)
tvanfosson
No problem! I wasn't suggesting this was better - I was just curious if it could be done without using .each(). Good point above.
ScottE
Yup, ran this through firebug with console.time and console.timeEnd, and even with this small piece of html, the code above performs badly compared to the accepted answer.
ScottE