Revealing Details with jQuery

A week or so ago, someone posted a comment on one of my previous articles, asking if I could help her split up the textual content of an element, showing the first part and replacing the second with a link that, when clicked, would reveal the text. This behavior would appear in an FAQ using a definition list (<dl>), with each question contained in a <dt> and each answer contained in a <dd>. I soon realized that the solution would be rather involved, so I decided to create a new entry out of it rather than simply answer her question in another comment.

Here is the simple definition list structure that I'll be using for the example:

[html]
Benedick's Question...
Beatrice's Answer...
Benedick's Question...
Beatrice's Answer...
[/html]

First Steps

As usual, we start the script with a "document.ready" line so that it can be fired when the DOM has finished loading. Next comes our primary selector expression. For this example, I've given the targeted <dl> element a class of "expander" so that we don't inadvertently attach the behavior to other <dl>s. And since we're manipulating the contents of each <dd> independently of one another, we can make things happen inside an .each() method:

[js]$(document).ready(function() { $('dl.expander dd').each(function(index) { }); });[/js]

We ought to declare some variables now. The first two will go right after the document.ready line, while the others will appear inside the .each() method because they change with each <dd> element.

  • slicePoint (integer): the number of characters at which the contents will be sliced into two parts. Note: any tag names in the HTML that appear inside the <dd> before the slicePoint will be counted along with the text characters.
  • widow (integer): this is a threshold of sorts for whether we want to initially hide part of the <dd>'s contents. If after slicing the contents in two there are fewer words in the second part than the value set by widow, we won't bother hiding anything.
  • allText (string): the full contents of the <dd>
  • startText (string): the first part of the <dd>'s contents — the part that is immediately visible. First, all characters up to the slicePoint are included, and then any "word" characters at the end of the string are removed to avoid ending with a partial word. Keep in mind that in almost every case this approach will leave us with fewer characters than we indicated in the slicePoint variable
  • endText (string): the second part of the <dd>'s contents. This part is initially hidden from view, replaced by a "read more..." link.

Here is what we have so far:

[js]$(document).ready(function() { var slicePoint = 100; var widow = 4; $('dl.expander dd').each(function() { var allText = $(this).html(); var startText = allText.slice(0,slicePoint).replace(/\w+$/,''); var endText = allText.slice(startText.length); }); });[/js]

Although jQuery has its own .slice() method, which operates on a jQuery object, we're using JavaScript's native method, which can operate on both strings (as is the case here) and arrays. Our first use of .slice(), declaring the startText variable, has two arguments — one for the beginning position of the string and one for the ending. Our second has only one argument, for the starting position; the (allText) string's length is the implied ending position. Also, note that for startText, we're chaining .slice() and the .replace() regular expression method.

Manipulating the Contents

Now that we have our two blocks of content, we're going to add a "read more..." link after the startText and wrap the endText in a span (with a class of "details"), but only if there are more words than the value defined by the widow variable. (tangent: am I the only one who can't type "widow" right the first time, always typing "window" instead?). There are many ways to create new elements and insert them into the DOM with jQuery. For this example, we're going to use the .html() method, create an array of strings inside it, and join those strings.

[js]$(document).ready(function() { var slicePoint = 100; var widow = 4; $('dl.expander dd').each(function() { var allText = $(this).html(); var startText = allText.slice(0,slicePoint).replace(/\w+$/,''); var endText = allText.slice(startText.length); // *** commence DOM manipulation ... if ( endText.replace(/\s+$/,'').split(' ').length > widow ) { $(this).html([ startText, 'read more...', '', endText, '' ].join('') ); } }); });[/js]

I picked up that technique of creating an array and joining its elements from the jQuery Google Group. It seems cleaner than string concatenation somehow, and rumor has it that it's a bit faster, too.

Finishing Touches

We have everything in place now, so all we need to do is hide the content inside our newly created <span class="details"> and attach a click handler to the "read more" link. Here is the completed code:

[js]$(document).ready(function() { var slicePoint = 100; var widow = 4; $('dl.expander dd').each(function() { var allText = $(this).html(); var startText = allText.slice(0,slicePoint).replace(/\w+$/,''); var endText = allText.slice(startText.length); if ( endText.replace(/\s+$/,'').split(' ').length > widow ) { $(this).html([ startText, 'read more...', '', endText, '' ].join('') ); } }); // *** hide details until read-more link is clicked; // then hide link and show details. $('span.details').hide(); $('a.read-more').click(function() { $(this).hide() .next('span.details').fadeIn(); return false; }); });[/js]

I used a .fadeIn() because I like the way it looks, but a simple .show() would do. The return false; line is important to prevent a jump to the top of the page on click.

the Demo

Here is a little demo:

Benedick's Question: I pray you, what is he?
Beatrice's Answer: Why, he is the prince's jester: a very dull fool; only his gift is in devising impossible slanders: none but libertines delight in him; and the commendation is not in his wit, but in his villany; for he both pleases men and angers them, and then they laugh at him and beat him. I am sure he is in the fleet: I would he had boarded me. (Much Ado About Nothing, II.1)
Benedick's Question: I do love nothing in the world so well as you: is not that strange?
Beatrice's Answer: As strange as the thing I know not. It were as possible for me to say I loved nothing so well as you: but believe me not; and yet I lie not; I confess nothing, nor I deny nothing. (Much Ado About Nothing, IV.1)
Karl's Question: Why aren't you using another question from Much Ado About Nothing here?
Karl's Answer: Because I'm lazy. And besides, this demo is getting a little too pretentious.