Content in reactstrap modal continues to exist after closing using enzyme/jest - reactjs

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.

Related

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 */)
};

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?

Refreshing issue when submitting form with react-redux-form

Issue
Important: This has been solved; see below explanation.
I've been having this issue of the page refreshing when submitting a react-redux-form. I have found a lot of similar issues since this is the default behavior of submitting a form in HTML; however I haven't found anything related to this specific library (react-redux-form).
I've also tried to apply what was suggested in other cases, mainly the event.preventDefault(), but I don't know how to use the event object with this library as they recommend the following syntax:
<LocalForm onSubmit={(values) => this.handleSubmit(values)}>
I've tried with values.event, but that was unsuccessful.
Below what I intend to have:
import React, { Component } from 'react';
import Hall from './HallComponent';
import { Row, Button, Label } from 'reactstrap';
import { Redirect } from 'react-router';
import { LocalForm, Control } from 'react-redux-form';
class Endgame extends Component {
constructor(props) {
super(props);
this.state = {
redirect: false
};
this.handleSubmit = this.handleSubmit.bind(this);
}
handleSubmit(values) {
const date = new Date();
this.props.addScore({
date: date.toLocaleDateString("en-GB"),
name: values.name,
score: this.props.points
});
this.setState({ redirect: true });
}
render() {
if (this.state.redirect) {
return (
<Redirect to="/hall" />
);
}
else {
return(
<div className="container">
<div className="row">
<div className="col-12 col-md-5 m-1">
<p>Your score is: {this.props.points}</p>
<p>Add you name to the Hall of Fame</p>
<LocalForm onSubmit={(values) => this.handleSubmit(values)}>
<Row className="form-group">
<Label htmlFor="name">Nickname</Label>
<Control.text model=".name" id="name" name="name" placeholder="Your Name" className="form-control" />
</Row>
<Button type="submit">Submit</Button>
</LocalForm>
</div>
<div className="col-12 col-md-5 m-1">
<Hall scores={this.props.scores} />
</div>
</div>
</div>
);
}
}
}
export default Endgame;
Solution
I've found that the problem was not coming from the submit button but the way I had arranged my components.
I'm still not entirely sure of what actually happens behind the scene, but my Hall component above was unmounted and re-mounted each time the addScore() e.g. a redux action was fired. I found out that this could happen if the state of the parent component was modified somehow this could trigger re-mounting of children components.
I've migrated the state locally to Hall component connecting it to redux store and it now works properly.
its because you have a button of type "submit". Its default action is to refresh the page.
<Button type="submit">Submit</Button>
You can put an evt.preventDefault() in your handle submit function to stop the page from refreshing, but I would encourage you to just change the button type and put your handleSubmit function on the onClick event like this.
<Button type="button" onClick={this.handleSubmit}>Submit</Button>

How to set TextField changes in to state using MaterialUI and Redux?

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.

Enter key event handler on react-bootstrap Input component

I have an Input component with a button (buttonAfter property), I set an onClick handler associated to the button and so user can type some text and clic the button to fire the right action.
However, I would like user to be able to press [Enter] key (keycode 13) to achieve the same effect as clicking on the button, just to make the UI easier to use.
I could not find a way to do it, of course I tried onKeydown to register an handler for key down event but it is just ignored.
I think this question is related to React itself instead of react-bootstrap.
Look at this for some basics about React event system: https://facebook.github.io/react/docs/events.html
When you use onKeyDown, onKeyPress or onKeyUp React will pass to your handler an instance of say "target" object with the following properties:
boolean altKey
number charCode
... (for all see link above)
So you can do something like this:
import React, { PropTypes } from 'react';
import ReactDOM from 'react-dom';
import { Input } from 'react-bootstrap';
class TestInput extends React.Component {
handleKeyPress(target) {
if(target.charCode==13){
alert('Enter clicked!!!');
}
}
render() {
return (
<Input type="text" onKeyPress={this.handleKeyPress} />
);
}
}
ReactDOM.render(<TestInput />, document.getElementById('app'));
I tested above code and it works. I hope this is helpful for you.
The question can also be related to React-bootstrap.
React-bootstrap also has a way to handle instance when ever a button or enter key or any form element is pressed.
The below code explains how to handle an instance when enterkey is pressed without the involvement of React Handlers.(and that makes it cool)
import React from "react";
import ReactDOM from "react-dom";
import { FormGroup, FormControl } from "react-bootstrap";
class TestInput extends Component {
search() {
console.log("Enter Button Pressed");
}
render() {
return (
<FormGroup>
<InputGroup>
<FormControl
placeholder="Press Enter"
type="input"
onKeyPress={event => {
if (event.key === "Enter") {
this.search();
}
}}
/>
</InputGroup>
</FormGroup>
);
}
}
React Bootstrap does not support Input form element anymore.
Instead it introduced below items at your disposal
The FormGroup component wraps a form control with proper spacing, along with support for a label, help text, and validation state.
Wrap your form control in an InputGroup, then use for normal add-ons and for button add-ons.
The FormControl component renders a form control with Bootstrap styling.
References:
https://react-bootstrap.github.io/components.html#forms
https://react-bootstrap.github.io/components.html#forms-input-groups
The right way to do it in a form is the same like in regular JavaScript:
Don't use onClick on a button but use type="submit"
The form should be wrapped with <form onSubmit={handler}>
Handler should prevent page reload handler = (event) => event.preventDefault(); processForm()
Now both the button and pressing Enter on any field will call handler
Piece of functional component doing this
function register(event) {
event.preventDefault()
distpach(authActionCreator.register(username, email, password));
}
return (
<Card>
<form onSubmit={register}>
<Card.Body>
<Card.Title as="h4">Register</Card.Title>
<FormGroup controlId="username">
<FormLabel>Username</FormLabel>
<FormControl type="text" label="Username" placeholder="Username" onChange={handleChange} />
</FormGroup>
<FormGroup controlId="email">
<FormLabel>Email</FormLabel>
<FormControl type="email" label="Email" placeholder="Email" onChange={handleChange} />
</FormGroup>
<FormGroup controlId="password">
<FormLabel>Password</FormLabel>
<FormControl type="password" label="Password" placeholder="Password" onChange={handleChange} />
</FormGroup>
<ButtonToolbar>
<Button variant="primary" type="submit">Register</Button>
</ButtonToolbar>
</Card.Body>
</form>
</Card>
);
A somewhat abbreviated version of the already suggested solutions is:
import React from 'react';
import ReactDOM from 'react-dom';
import { Input } from 'react-bootstrap';
const TestInput = () => {
const handleSubmit = () => {
/* handle form submit here */
}
return (
<Input type="text" onKeyPress={event => event.key === "Enter" && handleSubmit()} />
);
}
ReactDOM.render(<TestInput />, document.getElementById('app'));
onKeyPress={event => event.key === "Enter" && handleSubmit()} will ensure handleSubmit is called when the Enter key is pressed.
Uncaught TypeError: Cannot read property 'charCode' of undefined
You have to use:
handleKeyPress(target) {
if (target.key === 'Enter') {
alert('Enter clicked!!!');
}
}

Resources