10 minutes reading time (1997 words)

Collapsible sections with Bootstrap

2024---JCM-collapsible-bootstrap

Do you have pages with a lot of content, or do you want to create a page where the visitor can decide for themselves which info they’d like to see (like an FAQ page)? An accordion is perfect for that. The visitor gets an overview of the topics, clicks on the interesting ones and the content gets revealed. And the good part is: you don’t need an extension for this, you can do it with a little tweaking - creating an override - of one of Joomla’s core views. Here’s how you do it!

In a previous article (see Case study 2) I showed how to create a FAQ page using the Page Break Plugin. This works well with little content, but if you need to create more complex FAQs (or other kinds of collapsible content), the best way to do it is to create an override.

In this article, I will show how to redesign the category blog layout to display articles as "collapsible sections" or also called "accordions" using Bootstrap. I have adapted the code from the Bootstrap example and added some extras to make the accordion more accessible as it is recommended by Bootstrap following the example from the WAI.

Since you probably have the category blog layout in use several times on a page and don’t want all of them to look like an FAQ we will create a new menu type to handle the accordion view. That way we can choose our layout for this specific page. This means we make an override of the blog layout including the XML file:

blog.xml -> accordion.xml
blog.php -> accordion.php

You can find these in your template files. Go to System -> Website Templates, find your template and click on the files link. Click the tab Create override, find com_content (under Components), click category and Joomla creates your overrides for you. You’ll find them in the files listing in the sidebar next to your template editor, under html.

There will be a couple of files you don’t need, I removed them for this tutorial: blog-item.php, blog-children.php and blog-links.php.

Accordion.xml, our blog.xml override file

In the XML file we will remove some parameters not needed for an FAQ and add an option to set the first element of the accordion as open or closed:

