Benito Serna Tips and tools for Ruby on Rails developers

A "read-more" behavior truncating by number of lines with custom javascript and Stimulus.js

April 2, 2021

Are you looking for a way of implementing a “read-more” behavior truncating by the number of lines instead of the number of words?

If this is your case, this article can help you =) …

The example

Visit example page

The html and css

Your are going to need…

html { line-height: 1.5 }
.hide { display: none; }
<div data-controller="read-more"
     data-read-more-lines-value="3"
     data-read-more-hide-class="hide"
     data-action="resize@window->read-more#render">
  <p data-read-more-target="content">
    The content that you want to truncate...
  </p>
  <button class="hide"
    data-read-more-target="moreButton"
    data-action="read-more#showMore">
    Show more
  </button>
  <button class="hide"
    data-read-more-target="lessButton"
    data-action="read-more#showLess">
    Show less
  </button>
</div>

The javascript

On render you are going to truncate the content, to the configured linesValue.

But you are going to do it just if the height is greater that the expectedHeigt that is the product of the linesValue with the lineHeight.

class ReadMoreController extends Stimulus.Controller {
  static get targets() {
    return ["content", "moreButton", "lessButton"]
  }

  static get classes() {
    return ["hide"]
  }

  static get values() {
    return { lines: Number }
  }

  connect() {
    this.content = this.contentTarget.textContent;
    this.render()
  }

  render() {
    this.showAllContent()

    if (this.height() > this.expectedHeight()) {
      this.showLess()
    } else {
      this.showAllContent()
      this.hide(this.moreButtonTarget);
      this.hide(this.lessButtonTarget);
    }
  }

  showMore() {
    this.showAllContent();
    this.hide(this.moreButtonTarget);
    this.show(this.lessButtonTarget);
  }

  showLess() {
    this.truncateContent();
    this.hide(this.lessButtonTarget);
    this.show(this.moreButtonTarget);
  }

  showAllContent() {
    this.removeContent();
    this.wordsList().forEach((word) => this.addWordToContent(word))
  }

  truncateContent() {
    this.calculateWordsToDisplayWhenTruncated();
    this.renderTrucatedContentWithEllipsis();
  }

  calculateWordsToDisplayWhenTruncated() {
    this.wordsToDisplayWhenTrucated = [];
    this.removeContent();
    this.wordsList().forEach((word) => {
      if (this.height() < this.expectedHeight()) {
        this.wordsToDisplayWhenTrucated.push(word)
        this.addWordToContent(word)
      }
    })
  }

  renderTrucatedContentWithEllipsis() {
    this.wordsToDisplayWhenTrucated.pop()
    this.removeContent();
    this.wordsToDisplayWhenTrucated.forEach((word) => this.addWordToContent(word))
    this.addToContent("...")

    if (this.height() > this.expectedHeight()) {
      this.renderTrucatedContentWithEllipsis()
    }
  }

  show(target) {
    target.classList.remove(this.hideClass)
  }

  hide(target) {
    target.classList.add(this.hideClass)
  }

  removeContent() {
    this.contentTarget.textContent = "";
  }

  addWordToContent(word) {
    this.addToContent(" " + word);
  }

  addToContent(text) {
    this.contentTarget.textContent += text
  }

  lineHeight() {
    let style = window.getComputedStyle(this.contentTarget)
    return parseFloat(style.lineHeight, 10);
  }

  height() {
    return this.contentTarget.offsetHeight;
  }

  expectedHeight() {
    return this.linesValue * this.lineHeight();
  }

  wordsList() {
    return this.content.split(" ")
  }
}

Other ways of doing it…

If you are looking for a way to do the truncation using css line-clamp, you take a look to this example using “line-clamp”.

And if you can truncate by number of characters, you can check this example using the “truncate” helper.

Weekly tips and tools for Ruby on Rails developers

I send an email each week, trying to share knowledge and fixes to common problems and struggles for ruby on rails developers, like How to fetch the latest-N-of-each record or How to test that an specific mail was sent or a Capybara cheatsheet. You can see more examples on Most recent posts or All post by topic.