ForwardRef type based on props - javascript

I am using React + Typescript. I am working on a component that can return dynamic HTML element depends on props:
interface Props {
label?: string;
}
const DynamicComponent = forwardRef<
HTMLButtonElement | HTMLLabelElement,
Props
>((props, ref) => {
if (props.label) {
return (
<label ref={ref as React.ForwardedRef<HTMLLabelElement>}>
{props.label}
</label>
);
}
return (
<button ref={ref as React.ForwardedRef<HTMLButtonElement>}>BUTTON</button>
);
});
Is it possible to type ref's interface in a way that will depend on the label prop?
export default function App() {
const btnRef = useRef<HTMLButtonElement>(null); // Allowed
const labelRef = useRef<HTMLLabelElement>(null); // Allowed
// const labelRef = useRef<HTMLButtonElement>(null); // Not allowed, because `label` prop was not provided
return (
<>
<DynamicComponent ref={btnRef} />
<DynamicComponent ref={labelRef} label="my label" />
</>
);
}
Sandbox link

In order to do that we need to use function overloading, with higher order function pattern and typeguards:
FIXED
import React, { forwardRef, useRef, Ref } from 'react'
interface Props {
label?: string;
}
// typeguard
const isLabelRef = (props: Props, ref: React.ForwardedRef<any>): ref is React.ForwardedRef<HTMLLabelElement> => {
return true // ! NON IMPLEMENTED
}
// typeguard
const isButtonRef = (props: Props, ref: React.ForwardedRef<any>): ref is React.ForwardedRef<HTMLButtonElement> => {
return true // ! NON IMPLEMENTED
}
// Higher order COmponent with overloads
function DynamicComponent<T extends HTMLButtonElement>(reference: Ref<T>): any
function DynamicComponent<T extends HTMLLabelElement>(reference: Ref<T>, props: Props): any
function DynamicComponent<T extends HTMLElement>(reference: Ref<T> | undefined, props?: Props) {
const WithRef = forwardRef<HTMLElement, Props>((_, ref) => {
if (props && isLabelRef(props, ref)) {
return (
<label ref={ref}>
{props.label}
</label>
);
}
if (props && isButtonRef(props, ref)) {
return (
<button ref={ref}>BUTTON</button>
);
}
return null
});
return <WithRef ref={reference} />
}
export default function App() {
const btnRef = useRef<HTMLButtonElement>(null); // Allowed
const labelRef = useRef<HTMLLabelElement>(null); // Allowed
return (
<>
{DynamicComponent(btnRef)}
{DynamicComponent(labelRef, { label: 'sdf' })}
</>
);
}
Playground
As you might have noticed, I use only props from DynamicComponent.
Also T generic parameter serves for narrowing the ref type
I left isButtonRef and isLabelRef unimplemented
UPDATE
Seems that my previous example is useless. Sorry for that.
My bad. I have already fixed it.
As an alternative solution, you can override built in forwardRef function.
interface Props {
label: string;
}
declare module "react" {
function forwardRef<T extends HTMLButtonElement, P>(
render: ForwardRefRenderFunction<HTMLButtonElement, never>
): ForwardRefExoticComponent<
PropsWithRef<{ some: number }> & RefAttributes<HTMLButtonElement>
>;
function forwardRef<T extends HTMLLabelElement, P extends { label: string }>(
render: ForwardRefRenderFunction<HTMLLabelElement, { label: string }>
): ForwardRefExoticComponent<
PropsWithRef<{ label: string }> & RefAttributes<HTMLLabelElement>
>;
}
const WithLabelRef = forwardRef<HTMLLabelElement, Props>((props, ref) => (
<label ref={ref}>{props.label}</label>
));
const WithButtonRef = forwardRef<HTMLButtonElement>((props, ref) => (
<button ref={ref}>{props}</button>
));
function App() {
const btnRef = useRef<HTMLButtonElement>(null);
const labelRef = useRef<HTMLLabelElement>(null);
const divRef = useRef<HTMLDivElement>(null);
return (
<>
<WithButtonRef ref={btnRef} />
<WithLabelRef ref={labelRef} label="my label" />
<WithLabelRef ref={divRef} label="my label" /> //expected error
</>
);
}

Related

Creating TypeScript React component dynamically

