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

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.