I'm trying to handle the selection of dynamically generated <option>'s in a <select> element. I understand that the onChange trigger is what i need to setState with but i can't seem to get her to work.
Here's what i've got going on:
See the Pen dynamic select by Archibald Hammer (#archaeopteryx) on CodePen.
import React from 'react'
import ReactDOM from 'react-dom'
import _ from 'lodash'
const ITEMS = [
{ name: 'centos', text: 'centos', value: 'centosValue' },
{ name: 'ubuntu', text: 'ubuntu', value: 'ubuntuValue' },
]
const SelectComponent = (props) => (
<select name={props.name}>
{_.map(props.items, (item, i) => <Option
key={i}
name={item.name}
value={item.value}
text={item.text}
handleSelect={props.handleSelect}
/>
)}
</select>
)
const Option = (props) => (
<option
value={props.value}
onChange={props.handleSelect}>{props.text}</option>
)
class App extends React.Component {
constructor() {
super()
this.state = {
selected: ''
}
this.handleSelect = this.handleSelect.bind(this)
}
handleSelect(e) {
this.setState({selected: e.target.value})
}
render() {
return (
<div>
<SelectComponent
name="testSelect"
items={ITEMS}
handleSelect={this.handleSelect}
/>
<div>
<p>Selected: {this.state.selected}</p>
</div>
</div>
)
}
}
ReactDOM.render(<App/>, document.getElementById('app'))
This code does render the dropdown selector as expected but it isn't triggering the setState on the selected item. Any thoughts?
Also, does anyone have any pro-tips for troubleshooting this kind of problem? Any super slick dev-tools you know of or methods for finding out which props are being passed, etc?
The problem is that the options in a select element won't trigger any event, the change is happening in the select element not the option. All you have to do is pass the handleSelect method to the <select> component:
const SelectComponent = (props) => (
<select name={props.name}
onChange={props.handleSelect}
>
{_.map(props.items, (item, i) => <Option
key={i}
name={item.name}
value={item.value}
text={item.text}
handleSelect={props.handleSelect}
/>
)}
</select>
);
const Option = (props) => (
<option
value={props.value}
>{props.text}</option>
)
Sorry I forgot to add the live sample link:
https://codesandbox.io/s/31AyQ2woR
In terms of a tip for debugging, in this particular case just know that the event is triggered by the select component and not the option element ;). But the one I use all the time is React developer tools:
https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi
https://addons.mozilla.org/es/firefox/addon/react-devtools/
Related
I got TextField to work, now the Material UI Select will turn red if no selection is made but stays red after selection is made and won't let form submit. I'm using Yup as validation library.Maybe I keep using wrong Yup type I try String and array but I can't get it to work.
import {
makeStyles,
Box,
Select,
FormControl,
InputLabel,
MenuItem,
Typography,
} from "#material-ui/core";
import * as yup from 'yup';
import { yupResolver } from '#hookform/resolvers'
import { useForm, Controller } from "react-hook-form";
const FormFields = ({ typeOfInquiry, typeOfProviderSupplier, feedbackform }) => {
const schema = yup.object().shape({
typeofInquiry: yup.array().nullable().required(),
});
const { handleSubmit, control, reset, errors } = useForm();
return (
<Controller
style={{ minWidth: 220 }}
name="typeofInquiry"
render ={({ field: { ...field }, fieldState })=>{
console.log(props)
return ( <Select {...field} >
{typeOfInquiry.map((person) => (
<MenuItem key={person.value} value={person.value} >
{person.label}
</MenuItem>
))}
</Select>
)
}}
control={control}
defaultValue=" "
/>
<Typography className={classes.red}>{errors.typeofInquiry?.message}</Typography>
</FormControl>
</form>
);
}
You've to pass the ref to the TextField component.
Here is a working example
👉🏻 https://codesandbox.io/s/exciting-pateu-3n0i9
You should do something similar with Select.
Some examples with MUI: https://codesandbox.io/s/react-hook-form-v7-controller-5h1q5?file=/src/Mui.js
In the following block of code, the IonToggle is firing twice for some unknown reason. I had it already replaced with a normal button and it works fine. If I keep the IonToggle and remove the line setUpdating(true) it also works fine.
Is it some known bug, or is there something wrong with this code.
import { AppContext } from './../AppContextProvider';
const LightController: React.FC<InterfaceLamp> = ({ id, color, brightness, turnedOn }) => {
const { state, dispatch } = useContext(AppContext);
const [isUpdating, setUpdating] = useState(false);
const isMount = useIsMount();
const handleUpdateToggle = async (isToggled: boolean) => {
lightService.toggleLight(state.api, id, isToggled, state.auth.username,
state.auth.password).then((res) => {
if (!res.error) {
[...]
dispatch({
key: 'devices',
data: devices,
})
}
setUpdating(false);
})
}
const handleToggle = (isToggled: boolean) => {
setUpdating(true);
handleUpdateToggle(isToggled);
}
return (
<div className="c-light">
<Loader isLoading={isUpdating} message={"Updating devices"} onClose={() => { }} />
<div className="c-light__controls">
<div className="c-light__toggle">
<IonItem lines={"none"}>
<button onClick={(e)=>handleToggle(!turnedOn)}>toggle</button>
<IonToggle checked={turnedOn} onIonChange={(e) => handleToggle(e.detail.checked)}/>
</IonItem>
</div>
</div>
</div>
);
};
export default LightController;
value="true"
why are you setting the value here? dont think it is necessary
As of Ionic React v5.5.0, IonToggle and IonCheckbox have still got the same issue.
The simplest workaround is to add an onClick event listener to the IonItem component that usually wraps the IonToggle component. (Alternately, use any other wrapper component with onClick.) This approach makes it possible to keep the native-looking IonToggle.
I have a normal select list. I need to test handleChoice gets called when I choose an option. How can I do this with React Testing Library?
<select
onChange={handleChoice}
data-testid="select"
>
<option value="default">Make your choice</option>
{attributes.map(item => {
return (
<option key={item.key} value={item.key}>
{item.label}
</option>
);
})}
</select>
getByDisplayValue with the value of item.label doesn't return anything, perhaps this is because it's not visible on the page?
Add data-testid to the options
<option data-testid="select-option" key={item.key} value={item.key}>
{item.label}
</option>
Then, in the test call fireEvent.change, get all the options by getAllByTestId and check the selected option:
import React from 'react';
import { render, fireEvent } from '#testing-library/react';
import App from './App';
test('Simulates selection', () => {
const { getByTestId, getAllByTestId } = render(<App />);
//The value should be the key of the option
fireEvent.change(getByTestId('select'), { target: { value: 2 } })
let options = getAllByTestId('select-option')
expect(options[0].selected).toBeFalsy();
expect(options[1].selected).toBeTruthy();
expect(options[2].selected).toBeFalsy();
//...
})
For your question, the getByDisplayValue works only on displayed values
This solution didn't work for me, what did work, was userEvent.
https://testing-library.com/docs/ecosystem-user-event/
import React from 'react';
import { render } from '#testing-library/react';
import userEvent from '#testing-library/user-event';
import App from './App';
test('Simulates selection', () => {
const { getByTestId } = render(<App />);
// where <value> is the option value without angle brackets!
userEvent.selectOptions(getByTestId('select'), '<value>');
expect((getByTestId('<value>') as HTMLOptionElement).selected).toBeTruthy();
expect((getByTestId('<another value>') as HTMLOptionElement).selected).toBeFalsy();
//...
})
You can also forgo having to add a data-testid to the select element if you have a label (which you should!), and simply use getByLabelText('Select')
Further still, you can get rid of the additional data-testid on each option element, if you use getByText.
<label for="selectId">
Select
</label>
<select
onChange={handleChoice}
id="selectId"
>
<option value="default">Make your choice</option>
{attributes.map(item => {
return (
<option key={item.key} value={item.key}>
{item.label}
</option>
);
})}
</select>
Then:
import React from 'react';
import { render } from '#testing-library/react';
import userEvent from '#testing-library/user-event';
import App from './App';
test('Simulates selection', () => {
const { getByLabelText, getByText } = render(<App />);
// where <value> is the option value without angle brackets!
userEvent.selectOptions(getByLabelText('<your select label text>'), '<value>');
expect((getByText('<your selected option text>') as HTMLOptionElement).selected).toBeTruthy();
expect((getByText('<another option text>') as HTMLOptionElement).selected).toBeFalsy();
//...
})
This seems a more optimal way to do this type of test.
**In 2021, you can use userEvent which comes as part of the React Testing Library ecosystem. It allows you to write tests which are closer to real user behaviour. Example
it('should correctly set default option', () => {
render(<App />)
expect(screen.getByRole('option', {name: 'Make a choice'}).selected).toBe(true)
})
it('should allow user to change country', () => {
render(<App />)
userEvent.selectOptions(
// Find the select element
screen.getByRole('combobox'),
// Find and select the Ireland option
screen.getByRole('option', {name: 'Ireland'}),
)
expect(screen.getByRole('option', {name: 'Ireland'}).selected).toBe(true)
})
Have a look at this article for more info on how to test select elements using React Testing Library.
You can also use getByText which will save you from adding the data-testid attribute
In my case I did
import { screen } from '#testing-library/react';
....
<select>
<option value="foo">Foo</option>
<option selected value="bar">Bar</option>
</select>
....
expect((screen.getByText('Foo') as HTMLOptionElement).selected).toBeFalsy();
expect((screen.getByText('Bar') as HTMLOptionElement).selected).toBeTruthy();
This seems simpler to me:
userEvent.selectOptions(screen.getByLabelText('County'), 'Aberdeenshire');
1: Install user-event V14 and above
npm:
npm install --save-dev #testing-library/user-event
yarn:
yarn add --dev #testing-library/user-event
2: Import and set in up: (it's a default import so make sure you didn't import named one)
import userEvent from '#testing-library/user-event';
test('should change data', async () => {
const user = userEvent.setup();
});
3: Change selected option and test:
import userEvent from '#testing-library/user-event';
test('should change data', async () => {
const user = userEvent.setup();
// Check if default value in correct:
expect(screen.getByRole('option', { name: /default option/i }).selected).toBeTruthy();
// Change option: (Make sure to use Async/Await syntax)
await user.selectOptions(screen.getByRole("combobox"), 'optionToSelect');
// Check the result:
expect(screen.getByRole('option', { name: /optionToSelect/i }).selected).toBeTruthy();
});
In my case I was checking a number against a string, which failed. I had to convert the number with toString() to make it work.
The HTML:
<select>
<option value="1">Hello</option>
<option value="2">World</option>
</select>
And in my test:
await fireEvent.change(select, { target: { value: categories[0].id.toString() } });
expect(options[1].selected).toBeTruthy();
Given the parent component, I am using a child component DynamicFieldSet that is a grouping of FormItems. But I am receiving the error:
Warning: validateDOMNesting(...): <form> cannot appear as a descendant of <form>. See CreateTopic > Form > form > ... > DynamicFieldSet > Form > form.
I have tried to remove the <Form> </Form> tags in my child component, but then it is a compile error.
Is there a way I can disable rendering of the child Form view?
Parent component
class CreateTopic extends React.Component {
render() {
return (
<div className="create-topic-container">
<h3>Create an event</h3>
<Form onSubmit={this.handleSubmit}>
<FormItem>...</FormItem>
<FormItem>...</FormItem>
<FormItem>...</FormItem>
<FormItem
{...formItemLayout}
label="Results"
style={{ marginBottom: SPACING_FORM_ITEM }}
>
{getFieldDecorator('results', {
rules: [
{
required: true,
message: 'Results cannot be empty.',
},
],
})(<DynamicFieldSet
form={this.props.form}
/>)}
</FormItem>
</Form>
</div>
);
}
}
DynamicFieldSet - Child component
export class DynamicFieldSet extends React.Component {
render() {
getFieldDecorator('keys', { initialValue: ['0', '1'] });
const keys = getFieldValue('keys');
const formItems = keys.map((k, index) => {
return (
<FormItem
{...formItemLayoutWithOutLabel}
required={false}
key={k}
>
{getFieldDecorator(`results[${k}]`, {
validateTrigger: ['onChange', 'onBlur'],
rules: [
{
required: true,
whitespace: true,
message: 'Result name cannot be empty.',
},
{
validator: this.validateLength,
},
],
})(<Input placeholder={`Result #${index + 1}`} style={{ width: '80%', marginRight: 8 }} />)}
{keys.length > 2 ? (
<Icon
className="dynamic-delete-button"
type="minus-circle-o"
disabled={keys.length === 1}
onClick={() => this.remove(k)}
/>
) : null}
</FormItem>
);
});
return (
<Form>
{formItems}
<FormItem {...formItemLayoutWithOutLabel}>
{keys.length < 10 ? (
<Button type="dashed" onClick={this.add} style={{ width: '80%' }}>
<Icon type="plus" />Add Result
</Button>
) : null}
</FormItem>
</Form>
);
}
}
I faced this issue when using ant design table and turns out its not ant design which throws the warning. It's the web standards description
"Every form must be enclosed within a FORM element. There can be several forms in a single document, but the FORM element can't be nested."
So, there should not be a form tag inside a form tag.
To solve the issue (in our case), remove the Form tag inside the DynamicFieldSet "return" and replace with a div tag
Hope it helps :)
You can portal a form like this:
import Portal from '#material-ui/core/Portal';
const FooComponent = (props) => {
const portalRef = useRef(null);
return <>
<form>
First form
<div ref={portalRef} />
</form>
<Portal container={portalRef.current}>
<form>Another form here</form>
</Portal>
</>;
}
In the example above I use the react material-ui Portal component. But you can try to implement it with React Portals as well
If you're using MUI, the Box component contains an attribute that identifies them as any native HTML container; form is one of them. E.g:
<Box
xs={6}
sx={{
"& > :not(style)": { m: 1, width: "25ch" },
}}
component="form"
noValidate
autoComplete="off"
>
In such case, we just need to delete that attribute, it will default to a DIV. The form will continue to work as expected, and the error will disappear off the console.
In my case this is occur bcoz of i declared <form> inside another <form/> tag.
This is a bit longwinded so I'll do my best to explain clearly.
I'm making a simple poll app and on the home page is an array of polls where you can vote on each poll.
Each poll is on a card and there will be different radio buttons representing the different voting options for that poll.
I'm trying to set up a form for each poll which contains radio button inputs for each of the different options and push that onSubmit to an action creator.
However, I would also like to pass that title of the poll as well as an argument to the action creator so that I can create a single action creator that will help me submit the votes for all the polls. Something like submitVote(title, option).
Here is my polls page:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import * as actions from '../../actions';
import Loading from '../Loading';
class MyPolls extends Component {
constructor(props) {
super(props);
this.state = {
skip: 0,
isLoading: true,
isLoadingMore: false,
value: ''
};
this.handleSubmit = this.handleSubmit.bind(this);
this.handleChange = this.handleChange.bind(this);
}
componentDidMount() {
this.props.fetchMyPolls(this.state.skip)
.then(() => {
setTimeout(() => {
this.setState({
skip: this.state.skip + 4,
isLoading: false
});
}, 1000);
});
}
sumVotes(acc, cur) {
return acc.votes + cur.votes
}
loadMore(skip) {
this.setState({ isLoadingMore: true });
setTimeout(() => {
this.props.fetchMyPolls(skip)
.then(() => {
const nextSkip = this.state.skip + 4;
this.setState({
skip: nextSkip,
isLoadingMore: false
});
});
}, 1000);
}
handleSubmit(e) {
e.preventDefault();
}
handleChange(event) {
console.log(event.target.value);
this.setState({ value: event.target.value });
}
renderPolls() {
return this.props.polls.map(poll => {
return (
<div className='card' key={poll._id} style={{ width: '350px', height: '400px' }}>
<div className='card-content'>
<span className='card-title'>{poll.title}</span>
<p>Total votes: {poll.options.reduce((acc, cur) => { return acc + cur.votes }, 0)}</p>
<form onSubmit={this.handleSubmit}>
{poll.options.map(option => {
return (
<p key={option._id}>
<input
name={poll.title}
className='with-gap'
type='radio'
id={option._id}
value={option.option}
onChange={this.handleChange}
/>
<label htmlFor={option._id}>
{option.option}
</label>
</p>
)
})}
<button
type='text'
className='activator teal btn waves-effect waves-light'
style={{
position: 'absolute',
bottom: '10%',
transform: 'translateX(-50%)'
}}
>
Submit
<i className='material-icons right'>
send
</i>
</button>
</form>
</div>
<div className='card-reveal'>
<span className='card-title'>{poll.title}
<i className='material-icons right'>close</i>
</span>
<p>
dsfasfasdf
</p>
</div>
</div>
)
})
}
render() {
return (
<div className='center-align container'>
<h2>My Polls</h2>
{this.state.isLoading ? <Loading size='big' /> :
<div style={{ display: 'flex', flexWrap: 'wrap', justifyContent: 'space-evenly', alignItems: 'center', alignContent: 'center' }}>
{this.renderPolls()}
</div>}
<div className='row'>
{this.state.isLoadingMore ? <Loading size='small' /> :
<button
className='btn red lighten-2 wave-effect waves-light' onClick={() => this.loadMore(this.state.skip)}>
Load More
</button>}
</div>
</div>
);
}
}
function mapStateToProps({ polls }) {
return { polls }
}
export default connect(mapStateToProps, actions)(MyPolls);
Demo of the app so far: https://voting-app-drhectapus.herokuapp.com/
(use riverfish#gmail.com and password 123 to login).
Github repo: https://github.com/drhectapus/Voting-App
I'd like to program it so that when form is submitted via this.handleSubmit, the handleSubmit function can take 2 arguments, title and option and pass that onto an action creator in redux.
How do I do this?
It's a little difficult to understand everything going on here, but I get the sense that your main goal is to pass two args to this.handleSubmit. You may instead consider just passing poll.title and grabbing the selected option from state. Try something like this:
this.handleSubmit(title) {
// this.state.value should already have the selected option!
let obj = {
title,
option: this.state.value
};
// dispatch the object to redux, update your reducer, etc.
}
And in your render, be sure to bind poll.title as the argument:
render() {
...
<form onSubmit={this.handleSubmit.bind(this, poll.title)}>
}
Does that help at all? Let me know if I'm totally missing the mark on what you intend. With .bind() you pass the this context to use followed by a list of common separated args, so you could pass multiple args, but it's much easier to just grab option from state in this case.
Edit
If you want to access the SyntheticEvent that gets fired on submit, you simple specify it as the second argument to this.handleSubmit like so:
this.handleSubmit(title, event) {
// prevent form submit
event.preventDefault();
}
// this is the exact same as above, no need to pass event
render() {
...
<form onSubmit={this.handleSubmit.bind(this, poll.title)}>
}
In React, synthetic events are always passed as the last argument to a bound function and simply need to be specified to be in the method definition (no need to specify in render). This is Function.prototype.bind way of working with functions and events in React. Here are the supporting docs: https://reactjs.org/docs/handling-events.html#passing-arguments-to-event-handlers