How can I create component dynamically using React with TypeScript?
Assuming that I have some RandomComponent and I pass it as props to renderInput, how could I return <RandomComponent> from it?
TypeScript does not seem to understand something like this:
export const renderInput = (props) => {
const Field: JSX.Element = props.component;
return (
<Field {...props} />
)
}
Edit:
Made it work, but am wondering what if I wanted to add this ComponentType type. It works without it, but when I add it and the code looks like this:
const Field: ComponentType = component;
return (
<Field name={name} label={label} value={value} onChange={onChange} {...props} />
);
};
I get:
TS2322: Type '{ name: any; label: any; value: any; onChange: any; }' is not assignable to type 'IntrinsicAttributes'.   Property 'name' does not exist on type 'IntrinsicAttributes'
You can use generic type
export const renderInput = <T,>(props:T & {component: React.reactNode }) => {
const Field = props.component;
return (
<Field {...props}/>
)
}
Alternatively if only need to send a component you can send it as children
export const renderInput = (props: { children: React.reactNode }) => {
const { children } = props;
return children;
};

What is wrong with MUI TextField, when wrapped in a custom component?

I'm developing a complex solution for building forms from a JSON contract using Formik.
By design, inputs can have tooltips. Or they could not.
Trying to make some parts of the code reusable to avoid repeating myself. Created TextFieldSSWrapper for inputs with server-side validations, forwarded a ref as docs tells. Now, this component could be wrapped with Tooltip, if there is one.
The problem is the following: now, when using this middleware component, input loses focus after each onChange call. Also I'm getting a warning on each onChange call:
The anchorEl prop provided to the component is invalid.
If drop wrapper component and do simple copy-paste, everything works fine.
What am I doing wrong? Thanks a lot.
import {useField, Field} from 'formik';
import TextField from '#mui/material/TextField';
import {asyncValidationCheckers} from '../services/validation.service';
import {Tooltip} from '#mui/material';
interface ITextItem {
id: string;
type?: string;
name: string;
value: string | number;
label: string;
placeholder: string;
tooltip?: string;
variant: 'standard' | 'outlined' | 'filled';
size: 'small' | 'medium',
serverValidation: string;
}
interface IConfigFormControl {
error?: boolean;
errorText?: string;
}
interface ITextFieldSSWrapper {
form: any;
ref: any;
}
export const TextItem = (
{
id,
type,
name,
value,
label,
placeholder,
tooltip,
variant,
size,
serverValidation
}: ITextItem
) => {
const [field, meta] = useField(name);
const configFormControl: IConfigFormControl = {
error: false,
errorText: ''
};
if (meta && meta.touched && meta.error) {
configFormControl.error = true;
configFormControl.errorText = meta.error;
}
const helperText = configFormControl.errorText;
const props = {
fullWidth: true,
id,
type,
label,
placeholder,
helperText,
variant,
size,
error: configFormControl.error,
...field
};
const TextFieldSSWrapper = React.forwardRef<HTMLDivElement, ITextFieldSSWrapper>(({ form, ...rest }, ref) => (
<TextField
{...props}
{...rest}
ref={ref}
onChange={(e) => {
const value = e.target.value;
asyncValidationCheckers[serverValidation].setInputValid();
// it the line above we remove server-side validation error
// in order to be able to run server-side validation again on blur
// because it won't run if input has an error from previous sever-side validation cycle
form.setFieldTouched(id, true);
form.setFieldValue(id, value);
}}
onBlur={(e) => {
const value = e.target.value;
if (!form.errors[id]) {
setTimeout(() => {
asyncValidationCheckers[serverValidation].validateInput(value, () => {
form.setFieldTouched(id, true);
});
}, 0);
}
}}
/>
));
const ref = React.createRef<HTMLDivElement>();
return (
serverValidation
? (
<Field
name={id}
>
{
({form} : any) => (
tooltip
? (
<Tooltip title={tooltip} placement='bottom'>
<TextFieldSSWrapper ref={ref} form={form} />
</Tooltip>
)
: (
<TextFieldSSWrapper ref={ref} form={form} />
)
)
}
</Field>
)
: (
tooltip
? (
<Tooltip title={tooltip} placement='bottom'>
<TextField
{...props}
/>
</Tooltip>
)
: (
<TextField
{...props}
/>
)
)
);
};

React: Render and link toggle button outside the class

