mrcesco
mrcesco

Reputation: 49

Typescript + React: define a generic table component

it's been 2 days i'm struggling to make a reusable table but i have problems with types.
I'm not a typescript/react expert because i worked more with javascript so i would like you to show me the right way to develop this component.

Info:

typescript: "3.6.4"
react: "^16.11.0"

"engines": {
 "node": "10.x",
 "npm": "6.x"
}

My idea is (uncomplete and with types errors):

import * as React from 'react';

const getCellData = (fields: Record<string, string>, row) => {
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  return Object.entries(fields).reduce((res: Record<string, any>, val) => {
    res[val[0]] = row[val[1]];
    return res;
  }, {});
};

interface Props<C, R> {
  columns: C[];
  rows: R[];
}

const Table = <C, R>({ columns, rows }: Props<C, R>) => {
  return (
    <div className="table-responsive">
      <table className="table table-striped">
        <thead>
          <tr>
            {columns.map((column, i: number) => (
              <th scope="col" key={i}>
                {column.label}
              </th>
            ))}
          </tr>
        </thead>
        <tbody>
          {rows &&
            rows.map((row, j: number) => {
              return (
                <tr key={j}>
                  {columns.map((column, k: number) => {
                    const { Template, fields } = column;
                    const data = getCellData(fields, row);
                    return <td key={k}>{<Template {...data}></Template>}</td>;
                  })}
                </tr>
              );
            })}
        </tbody>
      </table>
    </div>
  );
};

export default Table;

I'm using the table like this in JSX (Is it right to pass types in jsx like this?):

      <Table<SportsTableColumn, RedaxSport> columns={columns} rows={sports}></Table>

where SportsTableColumn is:

export interface SportsTableColumn {
  label: string;
  Template: typeof TableTdText | typeof TableTdTextWithImage | typeof TableTdBoolCheck;
  fields: {
    text?: string;
    image?: string;
    checked?: string;
  };
}

TableTdText, TableTdTextWithImage, TableTdBoolCheck are React.FC

Then we have the RedaxSport that is a class with properties like:

this.sportId = sportId;
this.urlIcon = urlIcon;
this.redaxDescription = redaxDescription;
this.aamsDescription = aamsDescription;
this.isDailyProgram = isDailyProgram;

"columns" array data example:

[
  {
    label: intl.formatMessage({ id: 'sports.table.redaxDescription' }),
    Template: TableTdTextWithImage,
    fields: {
      text: 'redaxDescription',
      image: 'urlIcon'
    }
  },
  {
    label: intl.formatMessage({ id: 'sports.table.aamsDescription' }),
    Template: TableTdText,
    fields: {
      text: 'aamsDescription'
    }
  },
  {
    label: intl.formatMessage({ id: 'sports.table.dailyProgram' }),
    Template: TableTdBoolCheck,
    fields: {
      checked: 'isDailyProgram'
    }
  }
];

"rows" data example:

[
  {
      "sportId": 0,
      "urlIcon": "https://url.com/",
      "redaxDescription": "This is a description",
      "aamsDescription": "Another description",
      "isDailyProgram": true
  },
  {
      "sportId": 1,
      "urlIcon": "https://url2.com/",
      "redaxDescription": "This is a description 2",
      "aamsDescription": "Another description 2",
      "isDailyProgram": false
  }
]

I just hope to have been clear about my intent and gave you all the info you need. Thank you

Upvotes: 2

Views: 8364

Answers (3)

Saar
Saar

Reputation: 21

Have a look at a generic table for example
It can be reused and does not requires a lot of code
Example of using the generic table:

type MyElement = {
    firstName: string;
    lastName: string;
    age: number;
};

const elements: MyElement[] = [
    { firstName: 'A', lastName: 'A', age: 10 },
    { firstName: 'B', lastName: 'B', age: 11 },
    { firstName: 'C', lastName: 'C', age: 12 },
    { firstName: 'D', lastName: 'D', age: 13 },
    { firstName: 'E', lastName: 'E', age: 14 },
];

const model: TableModel<MyElement> = {
    columns: [
        {
            title: 'Name',
            html: e => <span>{e.firstName} {e.lastName}</span>
        },
        {
            title: 'Firstname',
            html: e => <span>{e.firstName}</span>
        },
        {
            title: 'Lastname',
            html: e => <span>{e.lastName}</span>
        },
        {
            title: 'Age',
            html: e => <span>{e.age}</span>
        },
    ]
};

export const Table = () => {
    return (
        <div className={styles.tableContainer}>
            <GenericTable model={model} elements={elements}></GenericTable>
        </div>
    );
};

Upvotes: 2

mrcesco
mrcesco

Reputation: 49

i replaced Template: React.ComponentClass; with Template: typeof TableTdText | typeof TableTdTextWithImage | typeof TableTdBoolCheck;
but i'm getting
Type 'SportsTableColumn' does not satisfy the constraint 'Column'. Types of property 'Template' are incompatible. Type 'FunctionComponent<OwnProps> | FunctionComponent<OwnProps> | FunctionComponent<OwnProps>' is not assignable to type 'ComponentClass<{}, any>'. Type 'FunctionComponent<OwnProps>' is not assignable to type 'ComponentClass<{}, any>'. Type 'FunctionComponent<OwnProps>' provides no match for the signature 'new (props: {}, context?: any): Component<{}, any, any>'.

Thank you for helping me!

Upvotes: 0

Stutje
Stutje

Reputation: 844

U have to give your columns a type or an interface like:

interface IColumn {
  label: string;
  fields: { [key: string]: string };
  Template: React.ComponentClass;
};

And say to the C generic in your table component that it accepts everything that extends this interface

const Table = <C extends IColumn, R>({ columns, rows }: Props<C, R>) => {...}

So this will let your component know it can use any field from the IColumn interface.

and then extend your SportsTableColumn with IColumn as well

export interface SportsTableColumn extends IColumn {
  label: string;
  Template: React.ComponentClass;
  fields: {
    text?: string;
    image?: string;
    checked?: string;
  };
}

Note: Replace React.ComponentClass to your TdComponents

Upvotes: 1

Related Questions