在 material ui 中将它们作为道具传递时如何键入 类

How to type classes when passing them as a prop in material ui

我发现自己在 material ui 中编写了一些通用组件,其中样式实际上是在父组件或祖父组件中定义的。例如:

const GenericDescendant = (props: DescendantProps) => {
  const { classesFromAncestor, ...other } = props
  return <Button className={classesFromAncestor} {...props}>Text here</Button>
}

我从 Ancestor 那里创建了一个 useStylesclasses 对象并将其传递下去:

const useDescendantStyles = makeStyles({
  selector: { 
    border: '1px solid green',
    // more customized styles
  }
})

const Ancestor = () => {
  const descendantClasses = useDescendantStyles()
  return <GenericDescendant classesFromAncestor={descendantClasses} />
}

或者另一种方式,我可以使用 withStyles 包装后代中使用的组件:

const GenericDescendant = (props: DescendantProps) => {
  const { tooltipClassesFromAncestor, buttonClassesFromAncestor, ...other } = props
  const CustmoizedTooltip = withStyles(tooltipClassesFromAncestor)(Tooltip);
  const CustomButton = withStyles(buttonClassesFromAncestor)(Button)
  return (
    <CustmoizedTooltip>
      <CustomButton>Text here</CustomButton>
    </CustmoizedTooltip>
  )
}

在这种情况下,我不会在 Ancestor 中调用 useStyles,我会像这样传递对象:

const Ancestor = () => {
  return <GenericDescendant classesFromAncestor={{
    selector: { 
      border: '1px solid green',
      // more customized styles
    }
  }} />
}

我不想将整个 GenericDescendant 包裹在 withStyles 中,因为这样我就无法确定哪个组件正在采用从祖先传递的自定义样式。

情况略有不同,但我可以选择任何一种方式来允许自定义祖先的通用后代。但是,当将自定义样式作为道具传递时,我不知道如何在 DescendantProps 中正确键入 classesFromAncestor 道具。它实际上可以采用任何形式,并且直到运行时才知道该形式。我不想将其键入 any。定义作为 prop 传递的 类 类型的最佳方法是什么?

如评论中所述,如果您要传递 makeStyles 的结果,则它是 Record<string, string>。但是,如果您要传递 CSS-in-JS 对象,那么您可以这样做:

import { ListItemClassKey } from '@material-ui/core/ListItem';

type RecursiveCSSProperties = {
  [key: string]: React.CSSProperties | RecursiveCSSProperties;
}

type MuiComponentClasses<T extends string> = {
  [key in T]?: React.CSSProperties | RecursiveCSSProperties;
};

const myListItemClasses: MuiComponentClasses<ListItemClassKey> = {
  secondaryAction: {},
  divider: {
    margin: 56,
    top: 0,
    "& .myClass": {
      background: 'red',
      textAlign: 'center'
    }
  }
}

输出结构应说明为什么需要递归 React.CSSProperties 以获得 CSS 的所有可能嵌套,这些嵌套可以使用 CSS-in-JSS 库进行。第二种类型 MuiComponentClasses 是可选的,但允许您从相关组件中提取键列表,这样您就可以只在 class 对象的顶层定位正确的键。

我一直在玩这个,有很多可能的设置。您可以通过传递单个 string className 来设置 material-ui 组件的样式,您还可以传递对应于可用的不同选择器的 classes 对象对于那个组件。大多数(但不是全部)Material 组件的主要选择器是 rootButton 接受 classes rootlabeltextmany others.

当您调用 makeStyles 时,您创建的是一个 object,其中的键是选择器,与您输入的键基本相同,值是 string class 生成的 classes 的名称。您可以将此类型作为 ClassNameMap@material-ui/styles 导入。它本质上是 Record<string, string>,但有一个可选的泛型,可以让您限制键。

export type ClassNameMap<ClassKey extends string = string> = Record<ClassKey, string>;

您传递给 makeStyleswithStyles 的第一个参数是样式属性的键控对象。它可以简化为 Record<string, CSSProperties> 或者您可以从 material-ui 导入复杂类型,其中包括对函数等的支持。类型 Styles<Theme, Props, ClassKey> 采用三个可选泛型,其中 Theme 是你的主题类型,Props 是组件自己的属性,ClassKey 又是支持的选择器。 ThemeProps 主要包含在内,以便您可以将样式映射为 themeprops.

的函数

如果我们想同时为多个组件提供样式,一种简单的方法是为每个组件创建一个具有一个键的样式对象。你可以随便称呼他们。然后我们将这些样式中的每一个作为 className 传递给组件。

const MyStyled = withStyles({
  button: {
    border: "green",
    background: "yellow"
  },
  checkbox: {
    background: "red"
  }
})(({ classes }: { classes: ClassNameMap }) => (
  <Button className={classes.button}>
    <Checkbox className={classes.checkbox} />
    Text Here
  </Button>
));

该设置允许在任何嵌套级别使用任意数量的组件。

我尝试制作一个可以包裹外部组件的高阶组件。您可以以此为起点并根据您的需要进行调整。

const withAncestralStyles = <Props extends { className?: string } = {}>(
  Component: ComponentType<Props>,
  style: Styles<DefaultTheme, Omit<Props, "children">, string>
) => {
  const useStyles = makeStyles(style);

  return (
    props: Omit<Props, "children"> & {
      children?: (styles: ClassNameMap) => ReactNode;
    }
  ) => {
    console.log(props);
    const classes = useStyles(props);
    const { children, className, ...rest } = props;
    return (
      <Component {...rest as Props} className={`${className} ${classes.root}`}>
        {children ? children(classes) : undefined}
      </Component>
    );
  };
};

基本上,class classes.root 将自动应用于父项的 className,同时所有样式都作为道具传递给子项(但未应用)。我在 React.cloneElement 工作时遇到了问题,所以我要求 ui 组件的 children 是接收 classes 对象的 function

const StyledButton = withAncestralStyles(Button, {
  root: {
    border: "green",
    background: "yellow"
  },
  checkbox: {
    background: "red"
  }
});

const MyComponent = () => (
  <StyledButton onClick={() => alert('clicked')}>
    {(classes) => <><Checkbox className={classes.checkbox}/>Some Text</>}
  </StyledButton>
);

Code Sandbox

此方法不适用于您的 Tooltip 示例,因为 Tooltip 是没有 root 选择器的组件之一,需要采用不同的样式来定位弹出框。