I have the following example where the toggleComponent.js is working perfectly.
The problem here is that I don't want to render the <ContentComponent/> inside the toggle, rather I want the opposite, I want to toggle the <ContentComponent/> that will be called in another component depending on the state of the toggle.
So the <ContentComponent/> is outside the toggleComponent.js, but they are linked together. So I can display it externally using the toggle.
An image to give you an idea:
Link to funtional code:
https://stackblitz.com/edit/react-fwn3rn?file=src/App.js
import React, { Component } from "react";
import ToggleComponent from "./toggleComponent";
import ContentComponent from "./content";
export default class App extends React.Component {
render() {
return (
<div>
<ToggleComponent
render={({ isShowBody, checkbox }) => (
<div>
{isShowBody && <h1>test</h1>}
<button onClick={checkbox}>Show</button>
</div>
)}
/>
<ToggleComponent
render={({ isShowBody, checkbox }) => (
<div>
{isShowBody && (
<h1>
<ContentComponent />
</h1>
)}
<button onClick={checkbox}>Show</button>
</div>
)}
/>
</div>
);
}
}
Bit tweaked your source.
Modified ToggleComponent
import React from "react";
export default class ToggleComponent extends React.Component {
constructor() {
super();
this.state = {
checked: false
};
this.handleClick = this.handleClick.bind(this);
}
handleClick = () => {
this.setState({ checked: !this.state.checked });
this.props.toggled(!this.state.checked);
};
checkbox = () => {
return (
<div>
<label>Toggle</label>
<span className="switch switch-sm">
<label>
<input type="checkbox" name="select" onClick={this.handleClick} />
<span />
</label>
</span>
</div>
);
};
render() {
return this.checkbox();
}
}
Added OtherComponent with ContentComponent inside.
import React, { Component } from "react";
import ContentComponent from "./content";
export default class OtherComponent extends React.Component {
render() {
return <div>{this.props.show ? <ContentComponent /> : null}</div>;
}
}
Separated as per your requirement.
Modified App
import React, { Component, PropTypes } from "react";
import ToggleComponent from "./toggleComponent";
import OtherComponent from "./otherComponent";
export default class App extends React.Component {
constructor() {
super();
this.toggled = this.toggled.bind(this);
this.state = { show: false };
}
toggled(value) {
this.setState({ show: value });
}
render() {
return (
<div>
<ToggleComponent toggled={this.toggled} />
<OtherComponent show={this.state.show} />
</div>
);
}
}
Working demo at StackBlitz.
If you want to share states across components a good way to do that is to use callbacks and states. I will use below some functional components but the same principle can be applied with class based components and their setState function.
You can see this example running here, I've tried to reproduce a bit what you showed in your question.
import React, { useState, useEffect, useCallback } from "react";
import "./style.css";
const ToggleComponent = props => {
const { label: labelText, checked, onClick } = props;
return (
<label>
<input type="checkbox" checked={checked} onClick={onClick} />
{labelText}
</label>
);
};
const ContentComponent = props => {
const { label, children, render: renderFromProps, onChange } = props;
const [checked, setChecked] = useState(false);
const defaultRender = () => null;
const render = renderFromProps || children || defaultRender;
return (
<div>
<ToggleComponent
label={label}
checked={checked}
onClick={() => {
setChecked(previousChecked => !previousChecked);
}}
/>
{render(checked)}
</div>
);
};
const Holder = () => {
return (
<div>
<ContentComponent label="First">
{checked => (
<h1>First content ({checked ? "checked" : "unchecked"})</h1>
)}
</ContentComponent>
<ContentComponent
label="Second"
render={checked => (checked ? <h1>Second content</h1> : null)}
/>
</div>
);
};
PS: A good rule of thumb concerning state management is to try to avoid bi-directional state handling. For instance here in my example I don't use an internal state in ToggleComponent because it would require to update it if given checked property has changed. If you want to have this kind of shared state changes then you need to use useEffect on functional component.
const ContentComponent = props => {
const { checked: checkedFromProps, label, children, render: renderFromProps, onChange } = props;
const [checked, setChecked] = useState(checkedFromProps || false);
const defaultRender = () => null;
const render = renderFromProps || children || defaultRender;
// onChange callback
useEffect(() => {
if (onChange) {
onChange(checked);
}
}, [ checked, onChange ]);
// update from props
useEffect(() => {
setChecked(checkedFromProps);
}, [ checkedFromProps, setChecked ]);
return (
<div>
<ToggleComponent
label={label}
checked={checked}
onClick={() => {
setChecked(previousChecked => !previousChecked);
}}
/>
{render(checked)}
</div>
);
};
const Other = () => {
const [ checked, setChecked ] = useState(true);
return (
<div>
{ checked ? "Checked" : "Unchecked" }
<ContentComponent checked={checked} onChange={setChecked} />
</div>
);
};

TypeError: name.toLowerCase is not a function - React

