Benito Serna
Trying to build software that works

Truncating multiple line text... (read-more behavior) with Stimulus.js

Are you looking for a way of implementing a read-more behavior but based on the number of lines instead of on the number of words?

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

I was searching the internet looking how to do this and found a nice article called Line-Based Truncation Methods by Carly Ho that explains different ways of doing it…

In it she describes a nice way of knowing when you should truncate the content based on the line-height of the element.

… The secret is to get the product of the line height and the number of lines that you want to show. With that you can obtain the expected height of the content that you can then compare with the actual height.

The code…

To do the implementation I used Stimulus.js to manipulate the html that look something like this…

<p class="lh-copy" data-controller="read-more">
  <span data-target="read-more.content">
    <%= content %>
  </span>

  <button class="hide"
    data-target="read-more.moreButton"
    data-action="read-more#showMore">
    Ver más
  </button>

  <button class="hide"
    data-target="read-more.lessButton"
    data-action="read-more#showLess">
    Ver menos
  </button>
</p>
.hide {
  display: none;
}

.lh-copy {
  line-height: 1.5;
}

Here is the javascript code using Stimulus…

import { Controller } from "stimulus"

export default class extends Controller {
  static targets = ["content", "moreButton", "lessButton"]
  lines = 3

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

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

  showMore() {
    this.removeContent();
    this.wordsList().forEach((word) => this.addWordToContent(word))
    this.hide(this.moreButtonTarget);
    this.show(this.lessButtonTarget);
  }

  showLess() {
    this.removeContent();
    this.wordsList().forEach((word) => {
      if (this.height() < this.expectedHeight())
        this.addWordToContent(word)
    })

    this.addToContent("...")
    this.hide(this.lessButtonTarget);
    this.show(this.moreButtonTarget);
  }

  show(target) {
    target.classList.remove("hide")
  }

  hide(target) {
    target.classList.add("hide")
  }

  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.lines * this.lineHeight();
  }

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

Demo

See the Pen Read More with Stimulus.js by Benito Serna (@bhserna) on CodePen.

Please go to codepen if you are on iOS and the demo is not working

And if you don’t use Stimulus.js

Don’t worry, stimulus is just a tool that acts like a “glue” between your html and your javascript, but if you are not using it you can just initialize and call the right methods by hand like this…

class ReadMoreController {
  lines = 3

  constructor({content, moreButton, lessButton}) {
    this.contentTarget = content;
    this.moreButtonTarget = moreButton;
    this.lessButtonTarget = lessButton;
  }

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

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

  showMore() {
    this.removeContent();
    this.wordsList().forEach((word) => this.addWordToContent(word))
    this.hide(this.moreButtonTarget);
    this.show(this.lessButtonTarget);
  }

  showLess() {
    this.removeContent();
    this.wordsList().forEach((word) => {
      if (this.height() < this.expectedHeight())
        this.addWordToContent(word)
    })

    this.addToContent("...")
    this.hide(this.lessButtonTarget);
    this.show(this.moreButtonTarget);
  }

  show(target) {
    target.classList.remove("hide")
  }

  hide(target) {
    target.classList.add("hide")
  }

  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.lines * this.lineHeight();
  }

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

const buildSelector = (type, name) => "[data-" + type + "='" + name + "']"
const findElement = (type, name) => document.querySelector(buildSelector(type, name));
const element = findElement("controller", "read-more");
const moreButton = findElement("target", "read-more.moreButton");
const lessButton = findElement("target", "read-more.lessButton");
const controller = new ReadMoreController({
  content: findElement("target", "read-more.content"),
  moreButton: moreButton,
  lessButton: lessButton
})

controller.connect();
moreButton.addEventListener("click", () => controller.showMore());
lessButton.addEventListener("click", () => controller.showLess());

And here is the demo…

See the Pen Read More with Javascript by Benito Serna (@bhserna) on CodePen.

Please go to codepen if you are on iOS and the demo is not working

And that’s all for now =)

Do you need some help with TDD?

I have an email course with with a guide to help you start with TDD!


Do you want to know more?