Dev

ReadMore.js

A simple dependency-free Read More JavaScript thing

A recent Alaskan travel site I was working on, Kawanti Adventures, required the ability to condense / truncate blocks of text with a Read More / Read Less feature.

After some poking around existing libraries and solutions, I decided to create my own as I couldn’t find anything that precisely fit my requirements.

Desired Features

  • Keep it super duper lightweight – so pure js only
  • End truncated text with an ellipse to indicate more content
  • The ability to specify word count that’s displayed before truncation
  • Support multiple instances per page, each with it’s own word count
  • Keep original markup intact
  • Change Read More / Read Less text via settings object
  • Ability to have the ReadMore link be inline or below the truncated text

The functionality would be implemented within custom CMS modules, with the ability for the user to define specified word count.

The Solution

  1. Targets the provided markup/content
  2. Stashes it into an array
  3. Uses a data attribute (data-rm-words) to determine the content to display / truncate
  4. Truncates content and insets an ellipse at the end
  5. Injects a Read More Link after the truncated content

On click of the Read More link, all content is revealed (untruncated?), outputting its original markup. The Read More is swapped with a Read Less link, which can then truncate the content again on click (retruncate?).

Interaction is kept to a minimum (no fades or slides) as to not distract the user from the actual content.

The JavaScript


/**
 *  Read More JS
 *  Truncates text via specfied character length with more/less actions.
 *  Maintains original format of pre truncated text.
 *  @author stephen scaff
 *  @todo   Add destroy method for ajaxed content support.
 *
 */
 const ReadMore = (() => {
   let s;

   return {

     settings() {
       return {
         content: document.querySelectorAll('.js-read-more'),
         originalContentArr: [],
         truncatedContentArr: [],
         moreLink: "Read More",
         lessLink: "Less Link",
       }
     },

     init() {
       s = this.settings();
       this.bindEvents();
     },

     bindEvents() {
       ReadMore.truncateText();
     },

     /**
      * Count Words
      * Helper to handle word count.
      * @param {string} str - Target content string.
      */
     countWords(str) {
       return str.split(/\s+/).length;
     },

     /**
      * Ellpise Content
      * @param {string} str - content string.
      * @param {number} wordsNum - Number of words to show before truncation.
      */
     ellipseContent(str, wordsNum) {
       return str.split(/\s+/).slice(0, wordsNum).join(' ') + '...';
     },

     /**
      * Truncate Text
      * Truncate and ellipses contented content
      * based on specified word count.
      * Calls createLink() and handleClick() methods.
      */
     truncateText() {

       for (let i = 0; i < s.content.length; i++) {
         //console.log(s.content)
         const originalContent = s.content[i].innerHTML;
         const numberOfWords = s.content[i].dataset.rmWords;
         const truncateContent = ReadMore.ellipseContent(originalContent, numberOfWords);
         const originalContentWords = ReadMore.countWords(originalContent);

         s.originalContentArr.push(originalContent);
         s.truncatedContentArr.push(truncateContent);

         if (numberOfWords < originalContentWords) {
           s.content[i].innerHTML = s.truncatedContentArr[i];
           let self = i;
           ReadMore.createLink(self)
         }
       }
       ReadMore.handleClick(s.content);
     },

     /**
      * Create Link
      * Creates and Inserts Read More Link
      * @param {number} index - index reference of looped item
      */
     createLink(index) {
       const linkWrap = document.createElement('span');

       linkWrap.className = 'read-more__link-wrap';

       linkWrap.innerHTML = `<a id="read-more_${index}" class="read-more__link" style="cursor:pointer;">${s.moreLink}</a>`;

       // Inset created link
       s.content[index].parentNode.insertBefore(linkWrap, s.content[index].nextSibling);

     },

     /**
      * Handle Click
      * Toggle Click eve
      */
     handleClick(el) {
       const readMoreLink = document.querySelectorAll('.read-more__link');

       for (let j = 0, l = readMoreLink.length; j < l; j++) {

         readMoreLink[j].addEventListener('click', function() {

           const moreLinkID = this.getAttribute('id');
           let index = moreLinkID.split('_')[1];

           el[index].classList.toggle('is-expanded');

           if (this.dataset.clicked !== 'true') {
              el[index].innerHTML = s.originalContentArr[index];
              this.innerHTML = s.lessLink;
              this.dataset.clicked = true;
           } else {
             el[index].innerHTML = s.truncatedContentArr[index];
             this.innerHTML = s.moreLink;
             this.dataset.clicked = false;
           }
         });
       }
     },

     /**
      * Open All
      * Method to expand all instances on the page.
      */
     openAll() {
       const instances = document.querySelectorAll('.read-more__link');
         for (let i = 0; i < instances.length; i++) {
           content[i].innerHTML = s.truncatedContentArr[i];
           instances[i].innerHTML = s.moreLink;
         }
       }
     }
 })();

// export default ReadMore;

Usage

ReadMore.js just looks for the class js-read-more.

Specify the desired words before truncation with the data attribute data-rm-words.

For Example


<article>
  <div class="read-more js-read-more" data-rm-words="60">
    <p><!-- some content call here -->

</div> </article>

Initialize


// Init ReadMore
ReadMore.init();

// Or, something like
import ReadMore from './components/_readMore.js'
ReadMore.init();

ToDos

Need to figure out how to handle Read More instances with Ajaxed/Fetched in content, since the word count on existing instances will be already truncated.

Thinking the solution is to destroy and rebuild via a click event. Or, at least open all and rebuild on click.

In The Wild

You can see ReadMore.js throughout the Kawanti Adventures site, especially on Activity Detail Pages like this one.

Github

You can go snag a version of ReadMore.js on the Githubs, Right Here.

The repo includes a few baseline styles and both ES5 and ES6 versions.

Reduced Example

Here's a simple CodePen to demonstrate usage.

Read Next

ExternalLinks.js

Read Story