Elegance, thy Name is jQuery
- by SGWellens
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> The requirement was to wrap a div around each h3 heading and the subsequent divs grouping them into sections. Why? I don't know, I suppose if you screen-scrapped some HTML from another site, you might want to reformat it before displaying it on your own. Anyways…
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" >'); });} The code here doesn't look nearly as concise and elegant as the original answer. However, unless you and your staff are jQuery masters, during development it really helps to work through algorithms step by step. You can step through this code in the debugger and examine the jQuery objects to make sure one step is working before proceeding on to the next. It's much easier to debug and troubleshoot when each logical coding step is a separate line of code.
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;} Here is the rendered output before:
…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') This will only collect div elements. But it merely skipped the anchor tag and it still collected the unrelated div:
The problem is we need a way to tell the nextUntil function when to stop. CSS selectors to the rescue!
nextUntil('.heading, a') This tells nextUntil to stop collecting elements when it gets to an element with a .heading class OR when it gets to an anchor tag. In this case it solved the problem. FYI: The comma operator in a CSS selector allows multiple criteria.
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); But in this case, the andSelf function reads real nice. In my opinion.
Here's a link to a jsFiddle if you want to play with it.
I hope someone finds this useful
Steve Wellens
CodeProject