Photo by Timothy Cuenat on Unsplash
Twice the Challenge: Optimising Middle Truncation for Modern UIs
How I had the opportunity to solve the same problem twice, and what I did to optimise the solution the second time around.
Table of contents
- My First Attempt
- The Problems with My Initial Solution
- A Fresh Perspective at My Second Job
- Step 1: Handling Responsiveness with ResizeObserver
- Step 2: Measuring the Width of Text
- Step 3: Calculating the Width of the Ellipsis Character
- Step 4: Determining the Maximum Length of the String
- Step 5: Dynamically Truncating the String
When I landed my first tech job as a Full-Stack Developer, one of my first tasks was to create a middle truncate component. The goal was to handle long file names in a file uploader by truncating the middle portion of the file name while keeping the extension. For example, if a user uploaded a file with a long name, it would display something like this:
AnilPakFr…Performio.pdf
My First Attempt
As a Junior Developer, my first solution seemed simple and effective.
Identified the index of the file extension (using the dot).
Measured the total character length.
Sliced the file name to keep the first and last few characters.
Added three dots (
...
) in the middle, followed by the extension.
function getFileName(fileName: string): string {
const dotIndex = fileName.lastIndexOf('.');
if (dotIndex === -1 || dotIndex === 0) {
// No extension found or invalid name, treat as a regular string
return fileName.length > 22
? fileName.substr(0, 11) + '...' + fileName.substr(-11)
: fileName;
}
const extension: string = fileName.substr(dotIndex);
const baseName: string = fileName.substr(0, dotIndex);
if (baseName.length > 22) {
return baseName.substr(0, 11) + '...' + baseName.substr(-11) + extension;
}
return fileName;
}
// Example Usage
const fileName: string = "AnilPakFrontEndPerformioComponent.pdf";
console.log(getFileName(fileName)); // Output: "AnilPakFron...mponent.pdf"
I also looked online for references and found similar approaches, which reassured me I was on the right track. It was straightforward and seemed practical at the time. However, I later realised it wasn’t an ideal solution.
The Problems with My Initial Solution
The first major flaw was that this approach didn’t account for the fact that not all characters have the same width (Different Font Families will have different character widths). For instance, the letters W and i are vastly different in size, and my method assumed equal width for all characters.
Additionally, I hadn’t considered responsiveness. On smaller screens, even the truncated names could overflow or break the layout. Supporting various screen sizes would mean creating multiple versions of the middle truncate logic — for instance, showing the first and last 8 characters on large screens but reducing that number as the screen size decreases.
A Fresh Perspective at My Second Job
When I started my second job as a Front-End Engineer, I encountered the same task: create a middle truncate component for a file uploader. However, this time, I wanted a dynamic solution that could handle responsiveness and automatically recalculate the truncated string whenever the container was resized.
Let’s walk through the steps I took to achieve this:
Step 1: Handling Responsiveness with ResizeObserver
The first challenge was ensuring the truncated file name adjusted dynamically when the container was resized. This required listening for changes in the container’s dimensions. I used the ResizeObserver
API, which efficiently observes size changes in an element and triggers a callback.
const observer = new ResizeObserver(() => {
// Recalculate truncation logic when container resizes
const containerWidth = container.clientWidth; // Get the container's new width
});
observer.observe(container); // Start observing the container
This way, we no longer need to manually handle window resize events.
Step 2: Measuring the Width of Text
The next step was to measure the width of any given text. Different characters have varying widths (e.g., “W” is wider than “i”), so I created a helper function that temporarily adds an invisible <span>
element to the DOM measures its width and then removes it.
const getTextWidth = (text: string, container: HTMLElement = document.body) => {
const span = document.createElement('span');
span.style.opacity = '0'; // Make the span invisible
span.style.position = 'absolute'; // Position it off-screen
span.style.whiteSpace = 'nowrap'; // Prevent wrapping
span.innerText = text; // Set the text to measure
container.appendChild(span); // Append span for measurement
const width = span.clientWidth; // Get the width
container.removeChild(span); // Clean up
return width;
};
This function gives us an accurate width for any string in the current font and style.
Step 3: Calculating the Width of the Ellipsis Character
The ellipsis (…
) is a key part of truncation, and its width also varies depending on the font. I used the same getTextWidth
function to calculate its width within the container.
const getEllipsisWidth = (container: HTMLElement) =>
getTextWidth('\u2026', container); // Unicode for the ellipsis character
Knowing the width of the ellipsis helps us determine how much space is left for the start and end parts of the truncated string.
Step 4: Determining the Maximum Length of the String
With the container width and ellipsis width in hand, I calculated how many characters of the file name could fit within the remaining space.
const getMaxStringLength = (containerWidth: number, ellipsisWidth: number) => {
return Math.floor((containerWidth - ellipsisWidth) / getTextWidth('W')); // Assuming 'W' as the widest character
};
This calculation gives us a starting point to determine how much of the file name can fit before and after the ellipsis.
Step 5: Dynamically Truncating the String
Finally, I wrote a function to slice the string into a start segment, an ellipsis (…
), and an end segment, ensuring the total fits within the calculated maximum length.
const truncateString = (str: string, maxLength: number) => {
if (str.length <= maxLength) return str; // If it already fits, return as is
const ellipsis = '\u2026';
const startLength = Math.ceil((maxLength - ellipsis.length) / 2);
const endLength = Math.floor((maxLength - ellipsis.length) / 2);
const start = str.slice(0, startLength);
const end = str.slice(str.length - endLength);
return start + ellipsis + end; // Combine start, ellipsis, and end
};
With the middle truncate applied, this is how the long file name will look!
Final Thoughts
Revisiting code you wrote a few years ago can be both eye-opening and humbling. You might wonder, Why did I do it that way? Why didn’t I come up with a better solution? But that’s all part of the growth process I think.
I’m glad I had the chance to revisit this challenge a year later and implement a more reliable, dynamic, and user-friendly approach :)
While this solution works well for me, I know there’s always room for improvement.
You might think this solution is a bit of overkill, but I don’t believe so :)
What about you? Have you encountered a similar challenge, or do you have an alternate way to handle middle truncation for long file names?
I’d love to hear your thoughts and ideas in the comments.
Here’s how the final code looks:
https://gist.github.com/anLpk/3b0830226461a0db324780e71779ee15