Conditionally use material-ui default prop - javascript

I have a StepLabel component in material ui. Depending on the props passed to the parent, I may want to change the icon prop of the StepLabel:
interface Props {
customClasses?: ClassNameMap;
customIcon?: ReactNode;
}
const MyStepComponent = (props: Props) => {
const { customClasses, customIcon } = props
return (
<Stepper {...stepperProps}>
<Step
icon={
customIcon
? React.cloneElement(customIcon as React.ReactElement, {
className: customClasses?.stepIcon
})
: null
}
{...otherStepProps}
/>
</Stepper>
)
}
So in this scenario, if a customIcon is passed to MyStepComponent it will render that icon instead of the default, and if customClasses is passed as well, it will apply some of those custom classes to that icon. Great.
But in the event that customIcon is not passed, I would like for the Step component to just use its default icon (which is the blue circle with the step number in it). However, in my code, it does not render the default icon, it renders null.
How can I tell the Step to use my customIcon if it exists, but if not, use the default?

if the customIcon is truthy then the first if will be triggered and the component with the custom icon will be rendered.
const MyStepComponent = (props: Props) => {
const { customClasses, customIcon } = props
if (customIcon) {
return (
<Stepper {...stepperProps}>
<Step
icon={ React.cloneElement(customIcon as React.ReactElement, {
className: customClasses?.stepIcon
})
}
{...otherStepProps}
/>
</Stepper>
)
}
return (
<Stepper {...stepperProps}>
<Step
icon={defaultIcon}
{...otherStepProps}
/>
</Stepper>
)
}

Related

How to type property based on other prop with Typescript and React?

How to make an intelligent prop type? I have Alert component which has some actions. Those actions can be clickable, there are some different components like Button or Link.
I would like to achieve something like this:
<Alert actions={[{ component: Link, props: { /* here only Link props available */ } }]} />
and
<Alert actions={[{ component: Button, props: { /* here only Button props available */ } }]} />
So props property should determine its type based on component property. Is this possible? I do not want to add any additional generics like
<Alert<ButtonProps> ... />
it should be "intelligent" and do it automatically
You can do this by generics, but it can get a little bit complicated: you need to explicit start which components are to be accepted by <Alert> via its prop type:
interface AlertAction<TFunctionalComponent extends FC<any>> {
component: TFunctionalComponent;
props: ComponentPropsWithoutRef<TFunctionalComponent>;
}
interface Props {
actions: Array<AlertAction<typeof Link | typeof Button>>;
}
export const Alert: FC<Props> = ({ actions }) => {
// Alert component body here
};
However I do see this as an anti-pattern: instead of splitting the component name and props into two separate keys, what if you simply let actions accept an array of ReactElement? i.e.:
interface Props {
actions: ReactElement[];
}
const Alert: FC<Props> = ({ actions }) => {
return <div>
{actions.map(action => <>{action}</>)}
</div>;
};
const MyApp: FC = () => {
return (
<>
{/* Will work */}
<Alert actions={[<Link {...linkProps} />]} />
<Alert actions={[<Button {...buttonProps} />]} />
</>
);
};
If you need to update the props or inject some custom child node into these elements, then you can take advantage of React.cloneChildren:
const Alert: FC<Props> = ({ actions }) => {
return (
<div>
{actions.map((action) => (
<>
{cloneElement(action, {
children: <>Custom child node for action elements</>,
})}
</>
))}
</div>
);
};

Is it possible to dynamically render elements with props using TypeScript?