<?xml version="1.0" encoding="UTF-8"?>
<metadata>
    <layout title="Accordion" option="COM_CONTENT_CATEGORY_VIEW_BLOG_OPTION">
        <help key = "Menu_Item:_Category_Blog" />
        <message>
            <![CDATA[Display articles from a category as accordion]]>
        </message>
    </layout>


    <!-- Add fields to the request variables for the layout. -->
    <fields name="request">
        <fieldset name="request"
            addfieldprefix="Joomla\Component\Categories\Administrator\Field"
        >
            <field
                name="id"
                type="modal_category"
                label="JGLOBAL_CHOOSE_CATEGORY_LABEL"
                extension="com_content"
                required="true"
                select="true"
                new="true"
                edit="true"
                clear="true"
            />


            <field
                name="filter_tag"
                type="tag"
                label="JTAG"
                multiple="true"
                mode="nested"
                custom="deny"
            />
        </fieldset>
    </fields>


    <!-- Add fields to the parameters object for the layout. -->
    <fields name="params">
        <fieldset name="basic" label="JGLOBAL_CATEGORY_OPTIONS">
                <field
                    name="layout_type"
                    type="hidden"
                    default="blog"
                />


                <field
                    name="show_category_title"
                    type="list"
                    label="JGLOBAL_SHOW_CATEGORY_TITLE"
                    useglobal="true"
                    class="form-select-color-state"
                    validate="options"
                    >
                    <option value="0">JHIDE</option>
                    <option value="1">JSHOW</option>
                </field>


                <field
                    name="show_description"
                    type="list"
                    label="JGLOBAL_SHOW_CATEGORY_DESCRIPTION_LABEL"
                    useglobal="true"
                    class="form-select-color-state"
                    validate="options"
                    >
                    <option value="0">JHIDE</option>
                    <option value="1">JSHOW</option>
                </field>


                <field
                    name="show_description_image"
                    type="list"
                    label="JGLOBAL_SHOW_CATEGORY_IMAGE_LABEL"
                    useglobal="true"
                    class="form-select-color-state"
                    validate="options"
                    >
                    <option value="0">JHIDE</option>
                    <option value="1">JSHOW</option>
                </field>


                <field
                    name="maxLevel"
                    type="list"
                    label="JGLOBAL_MAXLEVEL_LABEL"
                    description="JGLOBAL_MAXLEVEL_DESC"
                    useglobal="true"
                    validate="options"
                    >
                    <option value="-1">JALL</option>
                    <option value="0">JNONE</option>
                    <option value="1">J1</option>
                    <option value="2">J2</option>
                    <option value="3">J3</option>
                    <option value="4">J4</option>
                    <option value="5">J5</option>
                </field>


                <field
                    name="show_empty_categories"
                    type="list"
                    label="JGLOBAL_SHOW_EMPTY_CATEGORIES_LABEL"
                    useglobal="true"
                    class="form-select-color-state"
                    validate="options"
                    >
                    <option value="0">JHIDE</option>
                    <option value="1">JSHOW</option>
                </field>


                <field
                    name="show_no_articles"
                    type="list"
                    label="COM_CONTENT_NO_ARTICLES_LABEL"
                    useglobal="true"
                    class="form-select-color-state"
                    validate="options"
                    >
                    <option value="0">JHIDE</option>
                    <option value="1">JSHOW</option>
                </field>


                <field
                    name="show_cat_tags"
                    type="list"
                    label="COM_CONTENT_FIELD_SHOW_CAT_TAGS_LABEL"
                    useglobal="true"
                    class="form-select-color-state"
                    validate="options"
                    >
                    <option value="0">JHIDE</option>
                    <option value="1">JSHOW</option>
                </field>


        </fieldset>


        <fieldset name="advanced" label="JGLOBAL_BLOG_LAYOUT_OPTIONS" description="JGLOBAL_SUBSLIDER_BLOG_LAYOUT_LABEL">


            <field
                name="num_leading_articles"
                type="hidden"
                default="0"
            />


            <field
                name="num_intro_articles"
                type="number"
                label="JGLOBAL_NUM_INTRO_ARTICLES_LABEL"
                filter="integer"
                validate="number"
                min="0"
                useglobal="true"
                parentclass="stack span-1"
            />


            <field
                name="collapse_first_item"
                type="radio"
                layout="joomla.form.field.radio.switcher"
                label="First element open"
                parentclass="stack span-2-inline"
                validate="options"
                default="1"
                >
                <option value="0">JNO</option>
                <option value="1">JYES</option>
            </field>


            <field
                name="show_featured"
                type="list"
                label="JGLOBAL_SHOW_FEATURED_ARTICLES_LABEL"
                default=""
                useglobal="true"
                class="form-select-color-state"
                validate="options"
                parentclass="stack span-1"
                >
                <option value="show">JSHOW</option>
                <option value="hide">JHIDE</option>
                <option value="only">JONLY</option>
            </field>


            <field
                name="show_subcategory_content"
                type="list"
                label="JGLOBAL_SHOW_SUBCATEGORY_CONTENT_LABEL"
                useglobal="true"
                validate="options"
                parentclass="stack span-1-inline"
                >
                <option value="0">JNONE</option>
                <option value="-1">JALL</option>
                <option value="1">J1</option>
                <option value="2">J2</option>
                <option value="3">J3</option>
                <option value="4">J4</option>
                <option value="5">J5</option>
            </field>
            <field
                name="orderby_pri"
                type="list"
                label="JGLOBAL_CATEGORY_ORDER_LABEL"
                useglobal="true"
                validate="options"
                parentclass="stack span-2"
                >
                <option value="none">JGLOBAL_NO_ORDER</option>
                <option value="alpha">JGLOBAL_TITLE_ALPHABETICAL</option>
                <option value="ralpha">JGLOBAL_TITLE_REVERSE_ALPHABETICAL</option>
                <option value="order">JGLOBAL_CATEGORY_MANAGER_ORDER</option>
            </field>


            <field
                name="orderby_sec"
                type="list"
                label="JGLOBAL_ARTICLE_ORDER_LABEL"
                useglobal="true"
                validate="options"
                parentclass="stack span-2-inline"
                >
                <option value="front">COM_CONTENT_FEATURED_ORDER</option>
                <option value="rdate">JGLOBAL_MOST_RECENT_FIRST</option>
                <option value="date">JGLOBAL_OLDEST_FIRST</option>
                <option value="alpha">JGLOBAL_TITLE_ALPHABETICAL</option>
                <option value="ralpha">JGLOBAL_TITLE_REVERSE_ALPHABETICAL</option>
                <option value="author">JGLOBAL_AUTHOR_ALPHABETICAL</option>
                <option value="rauthor">JGLOBAL_AUTHOR_REVERSE_ALPHABETICAL</option>
                <option value="hits" requires="hits">JGLOBAL_MOST_HITS</option>
                <option value="rhits" requires="hits">JGLOBAL_LEAST_HITS</option>
                <option value="random">JGLOBAL_RANDOM_ORDER</option>
                <option value="order">JGLOBAL_ORDERING</option>
                <option value="rorder">JGLOBAL_REVERSE_ORDERING</option>
                <option value="vote" requires="vote">JGLOBAL_VOTES_DESC</option>
                <option value="rvote" requires="vote">JGLOBAL_VOTES_ASC</option>
                <option value="rank" requires="vote">JGLOBAL_RATINGS_DESC</option>
                <option value="rrank" requires="vote">JGLOBAL_RATINGS_ASC</option>
            </field>


            <field
                name="order_date"
                type="list"
                label="JGLOBAL_ORDERING_DATE_LABEL"
                useglobal="true"
                validate="options"
                parentclass="stack span-2-inline"
                >
                <option value="created">JGLOBAL_CREATED</option>
                <option value="modified">JGLOBAL_MODIFIED</option>
                <option value="published">JPUBLISHED</option>
                <option value="unpublished">JUNPUBLISHED</option>
            </field>


            <field
                name="show_pagination"
                type="list"
                label="JGLOBAL_PAGINATION_LABEL"
                useglobal="true"
                class="form-select-color-state"
                validate="options"
                parentclass="stack span-1"
                >
                <option value="0">JHIDE</option>
                <option value="1">JSHOW</option>
                <option value="2">JGLOBAL_AUTO</option>
            </field>


            <field
                name="show_pagination_results"
                type="list"
                label="JGLOBAL_PAGINATION_RESULTS_LABEL"
                useglobal="true"
                class="form-select-color-state"
                validate="options"
                parentclass="stack span-1-inline"
                >
                <option value="0">JHIDE</option>
                <option value="1">JSHOW</option>
            </field>


        </fieldset>


        <fieldset name="integration" label="COM_MENUS_INTEGRATION_FIELDSET_LABEL">
            <field
                name="show_feed_link"
                type="list"
                label="JGLOBAL_SHOW_FEED_LINK_LABEL"
                useglobal="true"
                class="form-select-color-state"
                validate="options"
                >
                <option value="0">JHIDE</option>
                <option value="1">JSHOW</option>
            </field>


            <field
                name="feed_summary"
                type="list"
                label="JGLOBAL_FEED_SUMMARY_LABEL"
                useglobal="true"
                validate="options"
                >
                <option value="0">JGLOBAL_INTRO_TEXT</option>
                <option value="1">JGLOBAL_FULL_TEXT</option>
            </field>
        </fieldset>
    </fields>
