Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Using mui-datatables in a type-safe way by having access to the original object #475

Open
dandrei opened this issue Mar 8, 2019 · 7 comments

Comments

@dandrei
Copy link

dandrei commented Mar 8, 2019

(largely based on my similar reply to issue #109, but decided it was more appropriate to start an issue as well to get the discussion rolling)

Hi @gregnb, thank you for your great work on this.

I wonder if it's possible use mui-datatables in a more "type-safe" way. Maybe it's already available or maybe it's something that can be easily implemented.

Currently we specify the columns as such:

const columns = [
  {
    name: 'object_field',
    label: 'Object field label'
  }
}

This will take the object_field field from each object in the array sent to data. However, specifying the field name as a string isn't type-safe. TypeScript has no way of knowing whether objects in data contain or don't contain the field object_field.

Question:

Is it possible perhaps, instead of having name as a required field, another option could be added? (let's call it value, required when name isn't specified), that would be used as such:

const columns = [
  {
    value: (obj) => obj.object_field,
    label: 'Object field'
  }
}

This way, we could use it with TypeScript:

value: (obj: KnownType) => obj.object_field,

...and TypeScript would check that object_field does indeed belong to KnownType. Since it's an extra option, it wouldn't interfere with the current way of doing things or JS-only users.

Would also be nice to get the original data object in customBodyRender. Use case for this is: if we want to render one column based on a value from another.

Right now we do have access to the values array in tableMeta.rowData, The caveat is: the order matters. Changing the column order requires a change in how we access tableMeta.rowData. But would be nice to be able to keep the type information if using TypeScript.

So maybe we could have the original object available in the parameters sent to customBodyRender?

What are your thoughts? Thanks!

@gregnb
Copy link
Owner

gregnb commented Mar 8, 2019

hey @dandrei thanks for taking the time to write and explain your issue. This would be a big breaking change and I'm not comfortable at the moment doing this. With that said, if a enough of the user community comes back saying this is affecting them I would consider making this change. I will make this issue for community feedback

@dandrei
Copy link
Author

dandrei commented Mar 8, 2019

@gregnb thanks for your prompt reply. In the meanwhile, I've written an API that sits in front of the mui-datatables API and does exactly what I wanted :).

It takes:

  • A list of objects.
  • Type-aware column definitions (a definition object consists of the column's label, and two functions: one for formatting the value (used for sorting and searching), the other used to render what gets displayed).

It then produces mui-datatables-compatible data and columns that can be plugged directly into <MUIDataTable />.

What happens under the hood is that I use a hidden ({display: "excluded"}) column to hold the raw object data, which I then get to fully access in the customBodyRender of every cell from the MUIDataTableMeta's rowData, when rendering.

The data sent to <MUIDataTable /> will be an array of objects with the following structure:

{
  data: /* the original raw object */
  data1: /* value for the first column */
  ...
  dataN: /* value for the Nth column */
}

As mentioned previously, the data1 ... dataN fields will be the values used for sorting and searching, while the original raw object will be used when displaying the data.

Long story short, here's how I use it:

const definitions: ColumnDefinitions<MyObjectType> = [
  {
    title: "Column 1",
    value: x => x.field1,
    render: x => <>{x.field1}</>
  },
  {
    title: "Column 2",
    value: x => x.field2,
    render: x => <>{x.field2}</>
  },
]

const [columns, data] = muiFormat(definitions, originalData);

Note 1: The functions passed to value and render will know that MyObjectType contains fields field1, field2. Parameter originalData is of type MyObjectType[]

Note 2: Real-life use cases are much more complex than just rendering the raw value. I'm drawing graphs, rendering the same data differently based on the column it's in, using values from multiple fields to render a single cell, etc. This logic can all be painlessly specified in the definitions object above.

This is the code that's working behind the scenes, if anyone's interested:

import {MUIDataTableColumnDef} from "mui-datatables";

type MUIDataTableMeta = {
    rowData: Array<unknown>
    rowIndex: number
}

type CustomBodyRender<T> = (v: T | string | number, a: MUIDataTableMeta) => JSX.Element | string

type ColumnBuilder<T> = {
    name: string,
    label: string,
    render: CustomBodyRender<T>,
    options?: object
}

export function makeMuiColumn<T>({name, label, render, options}: ColumnBuilder<T>): MUIDataTableColumnDef {
    return {
        label,
        name,
        options: {customBodyRender: render, ...options}
    };
}

export type ColumnDefinition<T> = {
    label: string,
    value?: (data: T, rowIndex: number) => string | number,
    render?: (data: T, rowIndex: number) => JSX.Element | string
}

export type ColumnDefinitions<T> = Array<ColumnDefinition<T>>

const getColumn = (i: number) => `data${i}`;

function makeMuiColumns<T>(definitions: ColumnDefinitions<T>) {
    return [
        makeMuiColumn({
            name: "data",
            label: "__DATA__",
            render: () => "",
            options: {display: "excluded"}
        }),

        ...definitions.map((definition, i) => {
            const {label, render} = definition;
            return makeMuiColumn({
                name: getColumn(i),
                label,
                render: (v, a) => {
                    if (render === undefined) {
                        return `${v}`;
                    } else if (Array.isArray(a.rowData)) {
                        const [data] = a.rowData as unknown as [T];
                        return render(data, a.rowIndex);
                    }
                    return "";
                }
            })
        })
    ]
}

type MuiRowData<T> = { data: T } & { [column: string]: string | number }

type MuiTableData<T> = Array<MuiRowData<T>>

function muiData<T>(tableRows: T[], definitions: ColumnDefinitions<T>): MuiTableData<T> {
    return tableRows.map((tableRow, rowIndex) => {
        return {
            ...definitions.reduce((acc, definition, i) => {
                const {value} = definition;
                if (value === undefined) {
                    return {...acc, [getColumn(i)]: ""};
                }
                return {...acc, [getColumn(i)]: value(tableRow, rowIndex)}
            }, {data: tableRow} as unknown as MuiRowData<T>)
        }
    })
}

export function muiFormat<T>(cols: ColumnDefinitions<T>, tableRows: T[]): [MUIDataTableColumnDef[], MuiTableData<T>] {
    return [
        makeMuiColumns(cols),
        muiData(tableRows, cols)
    ]
}

@erencay
Copy link
Contributor

erencay commented Mar 11, 2019

So maybe we could have the original object available in the parameters sent to customBodyRender?

I second better typescript support and flexible value resolution, but putting those points aside, the lacking source object parameter in customBodyRender method is an appreciable drawback. I wanted to render two fields of an object into one column but I was forced to add the other field to schema as an excluded field. (Which also changed row index, since it matters).

@rossknudsen
Copy link
Contributor

Hey,

How about just leveraging the TS compiler for strong typing of the data? I have just taken a few minutes to modify the Definitely Typed typings here.

It just uses generics to infer the type of the data and also uses keyof T to require that the names of the columns are actually keys of the data values.

Give it a test and if you think it works, then feel free to submit a PR to the definitely typed repo.

@jwindridge
Copy link

I would like to express my support for @dandrei 's approach (and thank them for the code!) - I think it would also help with issues such as #147, #403, #531 by allowing the calling code to customise which values are passed into the row as well as how they are rendered.

@dandrei
Copy link
Author

dandrei commented May 15, 2019

Quick update: out of necessity, I added another parameter that I can optionally add to any column definition: labelStyle.

How I use it:

const definitions: ColumnDefinitions<DataType> = [
    {
        label: "Date",
        labelStyle: {textAlign: "left", transform: "rotate(-45deg)"},
        value: x => x.month,
        render: x => <div>{mmmYyyyDate(x.month)}</div>,
    },
];

This allows me to change how any column is displayed right there in the definition.
Fields label and labelStyle have to do with what the label reads and how it looks.
Fields value and render have to do with how the content is sorted on (value) and how it's displayed as a function of mapping each individual DataType object (x is of type DataType in the example).

That definition gets into the mui-datatables API by using a custom customHeadRender:

customHeadRender: (o: any, updateDirection) => {
    return <th
    	onClick={() => updateDirection(o.index)}
    	style={{
    		fontWeight: 500,
    		cursor: "pointer",
    		padding: "20px",
    		textAlign: "left",
    		...labelStyle
    	}}>
    	{o.label}
    	<TableSortLabel active={o.sortDirection !== null} direction={o.sortDirection || "asc"}/>
    </th>;
}

@Teslima02
Copy link

Teslima02 commented Jun 30, 2019

@dandrei please explain more on how i can use this solution, have been trying to implement it but am not getting it work. Thanks

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

No branches or pull requests

6 participants