Force react component to only accept a child with a specific type - reactjs

I have this following component:
interface InputWithButtonProps {
label: string;
children: React.ReactElement<typeof CustomButton>;
}
const InputWithButton = ({
label,
fieldName,
disabled,
children,
}: InputWithButtonProps): JSX.Element => {
return (
<>
<TextField label={label} />
{Children.only(children)}
</>
);
};
export default InputWithButton;
And the code for CustomButton is:
export declare const CustomButton: import("#material-ui/core").ExtendButtonBase<import("#material-ui/core").ButtonTypeMap<{}, "button">>;
What I want from InputWithButton is to only accept one single child of type CustomButton, but specifying the type using React.ReactElement<typeof CustomButton> doesn't do any checks, so the following code will work just fine (Which should throw an error in this case):
<InputWithButton label="some label">
<div>test</div>
</InputWithButton>
The InputWithButton should only accept this:
<InputWithButton label="some label">
<Button>Some button</Button>
</InputWithButton>
I also tried to specify the children type as following:
children: React.ReactElement<ButtonProps>;
Which didn't work either.
How can I solve this?

You need to apply the condition that checks if the children's type is Button or not, consider the code below that will solve your problem if I understand your problem correctly,
interface InputWithButtonProps {
label: string;
children: React.ReactElement<typeof CustomButton>;
}
const InputWithButton = ({
label,
fieldName,
disabled,
children,
}: InputWithButtonProps): JSX.Element => {
return typeof children.type == 'function' &&
children.type.name == "Button" &&
(
<>
<input label={label} />
{children}
</>
);
};
export default InputWithButton;
The component will return nothing if the tag is other than 'Button', you can remove the 'typeof children.type' condition because it only checks if the children is custom made component or an HTML tag.

Related

Render an element based on a given prop taking on appropriate element attributes

I would like to create a component that renders a specific HTML element based on properties given to it. In this case, I'd like to render a div if the component's isDiv property is true, and a button if it's false.
I also want to be able to provide my component with any of the element's attributes, which will be passed on down to the element itself.
Without TypeScript, I might write the component like so:
const Button = ({ isDiv, elementProps, children }) => {
return isDiv ? (
<div {...elementProps} className="button">{children}</div>
) : (
<button {...elementProps} className="button">{children}</button>
);
};
To be used, for example, like:
<Button type="submit" />
{/* <button class="button" type="button">...</button> */}
<Button isDiv />
{/* <div class="button">...</div> */}
My attempt now, using TypeScript (and a technique I've read refered to as a "Discriminated Union") is as follows:
type DivProps = {
isDiv: true;
elementProps: React.HTMLAttributes<HTMLDivElement>
};
type ButtonProps = {
isDiv: false;
elementProps: React.ButtonHTMLAttributes<HTMLButtonElement>;
};
const Button: React.FC<DivProps | ButtonProps> = ({
isDiv,
elementProps,
children,
}) => {
return isDiv ? (
<div {...elementProps} className="button">{children}</div>
) : (
<button {...elementProps} className="button">{children}</button>
);
};
Where I get errors due to HTMLButtonElement and HTMLDivElement not being compatible, ultimately:
Property 'align' is missing in type 'HTMLButtonElement' but required in type 'HTMLDivElement'
How can I correctly implement this component using TypeScript?
you need to help TS know about the relationship between isDiv and elementProps so it could narrow down the discriminated union.
this works:
const Button: React.FC<DivProps | ButtonProps> = ({
children,
...props,
}) => {
return props.isDiv ? (
<div {...props.elementProps} className="button">{children}</div>
) : (
<button {...props.elementProps} className="button">{children}</button>
);
};

React with TypeScript: how to create ref prop

I am using Ionic with React (typescript) and I am creating my custom form builder. There I created my form that has to have a ref property, because I need a reference of it when I use it.
My problem is that I don't know how to define a prop that is reference type for my custom form builder.
This is how I am using it:
const form = useRef<HTMLFormElement>(null);
return (
<IonPage>
<Header />
<IonContent fullscreen>
<Form
ref={form}
submit={() => onSubmit()}
fields={ fields }
render={() => (
<React.Fragment>
<IonCard>
<IonCardHeader>
<IonLabel>Valamilyen form</IonLabel>
</IonCardHeader>
<IonCardContent>
<Field {...fields.name} />
</IonCardContent>
</IonCard>
</React.Fragment>
)}/>
</IonContent>
<Footer />
</IonPage>
);
Here I got an error:
Property 'ref' does not exist on type 'IntrinsicAttributes & IFormProps & IFormState & { children?: ReactNode; }'
My Form React.FC looks like this:
type formProps = IFormProps & IFormState;
export const Form: React.FC<formProps> = React.forwardRef<HTMLFormElement, formProps>( (props: formProps, porpsRef) => {
return (
<form onSubmit={handleSubmit} noValidate={true} ref={porpsRef} />
);
)};
I need to add to my Form component a property named ref of the reference, but I don't know how.
Thanks
I think there is small mistake. Can you please try this
type formProps = IFormProps & IFormState;
export const Form: React.FC<formProps> =
React.forwardRef<formProps, HTMLFormElement>( (props:
formProps, porpsRef) =>
{
return (
<form onSubmit={handleSubmit} noValidate={true} ref=
{porpsRef} />
);
)};
I just typed a very long answer to a similar question. In your case you seem to be making only one of those mistakes, which is declaring your Form as React.FC. The FC interface isn't aware of the ref forwarding that you've added with React.forwardRef so that's why you get an error when trying to pass a prop ref to it.
Delete this bit : React.FC<formProps> and you should be fine. You've already typed the generics on the React.forwardRef function, so typescript will know the return type and apply it to Form.
export const Form = React.forwardRef<HTMLFormElement, formProps>(...
If you wanted to explicitly declare the type, you can do that but it's a mouthful.
export const Form: React.ForwardRefExoticComponent<formProps & React.RefAttributes<HTMLFormElement>> = ...
(also you've forgotten to destructure handleSubmit from formProps)

Dynamically add a prop to Reactjs material ui Select

I have a question about the material UI Select component and how to set props dynamically.
I'm trying to wrap the material UI Select (https://material-ui.com/components/selects/) component in my CompanySelect so I can add some additional styling and other stuff.
Main question
How can I dynamically add/remove the disableUnderline prop on the material UI Select component.
When I set disableUnderline = null and variant = 'outlined' I get a warning that disableUnderline is an unknown prop. when using variant = 'standard' there is no warning.
CompanySelect component code
import React from 'react';
import Select from '#material-ui/core/Select';
import PropTypes from 'prop-types';
import ExpandMoreRoundedIcon from '#material-ui/icons/ExpandMoreRounded';
import './style.scss';
const CompanySelect= (props) => {
const {
variant,
disableUnderline,
children,
...
} = props;
return (
<Select
disableUnderline={disableUnderline}
variant={variant}
...
>
{children}
</Select>
);
};
CompanySelect.propTypes = {
variant: PropTypes.oneOf(['outlined', 'filled', 'standard']),
disableUnderline: PropTypes.bool,
children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]).isRequired
};
CompanySelect.defaultProps = {
variant: 'standard',
disableUnderline: null,
};
export default CompanySelect;
Standard usage
<AtriasSelect variant="standard" disableUnderline>
<MenuItem />
<MenuItem />
</AtriasSelect>
Outlined usage
<AtriasSelect variant="outlined">
<MenuItem />
<MenuItem />
</AtriasSelect>
The standard usage works. With the disableUnderline the default underline is removed as documented on the Input API page. (https://material-ui.com/api/input/).
Problem occurs when I use the outlined variant because then the Select inherits the OutlinedInput API. If you look at the OutlinedInput API (https://material-ui.com/api/outlined-input/) then you can see it does not have the disableUnderline prop.
I gave the disableUnderline prop the default value 'null' assuming it would not render when not supplied. But when using the Outlined variant (without disableUnderline prop) I get the following warning.
React does not recognize the `disableUnderline` prop on a DOM element. If you intentionally want it to appear in the DOM as a custom attribute, spell it as lowercase `disableunderline` instead. If you accidentally passed it from a parent component, remove it from the DOM element.
So my question, is there a way to not add the prop at all. Something like the following pseudo code:
return (
<Select
{variant !== 'outlined' ? disableUnderline : null} //Pseudo code, just to show what I need
variant={variant}
...
>
{children}
</Select>
);
Possible solution
The only solution I see now (my react knowledge is limited) is adding an if statement in the CompanySelect component that will check if the outlined variant is used or not. But this means I need to have a lot of duplicate code in the CompanySelect code.
const CompanySelect= (props) => {
const {
variant,
disableUnderline,
children,
...
} = props;
if (variant !== 'outlined'){
return (<Select disableUnderline={disableUnderline} variant={variant} ...> {children} </Select>);
} else {
return (<Select variant={variant} ...> {children} </Select>);
}
};
Is there maybe another way of solving this problem?
You can use spread operator (...) in returned JSX like this:
const CompanySelect= (props) => {
const {
variant,
disableUnderline,
children,
...
} = props;
return (
<Select
variant={variant}
{...(variant !== "outlined" && { disableUnderline: true })}
>
{children}
</Select>
);
};
I think the proper way is to use React.cloneElement
Something like
let props = {
variant: variant,
};
// Your dynamic props
if(variant !== 'outlined') {
props[disableUnderline] = 'your value';
}
<div>
{
React.cloneElement(
Select,
props
)
}
</div>

Is an index signature the correct solution to my React props TypeScript interface problem?

This is my component.
interface FormGroupProps {
label: string;
[otherProps: string]: any;
}
const FormGroup = ({ label, ...otherProps }: FormGroupProps) => (
<div>
<label>
{label}
<input {...otherProps} />
</label>
</div>
);
As you can see, I am using an index signature in my props interface to allow the instances of my component to pass any attributes to my input element.
Is this the correct way to solve this problem or a hack?
It is a solution. The problem with it is that now you basically loose all type checking on the props of your component, since any key is allowed with any type.
A better solution would be to extract the props of input using React.ComponentProps. You can then use an intersection type to add your extra property. Like this you will get your new field and get all the props of from input and all will be type checked:
type FormGroupProps = {
label: string;
} & React.ComponentProps<'input'>
const FormGroup = ({ label, ...otherProps }: FormGroupProps) => (
<div>
<label>
{label}
<input {...otherProps} />
</label>
</div>
);
Playground Link

Using a forwardRef component with children in TypeScript

Using #types/react 16.8.2 and TypeScript 3.3.1.
I lifted this forward refs example straight from the React documentation and added a couple type parameters:
const FancyButton = React.forwardRef<HTMLButtonElement>((props, ref) => (
<button ref={ref} className="FancyButton">
{props.children}
</button>
));
// You can now get a ref directly to the DOM button:
const ref = React.createRef<HTMLButtonElement>();
<FancyButton ref={ref}>Click me!</FancyButton>;
I get the following error in the last line under FancyButton:
Type '{ children: string; ref: RefObject<HTMLButtonElement>; }' is not
assignable to type 'IntrinsicAttributes & RefAttributes<HTMLButtonElement>'. Property 'children' does not
exist on type 'IntrinsicAttributes & RefAttributes<HTMLButtonElement>'.ts(2322)
It would seem that the type definition for React.forwardRef's return value is wrong, not merging in the children prop properly. If I make <FancyButton> self-closing, the error goes away. The lack of search results for this error leads me to believe I'm missing something obvious.
trevorsg, you need to pass the button properties:
import * as React from 'react'
type ButtonProps = React.HTMLProps<HTMLButtonElement>
const FancyButton = React.forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => (
<button type="button" ref={ref} className="FancyButton">
{props.children}
</button>
))
// You can now get a ref directly to the DOM button:
const ref = React.createRef<HTMLButtonElement>()
<FancyButton ref={ref}>Click me!</FancyButton>
ADDED:
In recent versions of TS and #types/react, you can also use React.ComponentPropsWithoutRef<'button'> instead of React.HTMLProps<HTMLButtonElement>
The answers given by aMarCruz and euvs both work, but they lie to consumers a little bit. They say they accept all HTMLButtonElement props, but they ignore them instead of forwarding them to the button. If you're just trying to merge in the children prop correctly, then you might want to use React.PropsWithChildren instead:
import React from 'react';
interface FancyButtonProps {
fooBar?: string; // my custom prop
}
const FancyButton = React.forwardRef<HTMLButtonElement, React.PropsWithChildren<FancyButtonProps>>((props, ref) => (
<button type="button" ref={ref} className="fancy-button">
{props.children}
{props.fooBar}
</button>
));
FancyButton.displayName = 'FancyButton';
Or explicitly add a children prop:
interface FancyButtonProps {
children?: React.ReactNode;
fooBar?: string; // my custom prop
}
const FancyButton = React.forwardRef<HTMLButtonElement, FancyButtonProps>((props, ref) => (
<button type="button" ref={ref} className="fancy-button">
{props.children}
{props.fooBar}
</button>
));
FancyButton.displayName = 'FancyButton';
Or if you actually want to accept all the button props and forward them (let consumers choose button type="submit", for example), then you might want to use rest/spread:
import React from 'react';
interface FancyButtonProps extends React.ComponentPropsWithoutRef<'button'> {
fooBar?: string; // my custom prop
}
const FancyButton = React.forwardRef<HTMLButtonElement, FancyButtonProps>(
({ children, className = '', fooBar, ...buttonProps }, ref) => (
<button {...buttonProps} className={`fancy-button ${className}`} ref={ref}>
{children}
{fooBar}
</button>
),
);
FancyButton.displayName = 'FancyButton';
The answer given by aMarCruz works well. However, if you also need to pass custom props to the FancyButton, here is how it can be done.
interface FancyButtonProps extends React.ComponentPropsWithoutRef<'button'> {
fooBar?: string; // my custom prop
}
const FancyButton = React.forwardRef<HTMLButtonElement, FancyButtonProps>((props, ref) => (
<button type="button" ref={ref} className="FancyButton">
{props.children}
{props.fooBar}
</button>
));
/// Use later
// You can now get a ref directly to the DOM button:
const ref = React.createRef<HTMLButtonElement>()
<FancyButton ref={ref} fooBar="someValue">Click me!</FancyButton>
Just adding here for completion.
You can use ForwardRefRenderFunction<YourRefType, YourProps> on your component.
Like:
const Component: ForwardRefRenderFunction<YourRef, YourProps> = (yourProps, yourRef) => return <></>
export default fowardRef(Component)

Resources