I've been digging around SO and the web at large for a solution, but I can't seem to nail it.
I have two components, Link and Button. Long story short: they are wrappers for <a> and <button> elements, but with the added options such as chevrons on the right-side, icons on the left-side, full-width mode, etc.
Here is what I have so far (and here's the code running on typescriptlang.org/play):
type Variant = "primary" | "secondary" | "tertiary";
interface CommonProps {
variant?: Variant;
showChevron?: boolean;
icon?: IconDefinition;
fullWidth?: boolean;
small?: boolean;
}
interface LinkOnlyProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
href: string;
}
interface ButtonOnlyProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
onClick: React.MouseEventHandler<HTMLButtonElement>;
}
export type LinkProps = CommonProps & LinkOnlyProps;
export type ButtonProps = CommonProps & ButtonOnlyProps;
export const Link = (props: LinkProps) => {
const {
children,
showChevron,
icon,
fullWidth,
variant,
small,
className,
...rest
} = props;
const { classes } = initButton(props);
return (
<a className={classes} {...rest}>
<Content {...props} />
</a>
);
};
export const Button = (props: ButtonProps) => {
const {
children,
showChevron,
icon,
fullWidth,
variant,
small,
className,
...rest
} = props;
const { classes } = initButton(props);
return (
<button className={classes} {...rest}>
<Content {...props} />
</button>
);
};
I've tried extracting the common logic for the Link and Button components into a single Component, however when I spread the ...rest props I get TypeScript yelling at me. From the error, it seems because I haven't been able to account for the possibility of <a> props being spread on to a <button> element and vice-versa.
I wanted to keep Link and Button as separate components, rather than specifying the type as a prop, so that the intentionality of the developer is clear when the components are being implemented.
Is there any possibility of extracting that common logic into a central component that both Link and Button can simply act as wrappers for? For example:
export const Link = (props: LinkProps) => {
return <Component element="a" {...props} />;
}
export const Button = (props: ButtonProps) => {
return <Component element="button" {...props} />;
}
Was able to work around the type assertion using as any when spreading the rest of my props:
return (
<Element className={classes} {...(rest as any)}>
<Content {...props} />
</Element>
);

How to add class to a passed Component