</metadata>

Accordion.php, our blog.php override file

The accordion.php file will also greatly simplify and leave only the block for the intro items:

<?php


/**
 * @package     Joomla.Site
 * @subpackage  com_content
 *
 * @copyright   (C) 2006 Open Source Matters, Inc. <https://www.joomla.org>
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */


defined('_JEXEC') or die;


use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Layout\FileLayout;
use Joomla\CMS\Layout\LayoutHelper;


$app = Factory::getApplication();


$this->category->text = $this->category->description;
$app->triggerEvent('onContentPrepare', [$this->category->extension . '.categories', &$this->category, &$this->params, 0]);
$this->category->description = $this->category->text;


$results = $app->triggerEvent('onContentAfterTitle', [$this->category->extension . '.categories', &$this->category, &$this->params, 0]);
$afterDisplayTitle = trim(implode("\n", $results));


$results = $app->triggerEvent('onContentBeforeDisplay', [$this->category->extension . '.categories', &$this->category, &$this->params, 0]);
$beforeDisplayContent = trim(implode("\n", $results));


$results = $app->triggerEvent('onContentAfterDisplay', [$this->category->extension . '.categories', &$this->category, &$this->params, 0]);
$afterDisplayContent = trim(implode("\n", $results));


$htag       = $this->params->get('show_page_heading') ? 'h2' : 'h1';
$htagart    = $this->params->get('show_page_heading') && $this->params->get('show_category_title') ? 'h3' : 'h2';


