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?
Related
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 */)
};
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.
I'm trying to do some testing with enzyme and jest in react, and things work fine when I open a modal e.g. input fields in the modal aren't there and the modal state is false (as intended) when I try to find them using
expect(wrapper.find("input")).toHaveLength(0);
and do exist after I've opened the modal using
const edit = wrapper.find("Button.update-button");
edit.simulate("click");
expect(wrapper.find("input")).toHaveLength(2);
which all works (including the modal state turning to true after it opens) as intended. But when I close the modal, the state gets toggled off correctly, but the modal content (e.g. the input boxes and buttons in the modal) still exist when I try:
expect(wrapper.find("input")).toHaveLength(0);
I still somehow have 2 input fields that shouldn't be there as the modal is closed.
Here is my code for the component I am trying to test if that helps:
/*
Artefact Component displays just UI for the Artefact itself and it's information.
*/
import React, { Component } from "react";
import DeleteArtefact from "../DeleteArtefact";
import UpdateArtefact from "../UpdateArtefact";
import {
Card,
CardImg,
CardTitle,
CardBody,
ButtonGroup,
Button,
CardFooter
} from "reactstrap";
class Artefact extends Component {
// Initialise State
state = {
updatemodal: false,
deletemodal: false
};
// Toggle function for toggling modal open/close
toggleUpdate = () => {
this.setState({
updatemodal: !this.state.updatemodal
});
};
toggleDelete = () => {
this.setState({
deletemodal: !this.state.deletemodal
});
};
prepareUpdateState = () => {
this.props.editUpdate(this.props.artefact);
this.toggleUpdate();
};
render() {
const {
artefact,
onChange,
onUpdateClick,
editUpdate,
onDeleteClick
} = this.props;
return (
<Card>
<CardImg
src={artefact.img}
alt={`Image for Artefact ${artefact.name}`}
/>
<CardBody>
<CardTitle>
<h6>{artefact.name}</h6>
</CardTitle>
</CardBody>
<CardFooter>
<ButtonGroup>
<Button
className="update-button"
color="dark"
onClick={this.prepareUpdateState}
>
Edit
</Button>
<Button
className="delete-button"
color="dark"
onClick={this.toggleDelete}
>
Delete
</Button>
</ButtonGroup>
<UpdateArtefact
artefact={artefact}
onChange={onChange}
onUpdateClick={onUpdateClick}
editUpdate={editUpdate}
toggle={this.toggleUpdate}
modal={this.state.updatemodal}
/>
<DeleteArtefact
_id={artefact._id}
onDeleteClick={onDeleteClick}
toggle={this.toggleDelete}
modal={this.state.deletemodal}
/>
</CardFooter>
</Card>
);
}
}
export default Artefact;
And here is the UpdateArtefact Component that has the modal I'm trying to test:
/*
UpdateArtefact Component is a child Component of ArtefactGallery and
creates a new Artefact by using functions onChange() and updateClick()
and editUpdate() which are passed as props from ArtefactGallery and
passes state back up and makes api calls using axios.
*/
import React, { Component } from "react";
import {
Button,
Modal,
ModalHeader,
ModalBody,
Form,
FormGroup,
Label,
Input
} from "reactstrap";
class UpdateArtefact extends Component {
// Passes state up to ArtefactGallery component and updates the artefact.
onSubmit = e => {
e.preventDefault();
this.props.onUpdateClick(this.props.artefact._id);
this.props.toggle();
};
// Sets state in ArtefactGallery to the initial values of the artefact
// to prepare for any edits to be made in the case that some fields have
// no change, so that there are no null fields.
prepareUpdateState = () => {
this.props.editUpdate(this.props.artefact);
this.props.toggle();
};
render() {
const { artefact } = this.props;
return (
<div style={{ marginLeft: "1rem" }}>
<Modal isOpen={this.props.modal} toggle={this.props.toggle}>
<ModalHeader toggle={this.props.toggle}>
Edit Artefact
</ModalHeader>
<ModalBody>
<Form onSubmit={this.onSubmit}>
<FormGroup>
<Label>Artefact</Label>
<Input
type="text"
name="name"
id="artefactName"
defaultValue={artefact.name}
onChange={this.props.onChange}
/>
<Label>Image</Label>
<Input
type="text"
name="img"
id="artefactImg"
defaultValue={artefact.img}
onChange={this.props.onChange}
/>
<Button
className="modal-submit-button"
color="dark"
style={{ marginTop: "2rem" }}
block
>
Submit
</Button>
</FormGroup>
</Form>
</ModalBody>
</Modal>
</div>
);
}
}
export default UpdateArtefact;
So basically I just want to know what the reason if for why the modal content is still being picked up by enzyme and how to fix this. I've tried searching all over but couldn't find an answer so I'm guessing there's something obvious that I'm missing.
See, your components does not use conditional rendering like
{someFlag && <SomeElement>}
but just pass down isOpen prop:
<Modal isOpen={this.props.modal} toggle={this.props.toggle}>
so probably Modal just hides its props.children and input is kept.
As a workaround you may validate against ModalComponentYouHaveRendered.props().isOpen instead of checking amount of inputs
You can try using:
wrapper.update()
after closing the modal.
In this way, the wrapper should get updated.
I want to dynamically generate TextFields and then Store their values in an array in the state.
My imports:
import TextField from '#material-ui/core/TextField';
import Button from '#material-ui/core/Button';
I created a button below the TextField. I want to dynamically add TextFields after the button is clicked.
TextField required={true}
onChange={e => this.state.book_authors.push(e.target.value)}
className={this.props.classes.textField}
label={"Author"}
variant="outlined"
type="text"/>
<Button
variant="contained" color="primary"
action={() => this.handleAddAuthorField()}
className={this.props.classes.button}
>
Add Author
</Button>
My method:
handleAddAuthorField(): JSX.Element {
return (
<Row>
<TextField
required={true}
onChange={e => this.state.book_authors.push(e.target.value)}
className={this.props.classes.textField}
label={"Author"}
variant="outlined"
type="text"
/>
</Row>
)
}
The state, book_authors is to store the TextField inputs as an array.
public state: IsBookState = {
book_authors: [],
};
After I clicked the button the TextField didn't genereate.
There's several things here that are a bit off, but lets focus on the question. I feel like I'm missing some code, I'm not seeing where the text field is meant to be rendered.
You'd want something like the following:
this.state = { authors: [] } // Set in constructor
render() {
const { authors } = this.state;
return (
<div>
{authors.map(author => (
<TextField author={author} />
)}
<Button onClick={this.handleAddAuthorField}>Add author field</Button>
</div>
)
}
This code is very abridged, but my point is that you need to render your components somewhere - and like I said I'm not sure I fully understand what you're asking, please clarify what exactly is the intended functionality.
Using React / Redux and MaterialUI I set up a component which needs to check your location or get the lat/lng based upon the address you put in.
When clicking on a button I would like the address, typed in to a TextField to be checked on a webservice.
However I can not seem to get the value of the TextField to the webservice using the button.
So I tried to set the value immediately in the props (of the state).
This works, but I do get an error:
Warning: A component is changing an uncontrolled input of type text to be controlled. Input elements should not switch from uncontrolled to controlled (or vice versa).
This code is like this:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Field, reduxForm } from 'redux-form'
import TextField from 'material-ui/TextField';
import IconButton from 'material-ui/IconButton';
import PlaceIcon from 'material-ui-icons/Place';
import SearchIcon from 'material-ui-icons/Search';
import { getLocationByAddress, getLocationByLatLng } from '../actions';
/**
* Location Search, Lets the user search for his location and coordinates
* #class LocationSearch
* #extends React.Component
*/
class LocationSearch extends Component {
/**
* getLocationByLatLng: uses navigator.geolocation to get the current position of the user <br/>
* then requests the information from a back-end service to retrieve full location data.
*/
getLocationByLatLng () {
let options = {
enableHighAccuracy: true,
timeout: 5000,
maximumAge: 0
};
navigator.geolocation.getCurrentPosition(function (pos) {
console.log(pos.coords);
}, function(err){
console.warn(`ERROR(${err.code}): ${err.message}`);
}, options)
}
/**
* getLocationByAddress: uses address to request full location data from back-end service
*/
getLocationByAddress = (e) => {
/*
console.log(e);
e.preventDefault();
*/
this.props.getLocationByAddress(this.props.location.address);
};
keepVal = ({target}) => {
this.props.location.address = target.value;
};
render () {
const { location } = this.props;
return (
<div className={'locationSearch'}>
<IconButton onClick={this.getLocationByLatLng} color="primary" className={'locationSearch_locationBtn'} aria-label="">
<PlaceIcon fontSize={35} />
</IconButton>
<TextField
value={location.address}
placeholder={'Voer uw locatie in'}
margin={'normal'}
className={'locationSearch_input'}
onChange={this.keepVal}
/>
<IconButton onClick={this.getLocationByAddress} color="primary" className={'locationSearch_searchBtn'} aria-label="">
<SearchIcon fontSize={35} />
</IconButton>
</div>
);
}
}
function mapStateToProps (state) {
return {
location: state.location
}
}
export default connect(mapStateToProps, {getLocationByAddress, getLocationByLatLng})(LocationSearch)
So because I do not want any errors I also looked into changing the state just for the location address.
So I created a new action setLocationAddress and that should go to the reducer and change the state just for the address, but thats silly to do on every change, as the value is already there... no need to change it.
Thus I used a form to do this:
getLocationByAddress = (e) => {
e.preventDefault();
const { target } = e;
this.props.getLocationByAddress(target['locationInput'].value);
};
render () {
const { location } = this.props;
return (
<div className={'locationSearch'}>
<IconButton onClick={this.getLocationByLatLng} color="primary" className={'locationSearch_locationBtn'} aria-label="">
<PlaceIcon fontSize={35} />
</IconButton>
<form onSubmit={this.getLocationByAddress}>
<TextField
name={'locationInput'}
value={location.address}
placeholder={'Voer uw locatie in'}
margin={'normal'}
className={'locationSearch_input'}
/>
<IconButton color="primary" className={'locationSearch_searchBtn'} aria-label="">
<SearchIcon fontSize={35} />
</IconButton>
</form>
</div>
);
}
But again this naggy message:
Warning: A component is changing an uncontrolled input of type text to be controlled. Input elements should not switch from uncontrolled to controlled (or vice versa)
So how do I do this?
Any help would be appreciated.
See the code for the controlled component in the TextField demo. In the code, the value prop is pulled from state and onChange is set to a function that updates state.
<div>
<TextField id="standard-name" label="Name" value={name} onChange={handleChange} />
<TextField id="standard-uncontrolled" label="Uncontrolled" defaultValue="foo" />
</div>
You're actually pretty close. You've mapped state to your location prop and you've set value={location.address}, but onChange is assigned to this.keepVal which attempts to update props, which is no good.
Since you're using Redux, you need to dispatch an action in keepVal that will update state.location.address with event.target.value. This will ensure that the component value is populated from state and that changes from fired onChange events update the store.