Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
String Truncation Algorithm

Naive

const TRUNCATE_WORDS_CUTOFF = 12;

export const truncateName = (name: string, truncateCutoff: number = TRUNCATE_WORDS_CUTOFF) =>
    name.length > truncateCutoff
        ? `${name.substring(0, truncateCutoff)}...`
        : name;

But depending on the font, characters in the string will have different widths. We don't necessarily want to hardcode the cutoff.

Better

To solve this problem, there's a CSS approach and a hash table approach where we map each characters to a weight representing its width (highly dependent on the font). The hash table approach is overkill. The CSS approach is simpler. Simply add the following attribute:

.truncatedName {
  width: 100px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
render() {
    const { name } = this.props;
    return <div className={styles.truncatedName}>{name}</div>
}

For more on CSS truncation of text, see this

If we also want to detect when the string truncation has occurred, we can use ref callback

const MAX_DISPLAY_NAME_WIDTH = 100; // number based on the CSS rule for truncating

constructor(props: {name: string}) {
    super(props);

    this.state = {
        displayTooltip: false
    };
}

getDisplayNameWidth = (element: HTMLInputElement) => {
    if (element) {
        const { width } = element.getBoundingClientRect();
        if (width >= MAX_DISPLAY_NAME_WIDTH) {
            this.setState({ displayTooltip: true });
        }
    }
};

renderDisplayName(name: string) {
    return <div className={styles.truncatedName} ref={this.getDisplayNameWidth}>{name}</div>
}

render() {
    const { name } = this.props;
    const { displayTooltip } = this.state;
    
    if (displayTooltip) {
        return (
            <Tooltip title={name}>
                {this.renderDisplayName(name)}
            </Tooltip>
        );
    }
    return this.renderDisplayName(name);
}

Even Better

The better solution outlined above provides a way to detect if the CSS truncation occurred during initial mount, but it doesn't account for changes to the name length after component's initial mount. An improvement upon the previous solution is as follows:

const MAX_DISPLAY_NAME_WIDTH = 100; // number based on the CSS rule for truncating

private displayNameRef: React.RefObject<HTMLInputElement> = React.createRef();

constructor(props: {name: string}) {
    super(props);
    this.state = {
        displayTooltip: false
    };
}

componentDidMount() {
    this.updateDisplayTooltip();
}

componentDidUpdate() {
    this.updateDisplayTooltip();
}

updateDisplayTooltip() {
    if (!this.displayNameRef.current) return;
    
    const { width } = this.displayNameRef.current.getBoundingClientRect();
    this.setState({ displayTooltip: width >= MAX_DISPLAY_NAME_WIDTH });
}

renderDisplayName(name: string) {
    return <div className={styles.truncatedName} ref={this.displayNameRef}>{name}</div>
}

render() {
    const { name } = this.props;
    const { displayTooltip } = this.state;
    
    return displayTooltip ?
        (
            <Tooltip title={name}>
                {this.renderDisplayName(name)}
            </Tooltip>
        ) : this.renderDisplayName(name);
}

According to the official documentation for React Refs and DOM,

React will assign the current property with the DOM element when the component mounts, and assign it back to null when it unmounts. ref updates happen before componentDidMount or componentDidUpdate lifecycle methods.

This is why we need to update displayTooltip in both componentDidMount and componentDidUpdate methods.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment