Custom HTML Button wrapped in Link not working - reactjs

I have made a custom Button component that returns HTML button. Now I am using this Button inside Link from next-routes. The problem is that it does not work this way. Weird thing is that button works correctly if I use HTML button inside Link. However, both button are being rendered in the DOM in exact same way. Following is the code:
// Button.js
import React from "react";
import classNames from "classnames";
const Button = ({
children,
newClass = "",
onClickHandler = () => { },
isSubmitting = false,
inlineBtn = true,
disabled,
primary,
secondary,
basic,
notCentered = true,
shaded,
miniLoader,
type = "button",
isTransparent,
fontClass = "",
small
}) => {
return (
<React.Fragment>
<button
className={classNames(
`${isTransparent ? 'btn-transparent' : 'btn'} ${fontClass} ${newClass}`,
{
"btn-block": !inlineBtn,
"col-mx-auto": !notCentered,
"btn-primary": primary,
"btn-secondary": secondary,
"btn-basic": basic,
shaded: shaded,
loading: isSubmitting,
"loading-sm": miniLoader,
"btn-sm": small
}
)}
disabled={disabled}
type={type}
onClick={onClickHandler}
>
<span>{children}</span>
</button>
</React.Fragment>
);
};
export { Button };
The following does not work:
<Link route="/register/location">
<Button basic small>
Sign Up
</Button>
</Link>
The following works fine:
<Link route="/register/location">
<button className="btn btn-basic btn-sm" type="button" onClick={() => { }}>
<span>Sign Up</span>
</button>
</Link>

You can update your Button component as the following.
const Button = ({
as = "button",
children,
newClass = "",
onClickHandler = () => {},
isSubmitting = false,
inlineBtn = true,
disabled,
primary,
secondary,
basic,
notCentered = true,
shaded,
miniLoader,
type = "button",
isTransparent,
fontClass = "",
small,
...rest
}) => {
const Wrapper = as;
return (
<Wrapper
className={classNames(
`${isTransparent ? "btn-transparent" : "btn"} ${fontClass} ${newClass}`,
{
"btn-block": !inlineBtn,
"col-mx-auto": !notCentered,
"btn-primary": primary,
"btn-secondary": secondary,
"btn-basic": basic,
shaded: shaded,
loading: isSubmitting,
"loading-sm": miniLoader,
"btn-sm": small
}
)}
disabled={disabled}
type={type}
onClick={onClickHandler}
{...rest}
>
<span>{children}</span>
</Wrapper>
);
};
This will allow custom Wrapper to be used for your Button component. And we pass every inherited props into your Wrapper so that your route props will be received in your Link component.
You can then use it like so
<Button basic small as={Link} route="/register/location">
Sign Up
</Button>
This uses the ES6 spread operator syntax. Basically you render your Button component as a Link component, and any inherited props will be passed to the Link component, hence route props is passed into Link component.
This follow the API Design Approach similar to Material-ui's spead approach. This will allow your component to be more flexible as well.

Related

I want to have a dialog as a separate component in React-MUI