HTMLHelper::_('bootstrap.collapse');
?>


<div class="com-content-category-blog blog">
    <?php if ($this->params->get('show_page_heading')) : ?>
        <div class="page-header">
            <h1> <?php echo $this->escape($this->params->get('page_heading')); ?> </h1>
        </div>
    <?php endif; ?>


    <?php if ($this->params->get('show_category_title', 1)) : ?>
    <<?php echo $htag; ?>>
        <?php echo $this->category->title; ?>
    </<?php echo $htag; ?>>
    <?php endif; ?>
    <?php echo $afterDisplayTitle; ?>


    <?php if ($this->params->get('show_cat_tags', 1) && !empty($this->category->tags->itemTags)) : ?>
        <?php $this->category->tagLayout = new FileLayout('joomla.content.tags'); ?>
        <?php echo $this->category->tagLayout->render($this->category->tags->itemTags); ?>
    <?php endif; ?>


    <?php if ($beforeDisplayContent || $afterDisplayContent || $this->params->get('show_description', 1) || $this->params->def('show_description_image', 1)) : ?>
        <div class="category-desc clearfix">
            <?php if ($this->params->get('show_description_image') && $this->category->getParams()->get('image')) : ?>
                <?php echo LayoutHelper::render(
                    'joomla.html.image',
                    [
                        'src' => $this->category->getParams()->get('image'),
                        'alt' => empty($this->category->getParams()->get('image_alt')) && empty($this->category->getParams()->get('image_alt_empty')) ? false : $this->category->getParams()->get('image_alt'),
                    ]
                ); ?>
            <?php endif; ?>
            <?php echo $beforeDisplayContent; ?>
            <?php if ($this->params->get('show_description') && $this->category->description) : ?>
                <?php echo HTMLHelper::_('content.prepare', $this->category->description, '', 'com_content.category'); ?>
            <?php endif; ?>
            <?php echo $afterDisplayContent; ?>
        </div>
    <?php endif; ?>


    <?php if (empty($this->intro_items)) : ?>
        <?php if ($this->params->get('show_no_articles', 1)) : ?>
            <div class="alert alert-info">
                <span class="icon-info-circle" aria-hidden="true"></span><span class="visually-hidden"><?php echo Text::_('INFO'); ?></span>
                    <?php echo Text::_('COM_CONTENT_NO_ARTICLES'); ?>
            </div>
        <?php endif; ?>
    <?php endif; ?>


    <?php if (!empty($this->intro_items)) : ?>
        <?php
            $count_items = 0;
            $collapse_first_item = $this->params->get('collapse_first_item',0);
        ?>
        <div class="accordion" id="accordionBlog">
        <?php foreach ($this->intro_items as $key => &$item) : ?>
            <?php
                $canEdit = $item->params->get('access-edit');
                $count_items++;
            ?>
            <div class="accordion-item">
                <<?php echo $htagart; ?> class="accordion-header">
                    <button id="accordionItem-<?php echo $item->id; ?>" class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-<?php echo $item->id; ?>" aria-expanded="<?php echo ($count_items == 1 && $collapse_first_item == 1) ? 'true' : 'false'; ?>" aria-controls="collapse-<?php echo $item->id; ?>">
                        <?php echo $this->escape($item->title); ?>
                    </button>
                </<?php echo $htagart; ?>>
                <div id="collapse-<?php echo $item->id; ?>" class="accordion-collapse collapse <?php echo ($count_items == 1 && $collapse_first_item == 1) ? 'show' : ''; ?>" data-bs-parent="#accordionBlog" role="region" aria-labelledby="accordionItem-<?php echo $item->id; ?>">
                    <div class="accordion-body">
                        <?php if ($canEdit) : ?>
                            <?php echo LayoutHelper::render('joomla.content.icons', ['params' => $item->params, 'item' => $item]); ?>
                        <?php endif; ?>


                        <?php // Content is generated by content plugin event "onContentAfterTitle" ?>
                        <?php echo $item->event->afterDisplayTitle; ?>


                        <?php // Content is generated by content plugin event "onContentBeforeDisplay" ?>
                        <?php echo $item->event->beforeDisplayContent; ?>
                        <div class="item-content">
                            <?php echo $item->introtext; ?>
                            <?php echo $item->fulltext; ?>
                        </div>
                        <?php // Content is generated by content plugin event "onContentAfterDisplay" ?>
                        <?php echo $item->event->afterDisplayContent; ?>
                    </div>
                </div>
            </div>
        <?php endforeach; ?>
        </div>
    <?php endif; ?>


    <?php // Code to add a link to submit an article. ?>
    <?php if ($this->category->getParams()->get('access-create')) : ?>
        <?php echo HTMLHelper::_('contenticon.create', $this->category, $this->category->params); ?>
    <?php endif; ?>
    <?php if (($this->params->def('show_pagination', 1) == 1 || ($this->params->get('show_pagination') == 2)) && ($this->pagination->pagesTotal > 1)) : ?>
        <div class="com-content-category-blog__navigation w-100">
            <?php if ($this->params->def('show_pagination_results', 1)) : ?>
                <p class="com-content-category-blog__counter counter float-md-end pt-3 pe-2">
                    <?php echo $this->pagination->getPagesCounter(); ?>
                </p>
            <?php endif; ?>
            <div class="com-content-category-blog__pagination">
                <?php echo $this->pagination->getPagesLinks(); ?>
            </div>
        </div>
    <?php endif; ?>
