loading...

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

bhserna profile image Benito Serna Originally published at bhserna.com ・3 min read

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 looks 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 {
  /* Is importat to set a line-height */
  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

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...

And that's all for now =)


This post was originally posted on https://bhserna.com/truncating-multiple-line-text-read-more.html

Discussion

markdown guide