I'm using React and MUI (v5)
I want to show a dialog in my component but I want this dialog as a separate component, like:
function MyComponent() {
const [ openDialog, setOpenDialog ] = useState(false);
const handleOpenDialog = () => {
setOpenDialog(true);
};
return (
<React.Fragment>
<Button variant="contained" size="medium" onClick={handleOpenDialog}>
Open Dialog
</Button>
<CreateCategory openDialog={openDialog} />
<Box>
...
</Box>
</React.Fragment>
);
}
and the dialog would be like:
export default function CreateCategory(props) {
const [openDialog, setOpenDialog] = useState(props.openDialog);
const [newCategoryName, setNewCategoryName] = useState("");
const handleDialogClose = () => {
setOpenDialog(false);
};
const handleAddCategory = (categoryName) => {
...
};
const handleCategoryNameChange = (e) => {
setNewCategoryName(e.target.value);
};
return (
<React.Fragment>
<Dialog
fullWidth
maxWidth={"sm"}
open={openDialog}
onClose={handleDialogClose}
>
<DialogTitle>Create Video Category</DialogTitle>
<DialogContent>
<TextField
...
/>
</DialogContent>
<DialogActions>
<Button ...>
Add Category
</Button>
<Button variant="outlined" onClick={handleDialogClose}>
Close
</Button>
</DialogActions>
</Dialog>
</React.Fragment>
);
}
But it is not working, I want to reuse the dialog in another component
I have it in code sandbox https://codesandbox.io/s/quizzical-merkle-ivquu?file=/src/App.js
Rafael
Copying props into state for no reason is an anti-pattern, and it's causing your issue.
You have two states called openDialog (one in the parent and one in the child), and are expecting them to operate as if they're the same.
To correct this, use props.openDialog directly in your component, and pass down the setter function as a prop as well. Then remove the local state version since it won't be used anymore.
// Pass the setter as well
<CreateCategory openDialog={openDialog} setOpenDialog={setOpenDialog} />
const handleDialogClose = () => {
props.setOpenDialog(false); // Use the prop.
};
<Dialog
fullWidth
maxWidth={"sm"}
open={props.openDialog} // Use value directly here
onClose={handleDialogClose}
>
In case this is also a source of confusion in the following snippet (which you should remove anyway):
const [openDialog, setOpenDialog] = useState(props.openDialog);
props.openDialog is only used once on the first render to initialize the state. Anytime in the future that the prop changes, the state will not change to match it. It is an initial value.

How can i get multiple recoil atoms when using components multiple?

In some components i am using recoil atoms to manage my states. One example is my modal component. It look something like this:
export const modalState = atom({
key: "modalState",
default: false
})
export const useToggleModalState = () => {
const setModalState = useSetRecoilState(modalState)
return (state, callback) => {
setModalState(state)
if (callback) {
callback()
}
}
}
export const Modal = (props) => {
<Transition show={modalState}>
<Dialog>
<Dialog.Title>My Modal Headline</Dialog.title>
<Dialog.Description>My Modal Description</Dialog.Description>
</Dialog>
</Transition>
}
and i am using this modal like this:
const toggleModalState = useToggleModalState();
return (
<Modal />
<Button text="Close Modal" onClick={() => toggleModalState(false)} />
)
however, if I use the modal multiple times, the modal is automatically duplicated, but I still use only one state for all modals. Of course, I don't want that. If I use a component multiple times, I want the state to be created multiple times, so that I can change the state of each component individually.
I have read that there are also atomFamilys. Could I use these at this point? What should my code look like then? Can multiple atoms also be created automatically if I use a component multiple times?
Why do you want to use recoil for that? The state of the modal is tied to the modal itself, it doesn't need to access global state.
You can just use useState to determine if you want to show a modal within a component:
export const Modal = (props) => {
<Transition show={props.show}>
<Dialog>
<Dialog.Title>My Modal Headline</Dialog.title>
<Dialog.Description>My Modal Description</Dialog.Description>
</Dialog>
</Transition>
}
export const ComponentWithModal = () => {
const [showModal, setShowModal] = useState(false);
return (
<>
<Modal show={showModal}/>
<Button text="Open Modal" onClick={() => setShowModal(true)} />
<Button text="Close Modal" onClick={() => setShowModal(false)} />
</>
)
}

why destroyOnClose={true} not working in React