</div>

For my example, the presentation of questions / answers is minimalistic: the title of the article is the question, the content is the answer.

The parent div element containing all articles get the class “accordion” (that is the Bootstrap class that does the magic) and the id “accordionBlog”. Each article is an “accordion-item”, also a Bootstrap class.

The title of the article is included in a button element that needs several attributes:

  • id: unique identifier for the element (will be used later)
  • class: accordion-button, Bootstrap styling
  • data-bs-toggle: Bootstrap attribute used by the Javascript
  • data-bs-target: Bootstrap attribute used by the Javascript, the value is the id of the next element (see below)
  • aria-expanded: additional information for assistive technologies (e.g. screen readers)
  • aria-controls: additional information for assistive technologies (e.g. screen readers), the value is the id of the next element (see below)

The attribute “aria-expanded” has the value "false" and the questions are closed by default. You can control if you want to have the first element open with a setting in the menu item. If it is the first element and it is defined to be open, then the value of “aria-expanded” will be “true”:

<?php echo ($count_items == 1 && $collapse_first_item == 1) ? 'true' : 'false'; ?>

The content of the article (the answer) is inside another div element with several attributes:

  • id: unique identifier for the element, match with the “data-bs-target” from the button. To make the id unique I use the id of the article: 
    id="collapse-<?php echo $item->id; ?>"
  • class: accordion-collapse collapse and additionally show if the the first element should be open by default, Bootstrap styling
  • data-bs-parent: it points to the id of the parent element (accordionBlog)
  • region: creates a landmark region that contains the currently expanded accordion panel
  • aria-labelledby: additional information for assistive technologies (e.g. screen readers), defines the accessible name for the region element (in our case the title of the article)

Now we need a category "FAQ" with the appropriate posts. Then we create the menu item:

Screenshot of menu options

The result looks like this:

Screenshot of the accordion

And here "live" on the Cassiopeia demo page.

You don’t need to write a single line of CSS or Javascript. The Bootstrap library delivered by Joomla does all the work for you.
A further advantage of this method to create an FAQ is that you can easily add / remove new questions by creating or unpublishing articles in the backend. You can also change the order of questions by changing the settings in the menu item or moving the articles around.

I hope I could show once again that Joomla is a very versatile system and with small changes in the code you can create almost everything!

2
Creating full width Joomla modules inside content
Harnessing the Power of Bearsampp: A Developer’s J...
 

Comments

Already Registered? Login Here
No comments made yet. Be the first to submit a comment

By accepting you will be accessing a service provided by a third-party external to https://magazine.joomla.org/