12

I need to measure the pixel length of individual words (strings) in a container. I am shrinking each div's width so when the text wraps it is perfectly flush against its container, as there is a max-width that causes it to wrap. This solution (and its use case) have previously been answered in Jquery on this answer:

Shrink DIV to text that's wrapped to its max-width?

However, the above solution places each word in a span and uses Jquery's .width() method to calculate its physical size. As I am using React, I do not have this option.

While I have been successful in using the offsetWidth method to find the width of an item that has been rendered to the DOM, I have not found other viable solutions that don't involve rendering the item to the dom I can use to find the width of a single world.

Some solutions I have found online provide methods that factor in the font, but the font could change, so I need a method more like Jquery's .width().

Does React/JavaScript (not Jquery) have any means of determining the physical length of a word in pixels without having to render the item to the dom and without putting the font type into a function?

Dog
  • 2,726
  • 7
  • 29
  • 66
  • Not sure I follow: React running in the brower is literally just Javascript running in the browser. if JS can do it, JS can do it in your React classes. Having said that, can you explain why you _actually_ need to do this? Because it is incredibly unlikely you actually need pixel perfect text bounding boxes, and instead you need some reasonably sized flexbox/grid with sensible media-queries for page width. – Mike 'Pomax' Kamermans Nov 05 '19 at 05:37
  • 1
    Jquery can do it, not JavaScript – Dog Nov 05 '19 at 05:38
  • Flexbox isn't effective for this either. The problem is better explained here: https://stackoverflow.com/questions/14596213/shrink-div-to-text-thats-wrapped-to-its-max-width/14597951 – Dog Nov 05 '19 at 05:40
  • That question doesn't give a good use-case, either. I can't think of a single reason why you would need to do this unless you were writing a typesetting solution (e.g. you were implementing LaTeX in the browser). As for "jquery can do it, not javascript": jquery _is written in Javascript_, so if jquery can do it, that's because javascript allows for it. E.g. create a span with your text, set its css to not wrap, and use [getBoundingClientRect](https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect), measure it on mount, then statefully bind the resulting value. – Mike 'Pomax' Kamermans Nov 05 '19 at 05:46
  • getBoundingClientRect needs the element rendered to the dom – Dog Nov 05 '19 at 05:59
  • Which is the case in `componentDidMount`, except you're in JS so you're literally free to write a function that just creates a span, makes it sized but invisible with CSS (e.g. absolute positioned at 0,0 with background and color `transparent`), tacks it onto document.body, gets the boundingrect, and then immediately removes it from the body again. Remember: just because you're in React doesn't mean you not also "just in the browser". You can run any code you want, and as long as you make sure that code touches "not react-managed DOM", React lifecycles won't ever interfere. – Mike 'Pomax' Kamermans Nov 05 '19 at 17:54

2 Answers2

38

You can use measureText of canvas like below:

function getTextWidth(text, font) {
  const canvas = document.createElement('canvas');
  const context = canvas.getContext('2d');

  context.font = font || getComputedStyle(document.body).font;

  return context.measureText(text).width;
}

You can just pass the font if you need to change it, it's basically a CSS font declaration.

function getTextWidth(text, font) {
  const canvas = document.createElement('canvas');
  const context = canvas.getContext('2d');

  context.font = font || getComputedStyle(document.body).font;

  return context.measureText(text).width;
}

const elements = document.querySelectorAll('h1, h2, h3, h4, h5, h6');

elements.forEach(element => {
  const { font } = getComputedStyle(element);

  element.insertAdjacentHTML('afterend', `
    <p class="text-details sans-serif">
      <span class="computed-font">${font}</span>
      <span class="computed-width">${getTextWidth(element.innerText, font)}</span>
    </p>
  `);
});
h1, h2, h3, h4, h5, h6 {
  font-family: serif;
  margin: 0;
}

.sans-serif {
  font-family: sans-serif;
}

.text-details {
  font-size: 12px;
  margin: 0 0 20px;
}
<h1>Hello, World</h1>
<h2>Hello, World</h2>
<h3>Hello, World</h3>
<h4>Hello, World</h4>
<h5>Hello, World</h5>
<h6>Hello, World</h6>
<h1 class="sans-serif">Hello, World</h1>
<h2 class="sans-serif">Hello, World</h2>
<h3 class="sans-serif">Hello, World</h3>
<h4 class="sans-serif">Hello, World</h4>
<h5 class="sans-serif">Hello, World</h5>
<h6 class="sans-serif">Hello, World</h6>
dork
  • 4,396
  • 2
  • 28
  • 56
  • 1
    How does the font change? Does it rely solely on the element itself? Would it work if you just get the computed style of the element and get its font? – dork Nov 05 '19 at 06:02
  • Could that be done in a function? For instance, if I didn't have the style or font to enter as a parameter. If I could pull those dynamically that would be effective. – Dog Nov 05 '19 at 06:03
  • 2
    Something like this: const elements = document.querySelectorAll('.my-elements'); elements.forEach(element => { console.log(getTextWidth(element.innerText, getComputedStyle(element).font)); }); – dork Nov 05 '19 at 06:06
1

Please try the following solution.

    var getWidthOfText = function(text, styles) {
        var isObjectJSON = function(obj) {
            return obj && typeof obj === 'object' && !Array.isArray(obj);
        };

        var element = document.createElement('div');
        if (isObjectJSON(styles)) {
            var styleKeys = Object.keys(styles);
            for (var i = 0, n = styleKeys.length; i < n; ++i) {
                element.style[styleKeys[i]] = styles[styleKeys[i]];
            }
        }
        element.style.display = 'inline-block';
        element.innerHTML = text;
        document.body.appendChild(element);
        var width = element.offsetWidth;
        document.body.removeChild(element);
        return width;
    };

You need to pass the first argument as the text/HTML for which you want to check the width and second argument if you want to append any styles to the outer layer.

Nikhil Goyal
  • 1,945
  • 1
  • 9
  • 17