Building an Accordion with jQuery and SharePoint 2010 | dig sharepoint

User experience (UX) is a big buzzword these days when building websites. How many clicks the user has to perform and how much scrolling will be done should always factor in the design process. In many cases content can be so large that your user could have finger cramps when scrolling (or kill a tree when printing the page!). Have you ever given thought to using a jQuery Accordion? This method works well, because it makes use of the horizontal space to store massive amounts of content in “panels” that are hidden until the user wants to see it. It also adds a nice interact experience that people normally get only when viewing Flash content.

What you are presented with here is a series of 7 panels, each representing a list item in a SharePoint List. The first panel is shown by default. Arrows on each panel offer the user a way to expand content in each panel while collapsing the panel currently being viewed.
To build such a webpart, I initially defined a SharePoint List that contained all the elements used by the visual webpart:

TitleName of panel section
SlideOrderDisplay Order of the panels
SlideHtmlRaw HTML for the panel content
HeaderUrlImage for the vertical text on the panel
PanelBackgroundImageBackground image for the panel (optional)
PanelBackgroundHexGeneral background color for the panel

Now that the list is defined, the next step was to build the visual webpart. The basic shell of the panels is as follows:

<div> <!-- container -->
    <ul>
        <li> <!-- list item for each panel -->
            <div>panel left shadow image</div>
            <div>panel container</div>
                <div>left vertical banner</div>
                    <div>vertical text image</div>
                    <div>navigation arrow</div>
                    <div>left vertical shadow</div>
                </div>
                <div>panel content</div>
            </div>
        </li>
    </ul>
</div>



Here is the corresponding html used in the visual webpart:

<asp:Repeater ID="rptSlider" runat="server" Visible="true" onitemdatabound="rptSlider_ItemDataBound">
    <HeaderTemplate>
        <asp:Literal ID="ltContainerBegin" runat="server"></asp:Literal>
        <ul class="accordion">
    </HeaderTemplate>
    <ItemTemplate>
            <li id="liItem" runat="server" class="accordion-top-li">
                <div class="li-content-vert-shadow"><img src="/SiteCollectionImages/img_vert_shadow.png" alt="vertical shadow" /></div>
                <div id="liContent" class="li-content" runat="server">
                    <div class="accordion-vert-header">
                        <div class="vert-header-text"><asp:Literal ID="ltVerticalHeaderImg" runat="server"></asp:Literal></div>
                        <div class="vert-header-arrow"><asp:Literal ID="ltArrow" runat="server"></asp:Literal></div>
                        <div class="vert-header-shadow"><asp:Literal ID="ltVerticalShadowImg" runat="server"></asp:Literal></div>
                    </div>
                    <div class="accordion-html-content">
                        <asp:Literal ID="ltSlideHtml" runat="server"></asp:Literal>
                    </div>
                </div>
            </li>
    </ItemTemplate>
    <FooterTemplate>
        </ul>
        </div>
    </FooterTemplate>
</asp:Repeater>

Since the panels are all the same structure, I used an ASP:Repeater control to build the unordered list items dynamically.  After setting up the structure in html, I began to tie the data to the controls, through the ItemDataBound event of my Repeater:

protected void rptSlider_ItemDataBound(object sender, RepeaterItemEventArgs e)
{
    if (e.Item.ItemType == ListItemType.Item || e.Item.ItemType == ListItemType.AlternatingItem)
    {
        DataRowView drv = (DataRowView)e.Item.DataItem;
 
        Literal ltContainerBegin = (Literal)rptSlider.Controls[0].FindControl("ltContainerBegin");
        ltContainerBegin.Text = string.Format("<div id='accordionBKG' style='background-color: {0};'>", drv["PanelBackgroundHex"]);
 
        HtmlGenericControl liItem = (HtmlGenericControl)e.Item.FindControl("liItem");
 
        if (nthSlide == 0)
        {
            liItem.Attributes.Add("class", "accordion-top-li-first");
        }
        else
        {
            liItem.Attributes.Add("style", "width: " + slideClosedWidth + "px");
 
            Literal ltArrow = (Literal)e.Item.FindControl("ltArrow");
            ltArrow.Text = "<img src='/SiteCollectionImages/but_arrow_lft_off.png' width='40px' height='40px' alt='accordion arrow' class='accordion_arrow' />";
 
            Literal ltVerticalShadowImg = (Literal)e.Item.FindControl("ltVerticalShadowImg");
            ltVerticalShadowImg.Text = "<img src='/SiteCollectionImages/img_vert_shadow.png' alt='vertical shadow' class='accordion_vertical_shadow' />";
        }
                
        HtmlGenericControl liContent = (HtmlGenericControl)e.Item.FindControl("liContent");
                
        // Set background image.  If it doesn't exist, set it to the background hex value in the column
        if (drv["PanelBackgroundImage"].ToString() != string.Empty)
            liItem.Attributes.Add("style", "background-image: url( " + drv["PanelBackgroundImage"] + "); background-position: bottom left; background-repeat: no-repeat;");
 
        if (drv["HeaderUrl"].ToString() != "")
        {
            Literal ltVerticalHeaderImg = (Literal)e.Item.FindControl("ltVerticalHeaderImg");
            ltVerticalHeaderImg.Text = string.Format("<img src='{0}' alt='{1}' /><br /><br />", drv["HeaderUrl"].ToString(), drv["Title"].ToString());
        }
 
        Literal ltSlideHtml = (Literal)e.Item.FindControl("ltSlideHtml");
        ltSlideHtml.Text = drv["SlideHtml"].ToString();
 
        nthSlide++;
    }
 
}