I'm creating a list of icons like this website. I'm trying to create a dynamic search bar that searches icons from the list of data. The list of icons is displaying; however, when I'm tapping on the search bar, I have the following error: TypeError: name.toLowerCase is not a function.
Thanks for your help!
//Parent
import iconData from "./icon_data.json";
import ResultContainer from "./ResultContainer";
const fetchIconData = () => Promise.resolve(() => iconData);
export type ModalProps = {
setModalVisible: (value: any) => void;
setProp: (value: any) => void;
modalVisible: boolean | undefined;
};
export const ModalIcons = ({setModalVisible, modalVisible, setProp,}: Partial<ModalProps>) => {
const [data, setData] = useState([]);
useEffect(() => {
fetchIconData().then((jsonData) => setData(jsonData));
}, []);
const [searchTerm, setSearchTerm] = React.useState("");
const handleChange = event => {
setSearchTerm(event.target.value);
};
const results = !searchTerm
? data
: data.filter(name =>
name.toLowerCase().includes(searchTerm.toLocaleLowerCase())
);
return (
<Modal
isOpen={modalVisible?modalVisible:false}
onRequestClose={() => {
setModalVisible && setModalVisible(false)
}}
>
<View >
<input
type="text"
placeholder="Search"
value={searchTerm}
onChange={handleChange}
/>
<ResultContainer data={results} setProp={setProp} />
</View>
</Modal>
);
};
//Child
import ResultRow from "./ResultRow";
type ResultContainerProps = {
data: [],
setProp: (value: any) => void;
}
const ResultContainer = ({
data,
setProp,
}:Partial <ResultContainerProps>) => {
return (
<>
{data.map(({ family, names }) => {
return <ResultRow title={family} icons={names} family={family} setProp={setProp} />;
})}
</>
);
};
export default ResultContainer;
//icon_data.json
[
{
"family": "AntDesign",
"names": ["stepforward", "stepbackward", "forward", "banckward"]
},
{
"family": "Entypo",
"names": ["500px", "500px-with-circle", "add-to-list", "add-user"]
}
]
When you're using state or props dynamically, it might be possible that the value is not loaded. So use this way:
name && name.toLowerCase && name.toLowerCase().includes(searchTerm.toLocaleLowerCase())
I feel the main issue looks like it's a data issue? What if you get undefined?

How to render html string

I have a server response which contains an html string which is passed to a react component as a prop. Within the html there are codes which need to be replaced with React components.
I've used react-string-replace to get this to work but it doesn't seem to work with HTML as the tags are escaped by React. Does anyone know how this might be fixed?
import React from 'react';
import replace from 'react-string-replace';
const reg = /\{(name)\}/g;
const Name = props => {
return (
<div>Hello {props.name}</div>
)
}
class Embed extends React.Component {
render() {
const person = this.props.person
const component = <Name name={person.name} key={person.id} />;
const output = replace(
this.props.content,
reg,
prop => component
);
return (
<div>{output}</div>
)
}
}
const Greeting = props => {
return <Embed content="<div><h1>Greetings</h1> <strong>{name}</strong></div>" person={{ name: 'John', id: 123 }} />;
};
export default Greeting;
Try this -
class Embed extends React.Component {
render() {
const person = this.props.person
const component = <Name name={person.name} key={person.id} />;
const output = replace(
this.props.content,
reg,
prop => component
);
return (
<div dangerouslySetInnerHTML={{ __html: output }}></div>
)
}
}
I suggest much better way to do it like below -
const reg = /\{(name)\}/g;
const Name = props => {
return (
<div>Hello {props.name}</div>
)
}
class Embed extends React.Component {
render() {
const person = this.props.person;
const component = <Name name={person.name} key={person.id} />;
return (
<div><h1>Greetings</h1> <strong>{component}</strong></div>
);
}
}
const Greeting = props => {
return (<Embed person={{ name: 'John', id: 123 }} >
</Embed>);
};
ReactDOM.render(
<Greeting/>,
document.getElementById('test')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="test">
</div>
Wants to remind you this:
From Facebook React Doc:
dangerouslySetInnerHTML is React's replacement for using innerHTML in
the browser DOM. In general, setting HTML from code is risky because
it's easy to inadvertently expose your users to a cross-site scripting
(XSS) attack. So, you can set HTML directly from React, but you have
to type out dangerouslySetInnerHTML and pass an object with a __html
key, to remind yourself that it's dangerous.
Try this:
const Name = props => {
return (
<div>
Hello {props.name}
</div>
)
}
class Embed extends React.Component {
render() {
let content = this.props.content.replace('{name}', this.props.person.name);
return (
<div>
<Name person={this.props.person}/>
<div dangerouslySetInnerHTML={{ __html: content}}></div>
</div>
)
}
}
const Greeting = props => {
return <Embed content="<div><h1>Greetings</h1> <strong>{name}</strong></div>" person={{ name: 'John', id: 123 }} />;
};
ReactDOM.render(<Greeting />, document.getElementById('container'));
Check working example on jsfiddle: https://jsfiddle.net/h81q9nqd/

Categories

Resources