0

I have a List component and I would like to pass listItems from a parent. I just need to enforce that the prop I am passing is of Divider or ListItem component. How can I enforce that? I do not want to do at the run time as in this example but rather at compile time. Please help. Also how do I add keys while rendering the list.

This is my codesandbox.

index.tsx

import * as React from "react";
import ReactDOM from "react-dom/client";
import { StyledEngineProvider } from "@mui/material/styles";
import ListItem from "@mui/material/ListItem";
import ListItemButton from "@mui/material/ListItemButton";
import Divider from "@mui/material/Divider";
import ListItemText from "@mui/material/ListItemText";
import Demo from "./demo";

ReactDOM.createRoot(document.querySelector("#root")).render(
  <React.StrictMode>
    <StyledEngineProvider injectFirst>
      <Demo
        additionalMenuItems={[
          <ListItem disablePadding>
            <ListItemButton>
              <ListItemText primary="Car" />
            </ListItemButton>
          </ListItem>,
          <Divider />,
          <ListItem disablePadding>
            <ListItemButton>
              <ListItemText primary="Bike" />
            </ListItemButton>
          </ListItem>
        ]}
      />
    </StyledEngineProvider>
  </React.StrictMode>
);

demo.tsx

import * as React from "react";
import Box from "@mui/material/Box";
import List from "@mui/material/List";
import ListItem from "@mui/material/ListItem";
import ListItemButton from "@mui/material/ListItemButton";
import ListItemIcon from "@mui/material/ListItemIcon";
import ListItemText from "@mui/material/ListItemText";
import Divider from "@mui/material/Divider";
import InboxIcon from "@mui/icons-material/Inbox";
import DraftsIcon from "@mui/icons-material/Drafts";

interface Props {
  additionalMenuItems: (typeof Divider | typeof ListItem)[];
}

const BasicList = (props: Props) => {
  return (
    <Box sx={{ width: "100%", maxWidth: 360, bgcolor: "background.paper" }}>
      <nav aria-label="main mailbox folders">
        <List>
          <ListItem disablePadding>
            <ListItemButton>
              <ListItemIcon>
                <InboxIcon />
              </ListItemIcon>
              <ListItemText primary="Inbox" />
            </ListItemButton>
          </ListItem>
          <ListItem disablePadding>
            <ListItemButton>
              <ListItemIcon>
                <DraftsIcon />
              </ListItemIcon>
              <ListItemText primary="Drafts" />
            </ListItemButton>
          </ListItem>
        </List>
      </nav>
      <Divider />
      <nav aria-label="secondary mailbox folders">
        <List>
          <ListItem disablePadding>
            <ListItemButton>
              <ListItemText primary="Trash" />
            </ListItemButton>
          </ListItem>
          <ListItem disablePadding>
            <ListItemButton component="a" href="#simple-list">
              <ListItemText primary="Spam" />
            </ListItemButton>
          </ListItem>
          {props.additionalMenuItems}
        </List>
      </nav>
    </Box>
  );
};

export default BasicList;
arunmmanoharan
  • 2,535
  • 2
  • 29
  • 60
  • If you want it at compile time, you're gonna need to adopt TypeScript. You cant do it with plain javascript as its not a strongly typed language. – adsy Sep 07 '22 at 18:10
  • I am using Typescript. – arunmmanoharan Sep 07 '22 at 18:11
  • Lol! Sorry I dont know how I missed that – adsy Sep 07 '22 at 18:11
  • Give me a sec to think about it – adsy Sep 07 '22 at 18:11
  • 1
    Sadly its not possible! Not supported in TS yet. Heres the github issue https://github.com/microsoft/TypeScript/issues/21699 – adsy Sep 07 '22 at 18:20
  • Only thing you could do (but it sucks) is instead have users pipe an array of objects in with keys for type (listitem or divider) and children then resolve that inside your component. – adsy Sep 07 '22 at 18:20
  • Can you give me an example? – arunmmanoharan Sep 07 '22 at 18:23
  • Sure, give me some mins – adsy Sep 07 '22 at 18:23
  • Added. I'd argue that its not worth damaging the ergonomics though personally. TS support will come eventually and everyone is having to do the same thing re: runtime checking. – adsy Sep 07 '22 at 18:35
  • (its also kind of a lost cause therefore -- even if you do this, someone can just violate the inner children as well by using another button other than ListItemButton -- unless you choose to have those as properties in the object too, which seems somewhat restrictive as you are limiting the expressiveness of JSX. Might be ok though if you want to avoid giving the consumers freedom.) – adsy Sep 07 '22 at 18:38