I am developing a React hook based functional application with TypeScript and I am using modal from ant design. I'm submitting a form through modal for a table. So, the modal will be called for more than once to fill-up different rows of the table.
The problem is, when the modal is popping up for the second, third or lateral times, it's always carrying the previous values.
To avoid that, I set in the modal EnableViewState="false" , it didn't work . I set
destroyOnClose={true}. It didn't work. In the modal documentation, it is written when destroyOnClose doesn't work then we need to use . But where to define it ? Because, when I am setting up as,
<Form onSubmit={props.inputSubmit} preserve={false} in my modal form, I'm getting an error saying Type '{ children: Element[]; onSubmit: any; preserve: boolean; }' is not assignable to type 'IntrinsicAttributes & IntrinsicClassAttributes<Form>......?
what do you use so that every time the modal reloads, it reloads as empty ? I don't want to assign the state in the form value fields of the input. Is there any other option such as, destroyOnClose={true} ?
Here is my modal,
<Form onSubmit={props.inputSubmit}>
<Row>
<Col span={10}>
<Form.Item>
<Text strong={true}>Article name: </Text>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item>
<Input
style={{ backgroundColor: '#e6f9ff' }}
name="articleName"
onChange={props.handleArticleModalInput}
/>
</Form.Item>
</Col>
</Row>
</Form>
Here is from where the modal is getting called,
return (
<>
<ArticleTableModal
destroyOnClose={true}
isVisible={modalVisibilty}
inputSubmit={inputSubmit}
handleCancel={handleCancel}
filledData={fetchedData}
articleNumber={articleNumber}
handleArticleModalInput={handleArticleModalInput}
/>
<Table
pagination={false}
dataSource={articleDataSource}
columns={articleColumns}
scroll={{ y: 400 }}
bordered
/>
</>
)
Any help is much appreciated.
You need to generate dynamic keys for the fields in the form on each modal launch.
Here's a sandbox to play around. If you don't make any changes to the key, the modal retains values inside it. If you change key and launch modal, the value gets cleared.
Sandbox Link
import React from "react";
import ReactDOM from "react-dom";
import "antd/dist/antd.css";
import "./index.css";
import { Modal, Button, Input } from "antd";
class App extends React.Component {
state = { visible: false, theKey: "dummy" };
showModal = () => {
this.setState({
visible: true
});
};
handleOk = (e) => {
console.log(e);
this.setState({
visible: false
});
};
handleCancel = (e) => {
console.log(e);
this.setState({
visible: false
});
};
handleChange = ({ target: { value } }) => {
this.setState({ theKey: value });
};
render() {
return (
<>
<Input onChange={this.handleChange} placeholder="key for input field"/>
<br />
<Button type="primary" onClick={this.showModal}>
Open Modal
</Button>
<Modal
title="Basic Modal"
visible={this.state.visible}
onOk={this.handleOk}
onCancel={this.handleCancel}
>
<Input
key={this.state.theKey}
style={{ backgroundColor: "#e6f9ff" }}
name="articleName"
/>
</Modal>
</>
);
}
}
ReactDOM.render(<App />, document.getElementById("container"));
Here we'll use a custom hook that wraps a ModalDialog component from somewhere else (like a 3rd party UI library) and gives us back a tuple of a setter and a self-contained component/null. Hooks make this neater but you can still accomplish all of this with class components at the cost of a little verbosity. Since you tagged Typescript this should all be straightforward but you may have to specify that your use of useState is useState<React.ReactChild>(); to avoid type errors.
const useDialog = (ModalComponent) => {
const [modalDialogState, setModalDialogState] = useState();
return modalDialogState
? [
setModalDialogState,
// You may have to tweak this a bit depending on
// how your library works.
() => (
<ModalComponent onClose={() => setModalDialogState('')>
{modalDialogState}
</ModalComponent>
),
]
: [setModalDialogState, null];
};
const MyComponent = () => {
const [setModal, Modal] = useDialog(WhateverLibraryComponent);
useEffect(() => {
// Cleanup function, run on unMount and clears dialog contents.
return () => setModal('');
}, [setModal]);
return Modal
? (
<>
<Modal />
/* Normal render stuff */
</>
)
// You can optionally render a button here with onClick
// set to a function that calls setModal with some
// appropriate contents.
: (/* Normal render stuff */)
};

How to call a component onClick of a button in a functional component with MaterialUI Button

I'm using Material UI for a button and want to render a different functional component onClick of a button.
I'm doing this all in a functional component as well. ( is the component I want to trigger)
const driveAction=props =>{
return <SharedDriveAction/>;
}
const Vehicle = ({vehicle}) => {
const classes = useStyles();
return (
<Button
onClick= {() => { driveAction }}
size="small"
color="secondary"
className={classes.button}
>
Drive
</Button>
);
}
export default Vehicle;
It looks like you want to leverage some state to conditionally render the button.
If you want to control whether or not the button is visible based on a click action, keep track of which component should render in state, then toggle that state with the onClick handler.
If what I am suspecting is true, then this should do the trick for you.
const Vehicle = ({vehicle}) => {
const classes = useStyles();
// Controls whether the drive action is rendered or not
const [showDriveAction, setShowDriveAction] = React.useState(false)
return showDriveAction ?
<SharedDriveAction/> :
<Button
onClick= {() => setShowDriveAction(true)}
size="small"
color="secondary"
className={classes.button}
>
Drive
</Button>
}
export default Vehicle;
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
You can use conditional render with react state.
and if you want to toggle use Something like that:
const Vehicle = ({ vehicle }) => {
const classes = useStyles();
const [showDrive, setShowDrive] = React.useState(false)
return (
<div>
{showDriveAction ?
<SharedDriveAction/> :
<Button
onClick= {() => setShowDriveAction(!showDriveAction)}
size="small"
color="secondary"
className={classes.button}
>
Drive
</Button>}
</div>
)
}
More simply speaking, have your overarching component have a state variable of open for the conditional render. OnClick, hook this.setState (for class components) to toggle the open variable. Inline conditional rendering can be handled quite easily in the below example.
{ props.open ? <Component/> : null }

RadioGroup's in React forms using formik-material-ui

I have a multi-page form controlled by a material-ui stepper, with each 'subform' component being brought in via a switch on the stepper value. Works perfectly well. The state is being held by the parent component via judicious use of the useState hook, and the back and forward functions handleBack, and handleNext are being passed in as props.
The issue is, that whilst the Radio buttons are inserted correctly, and the chosen value put back into state, an error seems to stopping the useEffect hook that calculates values for the next subform from firing.
The error is :
Material-UI: A component is changing an uncontrolled RadioGroup to be controlled.
Elements should not switch from uncontrolled to controlled (or vice versa).
Decide between using a controlled or uncontrolled RadioGroup element for the lifetime of the component.
The code looks roughly like this (with the usual Grid components nuked):
import React from 'react'
import { Formik, Field, Form } from 'formik'
import { TextField, RadioGroup } from 'formik-material-ui'
import * as Yup from 'yup'
import { FormControlLabel, Radio, Button, Container } from '#material-ui/core'
import { Styles } from '../Styles'
export default function subForm(props) {
const classes = Styles()
const {
formChoice,
templateChoices,
formTitle,
activeStep,
isLastStep,
handleBack,
handleNext,
templates,
} = props
return (
<div>
<Formik
initialValues={formChoice}
validationSchema={Yup.object({
formChoice: Yup.string().required('Required'),
})}
>
{({
submitForm,
validateForm,
setTouched,
isSubmitting,
values,
setFieldValue,
formChoice,
}) => (
<Container>
<Form>
<Field
name="formChoice"
label="Radio Group"
value={formChoice || ''}
component={RadioGroup} >
{templateChoices.map(({ name, description, index }) => (
<FormControlLabel
value={name}
control={<Radio disabled={isSubmitting} />}
label={name} />
))}
</Field>
</Form>
<div className={classes.buttons}>
{activeStep !== 0 && (
<Button
onClick={() => {
handleBack(values)
}}
className={classes.button} >Back</Button>
)}
<Button
className={classes.button}
variant="contained"
color="primary"
onClick={() =>
validateForm().then(errors => {
if (
Object.entries(errors).length === 0 &&
errors.constructor === Object
) {
handleNext(values)
} else {
setTouched(errors)
}
})
}
>
{isLastStep ? 'Submit Draft' : 'Next'}
</Button>
</div>
</Container>
)}
</Formik>
</div>
)
}
The list of options to display is handed in in templateChoices, and looks roughly like this:
[
{"index":0,"name":"Form 1","description":"The standard form.","version":"1.0"},
{"index":1,"name":"Form 2","description":"The special form.","version":"1.1"}
]
Should I be using a RadioGroup? Is there something I'm forgetting to pass through props?

Resources