I was working on a recent SharePoint Framework (SPFx) project where one of the requirements was that a list of data be displayed using the DetailsList Office UI Fabric React control and specific columns needed the width set based on the data contained within the list. So they basically wanted to make sure that, in this case the Title and Group Name fields, had a column width that was always at least as wide as the text for those fields so that they wouldn’t start off truncated.

Normally you could probably just set a default minimum width that is at least as large as the known data but you might not know how big (how many characters) the column data is at run time or it might change based on some criteria. So a simple solution is to just measure the rendered text (and corresponding label) for each row of the columns of interest and then use the max value as the width.

To do that you can use a canvas element to render the text in the background and then measure it. In the following code snippet I’ve created a simple static utility method which does just that and then I created another helper method, getFieldWidth() which takes in a collection of data, a column label, the property on the data to inspect and a default minimum value; the getFieldWidth() method calls the getTextWidth() method to measure the text. I also set it up so handle complex types so that I can measure the length of text in a child property of the main object.

 1export default class Utils {
 2    private static _canvas: HTMLCanvasElement | undefined = undefined;
 3    public static getTextWidth(text?: string, font?: string): number {
 4        if (text == null) { return 0; }
 5        // re-use canvas object for better performance
 6        const canvas = this._canvas || (this._canvas = document.createElement("canvas"));
 7        const context = canvas.getContext("2d");
 8        if (context == null) { return 0; }
 9        if (font != null) {
10            context.font = font;
11        }
12        const metrics = context.measureText(text);
13        return metrics.width;
14    }
15
16    public static getFieldWidth(data: any[], label: string, field: string, min: number) {
17        const font = '12px "Segoe UI"';
18        const labelWidth = this.getTextWidth(label, font);
19        if (labelWidth > min) { min = labelWidth; }
20        data.forEach(d => {
21            let obj: any = d;
22            const props = field.split('.');
23            if (props.length > 0) {
24                for (let i = 0; i < props.length; i++) {
25                    obj = obj[props[i]];
26                }
27            } else { obj = obj[field]; }
28            if (obj != null && obj.length > 0) {
29                const l = this.getTextWidth(obj, font);
30                if (l > min) { min = l; }
31            }
32        });
33        return min;
34    }
35}

The following interface defines the data item that I’m using for my example. As you can see, there’s a Group property with a Name property so that I can demonstrate how to measure the width of a child property value.

1export interface IData {
2    Title: string;
3    Description: string;
4    Group?: { Name: string; }
5}

To use the utility methods above you can add a simple DetailsList component to your control and define the columns with a minimum width set for each desired field using the getFieldWidth() utility method. In the example below we only care about the Title and Group.Name fields and set a fixed minimum width for the Description field. Note that because I’m using a complex type for the Group it’s necessary to create a handler for the onRenderItemColumn event so that we can display the group name properly.

 1public render(): React.ReactElement<IDemoProps> {
 2    const { data } = this.state;
 3    let columns: IColumn[] = [
 4        { key: 'Title', fieldName: 'Title', name: 'Title', isResizable: true, minWidth: Utils.getFieldWidth(data, 'Title', 'Title', 50) },
 5        { key: 'Description', fieldName: 'Description', name: 'Description', isResizable: true, minWidth: 150 },
 6        { key: 'Group.Name', fieldName: 'Group.Name', name: 'Group', isResizable: true, minWidth: Utils.getFieldWidth(data, 'Group', 'Group.Name', 50) }
 7    ];
 8
 9    const onRenderItemColumn = (item: IData, index: number, column: IColumn) => {
10        const fieldContent = item[column.fieldName as keyof IData] as any;
11        switch (column.key) {
12            case 'Group.Name':
13                return <span>{item.Group != null ? item.Group.Name : null}</span>;
14            default:
15                return <span>{fieldContent != null ? fieldContent.toString() : null}</span>;
16        }
17    };
18
19    return (
20        <DetailsList
21            items={data}
22            columns={columns}
23            layoutMode={DetailsListLayoutMode.justified}
24            onRenderItemColumn={onRenderItemColumn}>
25        </DetailsList>
26    );
27}

There may be other (and quite possibly much better) ways to do this but I found this approach to be pretty effective at making sure that all the text for a field is completely visible. Of course this won’t necessarily work if you’re using paging or loading data in batches as we can only evaluate the data available but for simple(r) scenarios it’s proven effective enough for me.