Friday, January 17, 2014

Using Section Elements With Accordions

HTML5 has a new section element. This is a generic grouping element, used to divide the page into sections. For instance, you might have three of these on a page, each with a header tag as the first section element. In other words, sections are essentially divs that just read better in the HTML. Who doesn't want more semantic meaning in their markup?

These section tags would be a good target for the accordion widget. The accordion essentially takes sections of the page, applies a theme, and makes each section expandable and collapsible through user interactions. Except it doesn't yet understand these elements. That's fine, there's nothing wrong with div elements. In case you're curious, however, here's how you can extend the accordion widget, to make it work with section elements.

To do that, we basically need to tell the widget about the new section tag, where it can find the headers, and so on. There's already a header option — a selector that tells the widget where to look for headers within the element. By default, the accordion expects a structure that looks like this.

<h3></h3>
<div></div>
<h3></h3>
<div></div>

When we want the accordion to be able to understand and work with looks something more like this.

<section>
    <h3></h3>
    ...
</section>
<section>
    <h3></h3>
    ...
</section>

Instead of alternating between header and content, the header is contained withing the section tag. Let's breakdown our extension of the accordion, starting with the constructor.

_create: function() {

    if ( this.element.find( "section" ).length === 0 ) {
        return this._super();
    }
            
    this.option( "header", "> section > :first-child" );
            
    this.element.find( this.options.header ).each( function() {
        $( this ).siblings().wrapAll( "&lt;div/>" );
    });
            
    this._super();
            
}

The first thing we're looking for in the element is the nonexistence of a section tag. If they don't exist, then we have nothing to do and can simply return after calling the original constructor. If there are section tags, on the other hand, we have to tell the widget where to find the headers. This is done by setting the header option, and we're telling it to look for the first element inside the section element.

So now that the accordion knows where to find the headers, we have to tell it where to find the corresponding content. But there's no such selector option that tells the accordion where to find the content elements. Instead, it's based on the alternating element structure — h3 and div. But we don't have that anymore, not inside sections. The problem now is that the section is the content. Including the header.

All we have to do is wrap() the section content, excluding the header, into a div element. And that's exactly what we do. The accordion then finds each content section as expected. But now how do we go about undoing all this when it comes time to destroy the widget?

_destroy: function() {
            
    if ( this.element.find( "section" ).length === 0 ) {
        return this._super();
    }
            
    this.element.find( this.options.header ).each( function() {
        var $content = $( this ).next();
        $content.replaceWith( $content.children() );
    });
            
    this._super();
            
}

The _destroy() method is similar to the _create() method in it's overall work flow. First we check for the existence of section tags, we then iterate over each header, unwrapping the section content using replaceWith(), then we use the default destructor implementation to perform the remaining cleanup.

The nice thing about this approach is that we're using the structure of the existing markup to modify the way the widget works. This simplifies things for those that wish to make use of the new capabilities. Why add a new widget option when there's no need to?