1 Answers1

1

Sadly, it isnt possible because of https://github.com/microsoft/TypeScript/issues/21699. We have to wait for TS support.

You could instead have the user pass a typed object which describes the structure, then resolve this in your core component.

index.tsx

import * as React from "react";
import ReactDOM from "react-dom/client";
import { StyledEngineProvider } from "@mui/material/styles";
import ListItem from "@mui/material/ListItem";
import ListItemButton from "@mui/material/ListItemButton";
import Divider from "@mui/material/Divider";
import ListItemText from "@mui/material/ListItemText";
import Demo from "./demo";

ReactDOM.createRoot(document.querySelector("#root")).render(
  <React.StrictMode>
    <StyledEngineProvider injectFirst>
      <Demo
        additionalMenuItems={[
          {
            type: "listitem",
            children: (
              <ListItemButton>
                <ListItemText primary="Car" />
              </ListItemButton>
            )
          },
          { type: "divider" },
          {
            type: "listitem",
            children: (
              <ListItemButton>
                <ListItemText primary="Bike" />
              </ListItemButton>
            )
          }
        ]}
      />
    </StyledEngineProvider>
  </React.StrictMode>
);

demo.tsx

import * as React from "react";
import Box from "@mui/material/Box";
import List from "@mui/material/List";
import ListItem from "@mui/material/ListItem";
import ListItemButton from "@mui/material/ListItemButton";
import ListItemIcon from "@mui/material/ListItemIcon";
import ListItemText from "@mui/material/ListItemText";
import Divider from "@mui/material/Divider";
import InboxIcon from "@mui/icons-material/Inbox";
import DraftsIcon from "@mui/icons-material/Drafts";

interface Props {
  additionalMenuItems: Array<{type: 'divider'} | {type: 'listitem', children: JSX.Element}>;
}

const BasicList = (props: Props) => {
  return (
    <Box sx={{ width: "100%", maxWidth: 360, bgcolor: "background.paper" }}>
      <nav aria-label="main mailbox folders">
        <List>
          <ListItem disablePadding>
            <ListItemButton>
              <ListItemIcon>
                <InboxIcon />
              </ListItemIcon>
              <ListItemText primary="Inbox" />
            </ListItemButton>
          </ListItem>
          <ListItem disablePadding>
            <ListItemButton>
              <ListItemIcon>
                <DraftsIcon />
              </ListItemIcon>
              <ListItemText primary="Drafts" />
            </ListItemButton>
          </ListItem>
        </List>
      </nav>
      <Divider />
      <nav aria-label="secondary mailbox folders">
        <List>
          <ListItem disablePadding>
            <ListItemButton>
              <ListItemText primary="Trash" />
            </ListItemButton>
          </ListItem>
          <ListItem disablePadding>
            <ListItemButton component="a" href="#simple-list">
              <ListItemText primary="Spam" />
            </ListItemButton>
          </ListItem>
          {props.additionalMenuItems.map((item) =>
             item.type === 'listitem' ? 
                 <ListItem disablePadding>{item.children}</ListItem>
             :
                 <Divider />
                 
          )}
        </List>
      </nav>
    </Box>
  );
};

export default BasicList;

Up to you if the damaged API is worth the strong types.

adsy
  • 8,531
  • 2
  • 20
  • 31