Multiple read more / read less sections are not working on a page

This is a task to do with event delegation. Click events (like the most of the events) are bubbling up to document from the clicked element, and you can detect events on their way up.

The example below shows you how to utilize event delegation. The code is reusable, it can be used on any page with almost any element structure, you can also keep your classes and styling of the elements. This snippet can also be used on dynamic pages, it works without need to make any changes to JS if you add or remove expandable elements to/from the containers.

function toggleContent (options) {
  const {container, expandable, triggerer, autoClose} = options,
    contents = document.querySelectorAll(container),
    buttonText = ['Read more', 'Read less'];
  let current = null; // Keeps book of the currently open expandee

  function toggle (e) {
    const button = e.target;
    if (!button.matches(triggerer)) {return;} // Quit, an irrelevant element clicked 
    const commonParent = button.closest(expandable);
    if (!commonParent) {return;} // Quit, the expandable element was not found
    if (autoClose && current && button !== current) {
      // If autoClose is on, closes the current expandee
      toggle({target: current});
    }
    const state = +commonParent.classList.toggle('expanded');
    button.textContent = buttonText[state];
    current = state ? button : null;
  }

  // Add click listeners to all elements containing expandables
  contents.forEach(cont => cont.addEventListener('click', toggle));
}

// Activate ContentToggler
toggleContent({
  container: '.expandables-container', // Selector for the elements containing expandable elements
  expandable: '.expandable',   // Selector for expandable elements
  triggerer: '.toggle-button', // Selector for the element triggering expansion
  autoClose: true              // Indicates whether the expanded element is closed when a new element is expanded (optional)
});
.expandable {
  border: 1px solid #000;
  margin-bottom: 1em;
}

/* Hides all the expandees */
.expandable .expandee {
  display: none;
}

/* Shows the expandee when .expanded is added to the parent of expandee */
.expanded .expandee {
  display: block;
}
<div class="expandables-container">
  <div class="expandable">
    <p>Always visible content.</p>
    <div class="expandee">
      <p>More content.</p>
    </div>
    <button class="toggle-button">Read more</button>
  </div>
  <div class="expandable">
    <p>Toggle button can be placed above the expandee element.</p>
    <button class="toggle-button">Read more</button>
    <div class="expandee">
      <p>More content.</p>
    </div>
  </div>

  <p>Elements can be placed between expandables.</p>

  <div class="expandable">
    <p>Always visible content.</p>
    <div class="expandee">
      <p>More content.</p>
    </div>
    <p>Elements can be placed between expandable and toggle button.</p>
    <button class="toggle-button">Read more</button>
  </div>
  <div class="expandable">
    <p>The only requirement is, that the toggle button and expandee are descendants of expandable.</p>
    <div class="expandee">
      <p>And the button must not be placed inside expandee.</p>
    </div>
    <button class="toggle-button">Read more</button>
  </div>
</div>

The idea is, that all the expandables are wrapped in an element. The click listener is attached to that wrapper, and only a single click listener per wrapper is needed. This is called event delegation, and it allows you to freely add or remove content to/from the wrapper at any time, without need to update the script at all.

Everything is wrapped in toggleContent function for three reasons. First, it encapsulates the code, no need to introduce global variables. Secondly, it makes possible to call the function with different parameters multiple times, and create different type of togglers on the same page. Thirdly, it’s just much easier to copy a compact pack of code to another page.

toggle function gets the clicked element from the event object (e is automatically passed from the event queue), if the element doesn’t match to the passed triggerer selector, the event is cancelled. Then it finds the common parent of the triggerer and the element to show (“expandee”), and when found, adds expanded class to the parent.

CSS will show the expandee using the descendant combinator .expanded .expandee, which refers to the expandee element when a parent element has expanded class. The current open expandee is closed simply by calling the same function with a dummy event object with the current element as the target. Finally the text value of the triggerer and the current active element are updated.

It’s notable, that the example is a very generic class toggler, it only changes the classname of the parent of the clicked element, and you’re free to make any styling in the CSS rules, it doesn’t have to be display, also height can be used, or anything.

The “widget” is activated by calling toggleContent function. Selectors for the wrappers, the expandable elements and the toggle buttons are passed in an object. The fourth parameter controls the automatic closing. If you want to keep only a single expanded element visible at the time, pass true, when passing false or omitting the property, toggle buttons will work independently from each other.

If you need more control over the toggleable content, I’d recommend OOP based approach. This OOP example wraps the code in the example in this answer in a class, and provides some usecases showing how to control the content. When you’ll get wind of how OOP works (or you already know), you can easily add methods to the class, ex. for hiding/showing all the content etc.

Leave a Comment