When trying to pass a component as a prop of another component, everything works fine.
But if i want instead pass a Component and handle its css classes inside the children, I'm currently lost.
In my mind im trying to achieve something similar to this:
import Navbar from 'what/ever/path/Navbar/is/in/Navbar.js';
export default function ParentComponent {
return(
<Navbar NavIcon={<MyIcon/>} />
)
}
.... Imports etc...
export default function Navbar(props) {
const {NavIcon} = props;
return(
<Navigation>
// Now use the Prop as a Component and pass default classNames to it.
// So that we don't need to wrap everything inside a span / div etc.
<NavIcon className="AddCustomStylesAlwaysHere" />
</Navigation>
)
}
Two approaches come to my mind:
Passing a component
Just pass the component and let the parent take care of its instantiation. This way, the only changes you need is making sure <MyIcon /> accepts a className prop:
const MyIcon = ({ className }) => {
return <div className={className} />
};
const Navbar = ({ NavIcon }) => {
return (
<Navigation>
<NavIcon className="AddCustomStylesAlwaysHere" />
</Navigation>
);
};
<Navbar NavIcon={MyIcon} />
Passing an element instance
This way, you take care of instantiating the component and the parent just renders it. In this case, you have to use React utilities to modify existing elements (https://reactjs.org/docs/react-api.html#cloneelement):
const MyIcon = ({ className }) => {
return <div className={className} />
};
const Navbar = ({ NavIcon }) => {
return (
<Navigation>
{React.cloneElement(NavIcon, { className: 'AddCustomStylesAlwaysHere' })}
</Navigation>
);
};
<Navbar NavIcon={<MyIcon />} />
You can use React.Children.map in combination with React.cloneElement:
{
React.Children.map(children, ( child, idx ) => {
return React.cloneElement(child, { className: 'additional-classnames' })
})
}

MaterialUI stepper use alternative color

How can I change the colour that a material ui Stepper uses? By default the material UI stepper's icons use the primary colour for the "active" as well as "completed" steps.
class HorizontalLinearStepper extends React.Component {
state = {
activeStep: 1,
skipped: new Set()
};
render() {
const { classes } = this.props;
const steps = getSteps();
const { activeStep } = this.state;
return (
<div className={classes.root}>
<Stepper activeStep={activeStep}>
{steps.map((label, index) => {
const props = {};
const labelProps = {};
return (
<Step key={label} {...props}>
<StepLabel {...labelProps}>{label}</StepLabel>
</Step>
);
})}
</Stepper>
</div>
);
}
}
Now the stepper uses the theme's main colour as "icon colour". How can I change this to use the secondary colour instead? Adding a color props to any of the Stepper, Step or StepLabel doesn't seem to work, neither does style={{color: 'red', backgroundColor: 'red'}} give the expected results in any of those things.
How can I modify the colour?
A fiddle can be found here.
You can use the StepIconProps prop on StepLabel to customise the classes and change the colour e.g. https://codesandbox.io/s/k1wp19vz6o

Specify specific props and accept general HTML props in Typescript React App

I have a React Wrapper Component, that accepts some props, but forwards all others to the child component (especially relevent for native props like className, id, etc.).
Typescript complains, however, when I pass native props. See error message:
TS2339: Property 'className' does not exist on type
'IntrinsicAttributes & IntrinsicClassAttributes< Wrapper > & Readonly< {
children?: ReactNode; }> & Readonly< WrapperProps>'.
How can I get a component with specific props that also accepts native props (without accepting any props and giving up on type checking)?
My code looks like this:
interface WrapperProps extends JSX.IntrinsicAttributes {
callback?: Function
}
export class Wrapper extends React.Component<WrapperProps>{
render() {
const { callback, children, ...rest } = this.props;
return <div {...rest}>
{children}
</div>;
}
}
export const Test = () => {
return <Wrapper className="test">Hi there</Wrapper>
}
FYI: I found a similar question here, but the answer basically gives up type checking, which I want to avoid: Link to SO-Question
We can have a look at how div props are defined:
interface IntrinsicElements {
div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>;
}
If we use React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement> as the base type we will have all properties of div. Since DetailedHTMLProps just adds ref to React.HTMLAttributes<HTMLDivElement> we can use just this as the base interface to get all div properties:
interface WrapperProps extends React.HTMLAttributes<HTMLDivElement> {
callback?: Function
}
export class Wrapper extends React.Component<WrapperProps>{
render() {
const { callback, children, ...rest } = this.props;
return <div {...rest}>
{children}
</div>;
}
}
export const Test = () => {
return <Wrapper className="test">Hi there</Wrapper> // works now
}
JSX.IntrinsicElements has this info, e.g.
const FooButton: React.FC<JSX.IntrinsicElements['button']> = props => (
<button {...props} className={`foo ${props.className}`} />
)
// alternative...
const FooButton: React.FC<React.PropsWithoutRef<
JSX.IntrinsicElements['button']
>> = props => <button {...props} className={`foo ${props.className}`} />
discovered this in the react-typescript-cheatsheet project.
Have a look at ComponentProps, ComponentPropsWithRef, and ComponentPropsWithoutRef - this will accept a generic input that can be "div", "button", or any other component. It will include react specific props such as className as well:
import React, {
forwardRef,
ComponentPropsWithoutRef,
ComponentProps,
ComponentPropsWithRef
} from "react";
const ExampleDivComponent = forwardRef<
HTMLDivElement,
ComponentPropsWithoutRef<"div">
>(({ children, ...props }, ref) => {
return (
<div {...props} ref={ref}>
{children}
</div>
);
});
<ExampleDivComponent
className=""
style={{ background: "green" }}
tabIndex={0}
onTouchStart={() => alert("touched")}
/>;
const ExampleButtonComponent: React.FC<ComponentProps<"button">> = ({
children,
...props
}) => {
return <button {...props}>{children}</button>;
};
<ExampleButtonComponent onClick={() => alert("clicked")} />;
A co-worker of mine figured it out. Sharing here for broader visibility:
interface ComponentPropTypes = {
elementName?: keyof JSX.IntrinsicElements; // list of all native DOM components
...
}
// Function component
function Component({
elementName: Component = 'div',
...rest,
// React.HTMLAttributes<HTMLOrSVGElement>) provides all possible native DOM attributes
}: ComponentPropTypes & React.HTMLAttributes<HTMLOrSVGElement>)): JSX.Element {
return <Component {...rest} />;
}
// Class component
class Component extends React.Component<ComponentPropTypes & React.HTMLAttributes<HTMLOrSVGElement>> {
render() {
const {
elementName: Component,
...rest,
} = this.props;
return <Component {...rest} />
}
}

Categories