javascriptstring

Add an ellipsis to middle of long string in React16


I am trying to add an ellipsis to the mid-point in a string with the following complications:

I have a plunk here to illustrate it. The script only assumes one instance, but you should get the idea:

(function(){
    // variables
    var parent = document.querySelectorAll(".wrapper")[0],
    parentWidth = parent.clientWidth,x = 0, elem, hellip
    txtStr = document.querySelector("#shorten"),
    strWidth = txtStr.clientWidth,
    strTxt = txtStr.innerText,
    ending = document.createElement("span"),
    endTxt = strTxt.slice(Math.max(strTxt.length - (strTxt.length / 4))) || endTxt;
    txtStr.style.overflow = "hidden"
    txtStr.style.textOverflow = "ellipsis"
    ending.appendChild(document.createTextNode(endTxt))
    ending.classList.add("ellipsis")
    document.querySelectorAll(".wrapper")[0].appendChild(ending)
    var ell = function(a, b){
        if (a <= b){
            ending.classList.add("visible")
        }
        else {
            ending.classList.remove("visible")
        }
    }
    ell(parentWidth, strWidth) // We need to display any changes immediately
    window.onresize = function(){ // if the window is resized, we also need to display changes
        hellip = document.querySelectorAll(".ellipsis")[0].clientWidth
        parentWidth = parent.clientWidth
        // the use of 'calc()' is because the length of string in px is never known
        txtStr.style.width = "calc(100% - " + hellip + "px"
        ell(parentWidth, strWidth)
    }
})();

It's a bit clunky, but demonstrates the idea.

The issue I am having in React 16, is that the string is not rendered at the point I need to measure it to create the bit of text at the end. Therefore, when the new node is created it has no dimensions and cannot be measured as it doesn't exist in the DOM.

The functionality works - sort of as the screen resizes, but that's beside the point. I need to get it to do the do at render time.

The actual app is proprietary, and I cannot share any of my code from that in this forum.

EDIT: Another thing to bare in mind (teaching to suck eggs, here) is that in the example, the script is loaded only after the DOM is rendered, so all of the information required is already there and measurable.


Solution

  • Thank you to all that looked at this, but I managed to figure out the problem.

    Once I worked out the finesse of the lifecycle, it was actually still quite tricky. The issue being measuring the original string of text. Looking back now, it seems insignificant.

    Essentially, I pass a few elements into the component as props: an id, any required padding, the length of the ending text required for context and the text (children).

    Once they are in, I need to wait until it is mounted until I can do anything as it all depends on the DOM being rendered before anything can be measured. Therefore, componentDidMount() and componentDidUpdate() are the stages I was interested in. componentWillUnmount() is used to remove the associated event listener which in this instance is a resize event.

    Once mounted, I can get the bits required for measuring: the element and importantly, its parent.

    getElements(){
      return {
        parent: this.ellipsis.offsetParent,
        string: this.props.children
      }
    }
    

    Then, I need to make sure that I can actually measure the element so implement some inline styles to allow for that:

    prepareParentForMeasure(){
      if(this.getElements().parent != null){
        this.getElements().parent.style.opacity = 0.001
        this.getElements().parent.style.overflow = 'visible'
        this.getElements().parent.style.width = 'auto'
      }
    }
    

    As soon as I have those measurements, I removed the styles.

    At this point, the script will partially work if I carry on down the same path. However, adding an additional element to work as a guide is the kicker.

    The returned element is split into three elements (span tags), each with a different purpose. There is the main bit of text, or this.props.children, if you like. This is always available and is never altered. The next is the tail of the text, the 'n' number of characters at the end of the string that are used to contextually display the end of the string - this is given a class of 'ellipsis', although the ellipsis is actually added to the original and first element. The third is essentially exactly the same as the first, but is hidden and uninteractable, although it does have dimensions. This is because the first two - when rendered - have different widths and cannot be relied upon as both contribute to the width of the element, whereas the third doesn't.

    <Fragment>
      <span className='text'>{this.props.children}</span>
      <span className='ellipsis'>{this.tail()}</span>
      <span className='text guide' ref={node => this.ellipsis = node}>
        {this.props.children}</span>
    </Fragment>
    

    These are in a fragment so as to not require a surrounding element.

    So, I have the width of the surrounding parent and I have the width of the text element (in the third span). which means that if I find that the text string is wider than the surrounding wrapper, I add a class to the ellipsis span of 'visible', and one to the 'text' element of 'trimmed', I get an ellipsis in the middle of the string and I use the resize event to make sure that if someone does do that, all measurements are re-done and stuff is recalculated and rendered accordingly.