There are a few things here to notice.  I used ASP:Literal controls in this webpart instead of ASP:Image controls.  This is purely a matter of preference.  You can use either.  The important thing I am doing here is dynamically building my panels and then let the CSS and jQuery handle the “magic”.  
I built this accordion with future addition/subtractions in mind. In my Page_Load method, I made a determination of what the closed panel widths should be and stored that value in a global variable called “dblSlideClosedWidth”:

ListItems = dt;
if (ListItems.Rows.Count > 0)
{
    // Set widths dynamically based on how many rows are currently in the list
    double rows = (double)ListItems.Rows.Count;
    dblSlideClosedWidth = Math.Floor(dblSlideClosedContainerWidth / (rows - 1));
    // Determine Slide Width, but add 9px onto it because there is a -9px margin assigned through css
    slideClosedWidth = (int)dblSlideClosedWidth + 9;
 
    rptSlider.DataSource = ListItems;
    rptSlider.DataBind();
 
}

Then in the repeater event you’ll notice I set the width of each panel dynamically.  If this is not the first time through the loop (designated by the global variable “nthSlide”), I set the width panel equal to “dblSlideClosedWidth”.  The first panel is ignored, as it needs to be displayed in full width specified in the css:

if (nthSlide == 0)
{
    liItem.Attributes.Add("class", "accordion-top-li-first");
}
else
{
    liItem.Attributes.Add("style", "width: " + slideClosedWidth + "px");
 
    Literal ltArrow = (Literal)e.Item.FindControl("ltArrow");
    ltArrow.Text = "<img src='/SiteCollectionImages/but_arrow_lft_off.png' width='40px' height='40px' alt='accordion arrow' class='accordion_arrow' />";
 
    Literal ltVerticalShadowImg = (Literal)e.Item.FindControl("ltVerticalShadowImg");
    ltVerticalShadowImg.Text = "<img src='/SiteCollectionImages/img_vert_shadow.png' alt='vertical shadow' class='accordion_vertical_shadow' />";
}

Now that I got the webpart built, it’s time to talk about the css and jQuery.  To describe the css in a nutshell, I basically have a container of unordered list items (the panels) that are displayed inline.  There is a left vertical shadow to the left of each panel, so I had to also position each panel relative to each other and then set a negative margin on each panel equal to the width of the shadow image.  This creates an effect of each panel overlapping each other.
The jQuery portion is really where the “magic” happens. From a usability standpoint, a user may click on the entire closed panel itself or only on the arrow on the closed panel. So I made a determination to add the “click” event to the entire close panel. This way, it doesn’t matter if the user clicks on the panel or the arrow…the event will still fire. The main idea when animating the opening and closing of panels is to do 2 events in parallel: open up the panel that was clicked on, and close the panel that is currently open. I needed to perform these actions with the same animation duration. If they aren’t occurring at the same time, the panels will float outside of the container, as the total widths of all panels in the container during the process will be greater than the overall container width.
To create the sliding effect, I made use of the jQuery animate() method. The two key attributes is the “width” and “duration”. Setting the “width” attribute specifies what width it needs to become. In my case I needed to close the currently opened panel, so I set the “width” equal to a predetermined variable called “minWidth”:

// Close Previously Clicked Panel before Opening the current clicked panel
if (lastOpenedBlock.index() > 0)
{
    // Hide last clicked element
    $(lastOpenedBlock).animate(
        {width: minWidth}, 
        {queue:true, duration:1000, 
            complete: function()
            {
                closeAnimationIsRunning = false;
            },
            step: function()
            {
            }
        }
    );
}

Then I opened the selected panel using the same animate() method, but this is using a predetermined value called “maxWidth”:

$(currentBlock).animate(
    {width: minWidth}, 
    {queue:true, duration:1000, 
        complete: function()
        {
            closeAnimationIsRunning = false;
        },
        step: function()
        {
        }
    }
);

The “minWidth” and “maxWidth” variables are determined dynamically based on the first panel as this is the space we need to fill.  It is necessary to close a panel before opening another because these are sequential events.  If you begin to open a panel first, the combined total of panel widths will exceed the container widths and you will see an undesirable effect of the panels floating outside of the container.
One thing I had to consider was the possibility that the user may click on multiple panels during transition. I found that during this scenario, there was a period of time when the page seemed to play “catch up”…the panels appeared to have a life of their own (opening and closing multiple times after the initial click). To overcome this, I set two variables called “openAnimationIsRunning” and “closeAnimationIsRunning” that get set when the user clicks on a panel. Within my “click” event, I first check these variables. If either are true, then I stop the “click” event. This prevents the user from clicking multiple panels while the animation is in motion:

$(".accordion-vert-header").click(
    function()
    {    
        // This allows one animation to run at a time.
        if (openAnimationIsRunning || closeAnimationIsRunning)
        {
            return;
        }
        
        openAnimationIsRunning = true;
        closeAnimationIsRunning = true;
}

The last consideration was what happens to the content within the panels during the transition.  I found that when I animated the panel opened/closed, the content within the panel needed to also animate open/close.  To solve this, I found that a nice fade worked well:
// Open current block content
$(currentBlock).children('.li-content').children('.accordion-html-content').fadeIn();
Accordions are a great way to consolidate content using minimal space, while creating an interactive experience for your users. I provided a complex use of an accordion in my example, but I hope it allowed you to understand the elements and the process involved when building your own!

If you’d like to download the code, I am providing the webpart, jQuery, css, and list template I used in my example.