Elegance, thy Name is jQuery
So, I'm browsing though some questions over on the Stack Overflow website and I found a good jQuery question just a few minutes old. Here is a link to it. It was a tough question; I knew that by answering it, I could learn new stuff and reinforce what I already knew: Reading is good, doing is better. Maybe I could help someone in the process too.
I cut and pasted the HTML from the question into my Visual Studio IDE and went back to Stack Overflow to reread the question. Dang, someone had already answered it! And it was a great answer. I never even had a chance to start analyzing the issue.
Now I know what a one-legged man feels like in an ass-kicking contest.
Nevertheless, since the question and answer were so interesting, I decided to dissect them and learn as much as possible.
The HTML consisted of some divs separated by h3 headings. Note the elements are laid out sequentially with no programmatic grouping:
<h3 class="heading">Heading 1</h3>
<div>Content</div>
<div>More content</div>
<div>Even more content</div>
<h3 class="heading">Heading 2</h3>
<div>some content</div>
<div>some more content</div>
<h3 class="heading">Heading 3</h3>
<div>other content</div>
</form>
</body>
Here is the marvelously, succinct posted answer:
$('.heading').each(function(){
$(this).nextUntil('.heading').andSelf().wrapAll('<div class="section">');
});
I was familiar with all the parts except for nextUntil and andSelf. But, I'll analyze the whole answer for completeness. I'll do this by rewriting the posted answer in a different style and adding a boat-load of comments:
function Test()
{
// $Sections is a jQuery object and it will contain three elements
var $Sections = $('.heading');
// use each to iterate over each of the three elements
$Sections.each(function ()
{
// $this is a jquery object containing the current element
// being iterated
var $this = $(this);
// nextUntil gets the following sibling elements until it reaches
// an element with the CSS class 'heading'
// andSelf adds in the source element (this) to the collection
$this = $this.nextUntil('.heading').andSelf();
// wrap the elements with a div
$this.wrapAll('<div class="section" >');
});
}
Note: You may think the original code runs much faster than this version. However, the time difference is trivial: Not enough to worry about: Less than 1 millisecond (tested in IE and FF).
Note: You may want to jam everything into one line because it results in less traffic being sent to the client. That is true. However, most Internet servers now compress HTML and JavaScript by stripping out comments and white space (go to Bing or Google and view the source). This feature should be enabled on your server: Let the server compress your code, you don't need to do it.
Free Career Advice: Creating maintainable code is Job One—Maximum Priority—The Prime Directive. If you find yourself suddenly transferred to customer support, it may be that the code you are writing is not as readable as it could be and not as readable as it should be. Moving on…
I created a CSS class to enhance the results:
.section
{
background-color: yellow;
border: 2px solid black;
margin: 5px;
}
…and after the jQuery code runs.
Pretty Cool! But, while playing with this code, the logic of nextUntil began to bother me: What happens in the last section? What stops elements from being collected since there are no more elements with the .heading class? The answer is nothing. In this case it stopped collecting elements because it was at the end of the page. But what if there were additional HTML elements?
I added an anchor tag and another div to the HTML:
<h3 class="heading">Heading 1</h3>
<div>Content</div>
<div>More content</div>
<div>Even more content</div>
<h3 class="heading">Heading 2</h3>
<div>some content</div>
<div>some more content</div>
<h3 class="heading">Heading 3</h3>
<div>other content</div>
<a>this is a link</a>
<div>unrelated div</div>
</form>
</body>
The code as-is will include both the anchor and the unrelated div. This isn't what we want.
My first attempt to correct this used the filter parameter of the nextUntil function:
nextUntil('.heading', 'div')
The problem is we need a way to tell the nextUntil function when to stop. CSS selectors to the rescue!
nextUntil('.heading, a')
Bingo!
One final note, we could have broken the code down even more:
We could have replaced the andSelf function here:
$this = $this.nextUntil('.heading, a').andSelf();
With this:
// get all the following siblings and then add the current item
$this = $this.nextUntil('.heading, a');
$this.add(this);
Here's a link to a jsFiddle if you want to play with it.
I hope someone finds this useful
Steve Wellens