Using useComboBox from DownShift with react-hook-form - react-hook-form

I'm trying to use useComboBox from DownShift with react-hook-form and the value of the input is always undefined. I started with this: https://codesandbox.io/s/react-hook-form-controller-079xx?file=/src/DonwShift.js
And replaced the DownShift.js component with this: https://codesandbox.io/s/usecombobox-usage-1fs67?file=/src/index.js:168-438
Everything works except when I submit the value is undefined.What am I missing to set the value?
<form className="card" onSubmit={handleSubmit(handleShare)}>
<div className="body">
<Controller
as={Autocomplete}
control={control}
name="recipient"
items={userList}
/>
<button
className="secondaryActionBtn inputBtn"
type="submit"
enabled={String(formState.dirty)}
>
<FontAwesomeIcon icon={faPlus} />
</button>
{errors.lastname && 'Feed Name is required.'}
</div>
<footer></footer>
</form>
import React, { memo, useState } from 'react';
import PropTypes from 'prop-types';
import { useCombobox } from 'downshift';
const menuStyles = {
maxHeight: '180px',
overflowY: 'auto',
width: '135px',
margin: 0,
borderTop: 0,
background: 'white',
position: 'absolute',
zIndex: 1000,
listStyle: 'none',
padding: 0,
left: '135px'
};
const comboboxStyles = { display: 'inline-block', marginLeft: '5px' };
function Item({ isHighlighted, getItemProps, item, index }) {
return (
<li
style={isHighlighted ? { backgroundColor: '#bde4ff' } : {}}
key={`${item}${index}`}
{...getItemProps({ item, index })}
>
{item}
</li>
);
}
Item = memo(Item);
const Autocomplete = ({ items }) => {
const [inputItems, setInputItems] = useState(items);
const {
isOpen,
getToggleButtonProps,
getLabelProps,
getMenuProps,
getInputProps,
getComboboxProps,
highlightedIndex,
getItemProps
} = useCombobox({
items: inputItems,
onInputValueChange: ({ inputValue }) => {
setInputItems(
items.filter(item =>
item.toLowerCase().includes(inputValue.toLowerCase())
)
);
}
});
return (
<div>
<label htmlFor="recipient" {...getLabelProps()}>
Choose an element:
</label>
<div style={comboboxStyles} {...getComboboxProps()}>
<input name="recipient" {...getInputProps()} id="recipient" />
<button {...getToggleButtonProps()} aria-label="toggle menu">
↓
</button>
</div>
<ul {...getMenuProps()} style={menuStyles}>
{isOpen &&
inputItems.map((item, index) => (
<Item
key={item}
isHighlighted={highlightedIndex === index}
getItemProps={getItemProps}
item={item}
index={index}
/>
))}
</ul>
</div>
);
};
Autocomplete.propTypes = {
list: PropTypes.array
};
export default Autocomplete;

For others who get stuck on this here's how I solved it. The Controller in react-hook-form injects an onChange into the component as a prop. So i set the onSelectedItemChange prop in useCombobox hook to pass its value into onChange. Like this:
const {
isOpen,
getToggleButtonProps,
getLabelProps,
getMenuProps,
getInputProps,
getComboboxProps,
highlightedIndex,
getItemProps
} = useCombobox({
items: inputItems,
onSelectedItemChange: ({ inputValue }) => onChange(inputValue),
onInputValueChange: ({ inputValue }) => {
setInputItems(
items.filter(item =>
item.toLowerCase().includes(inputValue.toLowerCase())
)
);
}
});

Related

I want to get my modified post, net ADD modified post on list (React - using react-redux)

// store.js
import { createStore, combineReducers } from "redux";
const INITIAL_STATE = [];
const postingReducer = (state, action) => {
if (action.type === "POST_SUCCESS") {
const newPost = {
title: action.payload.title,
content: action.payload.content,
};
return state.concat(newPost);
} else if (action.type === "POST_DELETE") {
return state.filter((item) => {
return item.title !== action.payload;
});
} else if (action.type === "POST_EDIT_SUCCESS") {
const modifiedPost = {
title: action.payload.title,
content: action.payload.content,
};
// console.log(modifiedPost)
// console.log(state.map((item,index)=>item[0]))
// console.log(state[0].title)
// state[0].title = modifiedPost.title
// state[0].content = modifiedPost.title
return state.concat(modifiedPost);
}
return INITIAL_STATE;
};
const store = createStore(
combineReducers({
posting: postingReducer,
})
);
export default store;
// EditForm.js
import { Link , useParams} from "react-router-dom";
import { useState } from "react";
import { useDispatch , useSelector } from "react-redux";
const EditForm = () => {
const dispatch = useDispatch();
const state = useSelector(state=>state.posting)
const params = useParams()
const [titleInput, setTitleInput] = useState(state[params.title].title);
const [contentInput, setContentInput] = useState(state[params.title].content);
const publishBtn = () => {
window.alert('Modified.')
return dispatch({
type: "POST_EDIT_SUCCESS",
payload: { title: titleInput, content: contentInput },
});
}
return (
<>
<form>
<div>
<label htmlFor="Title">Title</label>
<br />
<input
id="Title"
value={titleInput}
onChange={event=>{setTitleInput(event.target.value)}}
type="text"
/>
<br />
</div>
<br />
<div>
<label htmlFor="Content">Content</label>
<br />
<textarea
id="Content"
value={contentInput}
onChange={event=>{setContentInput(event.target.value)}}
type="text"
/>
<br />
</div>
<div>
<Link to="/">
<button onClick={publishBtn}>Modify</button>
</Link>
</div>
</form>
</>
);
};
export default EditForm
I want to get my modified post, net ADD modified post on list.
when i click Modify button in 'EditForm.js' I want to get my modified post, net ADD modified post on list.
in this situation, modified post added on the postlist.
I don't know how to fix "POST_EDIT_SUCCESS" return in 'store.js'
please help me!

antd Form.Item accepts only one child

I've created a little Fiddle to illustrate the issue: https://stackblitz.com/edit/react-avejvc-mmhqda?file=index.js
This form works:
<Form initialValues={{ surname: 'Mouse'}}>
<Form.Item name="surname">
<Input />
</Form.Item>
</Form>
This form doesn't:
<Form initialValues={{ surname: 'Mouse'}}>
<Form.Item name="surname">
<Input />
{null}
</Form.Item>
</Form>
The only difference is that the Form.Item in the second form has two children.
Is there an intention behind this?
In case anyone wonders why I am asking. So sth like this is breaking the form:
<Form.Item name={name}>
{type==="string" && <Input />}
{type==="integer" && <InputNumber />}
</Form.Item>
The official documentation here gives examples of using multiple children in one Form.Item.
<Form.Item label="Field">
<Form.Item name="field" noStyle><Input /></Form.Item> // that will bind input
<span>description</span>
</Form.Item>
You appear to have a problem with what you are putting in the Form.Item, ie. {null} may not be allowed.
I found a solution and have a better understanding now of what is going on.
From the docs (https://ant.design/components/form/#Form.Item):
After wrapped by Form.Item with name property, value(or other property defined by valuePropName) onChange(or other property defined by trigger) props will be added to form controls, the flow of form data will be handled by Form
There is a working example in the docs too, here is the codepen: https://codepen.io/pen?&editors=001
const { useState } = React;;
const { Form, Input, Select, Button } = antd;
const { Option } = Select;
const PriceInput = ({ value = {}, onChange }) => {
const [number, setNumber] = useState(0);
const [currency, setCurrency] = useState('rmb');
const triggerChange = (changedValue) => {
onChange?.({
number,
currency,
...value,
...changedValue,
});
};
const onNumberChange = (e) => {
const newNumber = parseInt(e.target.value || '0', 10);
if (Number.isNaN(number)) {
return;
}
if (!('number' in value)) {
setNumber(newNumber);
}
triggerChange({
number: newNumber,
});
};
const onCurrencyChange = (newCurrency) => {
if (!('currency' in value)) {
setCurrency(newCurrency);
}
triggerChange({
currency: newCurrency,
});
};
return (
<span>
<Input
type="text"
value={value.number || number}
onChange={onNumberChange}
style={{
width: 100,
}}
/>
<Select
value={value.currency || currency}
style={{
width: 80,
margin: '0 8px',
}}
onChange={onCurrencyChange}
>
<Option value="rmb">RMB</Option>
<Option value="dollar">Dollar</Option>
</Select>
</span>
);
};
const Demo = () => {
const onFinish = (values) => {
console.log('Received values from form: ', values);
};
const checkPrice = (_, value) => {
if (value.number > 0) {
return Promise.resolve();
}
return Promise.reject(new Error('Price must be greater than zero!'));
};
return (
<Form
name="customized_form_controls"
layout="inline"
onFinish={onFinish}
initialValues={{
price: {
number: 0,
currency: 'rmb',
},
}}
>
<Form.Item
name="price"
label="Price"
rules={[
{
validator: checkPrice,
},
]}
>
<PriceInput />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
};
ReactDOM.render(<Demo />, mountNode);

Allow Downshift useCombobox to select items not in the list

I'm using useCombobox from Downshift as a use-hook-form component and everything works fine except that I can't get the value when a user types in a value not in the list that is passed into useComboBox.
onSelectedItemChange is never fired unless the value is in the inputItems. This seems like it should be easy but I can't find an answer from the docs.
import React, { memo, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { useCombobox } from 'downshift';
import { FontAwesomeIcon } from '#fortawesome/react-fontawesome';
import { faChevronDown } from '#fortawesome/free-solid-svg-icons';
const comboboxStyles = { display: 'inline-block', marginLeft: '5px' };
let Item = ({ isHighlighted, getItemProps, item, index }) => {
return (
<li
className="auto-complete-list-item"
style={isHighlighted ? { backgroundColor: '#bde4ff' } : {}}
key={`${item}${index}`}
{...getItemProps({ item, index })}
>
{item}
</li>
);
};
Item = memo(Item);
const Autocomplete = ({ items, onChange, isSubmitting }) => {
const [inputItems, setInputItems] = useState(items);
const {
isOpen,
getToggleButtonProps,
getLabelProps,
getMenuProps,
getInputProps,
getComboboxProps,
highlightedIndex,
getItemProps,
inputValue,
reset
} = useCombobox({
items: inputItems,
onSelectedItemChange: ({ inputValue }) => onChange(inputValue),
onInputValueChange: ({ inputValue }) => {
setInputItems(
items.filter(item =>
item.toLowerCase().includes(inputValue.toLowerCase())
)
);
}
});
useEffect(() => {
if (inputValue.length > 0 && isSubmitting) reset();
}, [inputValue, isSubmitting, reset]);
return (
<div className="input-field">
<div style={comboboxStyles} {...getComboboxProps()}>
<input name="autocomplete" {...getInputProps()} />
<button
type="button"
{...getToggleButtonProps()}
aria-label="toggle menu"
>
<FontAwesomeIcon icon={faChevronDown} />
</button>
</div>
<ul {...getMenuProps()} className="auto-complete-list">
{isOpen &&
inputItems.map((item, index) => (
<Item
key={item}
isHighlighted={highlightedIndex === index}
getItemProps={getItemProps}
item={item}
index={index}
/>
))}
</ul>
</div>
);
};
Autocomplete.propTypes = {
list: PropTypes.array
};
export default Autocomplete;
You need to take control of item selection:
const {
isOpen,
selectItem,
getToggleButtonProps,
...
}
Then call selectItem in an onClick handler:
<Item
key={item}
onClick={() => selectItem(item)}
isHighlighted={highlightedIndex === index}
...
/>

how can i get Select option value in antd

I have two select drop downs. If I click on One select option, it should be select and after click, without clicking second dropdown, i am not suppose to click update button. It should be show error.
import React from 'react'
import ReactDOM from 'react-dom'
import { Select, Button } from 'antd';
const Option =Select.Option
class SelectOption extends React.Component{
handleTeacherChange=(value)=>{
console.log(value)
}
handleCourseChange=(value)=>{
console.log(value)
}
render()
{
return(
<div align="center">
<div>
<h2>Shishu Bharathi</h2>
<label>Teacher List :</label>
<Select defaultValue="Select" style={{ width: 120 }} onChange={this.handleTeacherChange}>
<Option value="Vikram">Vikram</Option>
<Option value="Ramesh">Ramesh</Option>
</Select>
<label>Course List :</label>
<Select defaultValue="Select" style={{ width: 120 }} onChange={this.handleCourseChange}>
<Option value="cul1a">CUL1A</Option>
<Option value="cul1b">CUL1B</Option>
</Select>
</div>
<br></br>
<br></br>
<Button >Update</Button>
</div>
)
}
}
export default SelectOption
Use validateFields to check if a field is empty or not.
import React from "react";
import ReactDOM from "react-dom";
import "antd/dist/antd.css";
import "./index.css";
import { Select, Button, Form } from "antd";
const Option = Select.Option;
class SelectOption extends React.Component {
handleTeacherChange = value => {
console.log(value);
// this.props.form.validateField(["Dropdown2"]);
};
handleCourseChange = value => {
console.log(value);
// this.props.form.validateField(["Dropdown1"]);
};
updateClick = () => {
const { getFieldValue, validateFields } = this.props.form;
const dropdown1Value = getFieldValue("Dropdown1");
const dropdown2Value = getFieldValue("Dropdown2");
if (dropdown1Value === "Select" && dropdown2Value !== "Select") {
validateFields(["Dropdown1"]);
}
if (dropdown1Value !== "Select" && dropdown2Value === "Select") {
validateFields(["Dropdown2"]);
}
};
render() {
const { getFieldDecorator } = this.props.form;
return (
<div align="center">
<div>
<h2>Shishu Bharathi</h2>
<label>Teacher List :</label>
<Form.Item>
{getFieldDecorator("Dropdown1", {
initialValue: "Select",
rules: [
{ required: true, message: "Select the teacher" },
{
validator: (rule, value, callback) => {
console.log("value", value);
if (value === "Select") {
callback("Select the teacher");
}
callback();
}
}
]
})(
<Select
style={{ width: 120 }}
onChange={this.handleTeacherChange}
>
<Option value="Vikram">Vikram</Option>
<Option value="Ramesh">Ramesh</Option>
</Select>
)}
</Form.Item>
<label>Course List :</label>
<Form.Item>
{getFieldDecorator("Dropdown2", {
initialValue: "Select",
rules: [
{ required: true, message: "Select the course" },
{
validator: (rule, value, callback) => {
if (value === "Select") {
callback("Select the course");
}
callback();
}
}
]
})(
<Select style={{ width: 120 }} onChange={this.handleCourseChange}>
<Option value="cul1a">CUL1A</Option>
<Option value="cul1b">CUL1B</Option>
</Select>
)}
</Form.Item>
</div>
<br />
<br />
<Button onClick={this.updateClick}>Update</Button>
</div>
);
}
}
const A = Form.create()(SelectOption);
ReactDOM.render(<A />, document.getElementById("container"));
Here is a working demo :CodeSandbox
If you want show error when without clicking second dropdown, you need wrap it using formItem and validate it. Like follow code:
<FormItem
{...formItemLayout}
label={'month'}
>
{this.props.form.getFieldDecorator('loanMonth', {
initialValue: 3,
rules: [{
required: true, message: 'please select month!',
}],
})(
<Select>
{
this.formInitData &&
toJS(this.formInitData).loanMonthList.map((data) => {
return (
<Option value={data.key}>{data.value}</Option>
);
})
}
</Select>,
)}
</FormItem>
using the rules: [{required: true, message: 'please select month!'}] to get it.
Suggesting you see https://ant.design/components/form/

React Child Component Causing Parent Re-Render on Ajax Call

I am using React with React Router on top of Rails to handle the front end of an app that is supposed to return info about whatever gem the user searches for, however, once I hit submit, the child component, who manages it's own state, causes a re-render for the parent component.
EXPECTED RESULT: SavedGems.jsx re-renders
ACTUAL RESULT: Search.jsx re-renders
Here is my code:
StaticPage.jsx
export default class StaticPage extends React.Component {
render() {
return (
<BrowserRouter>
<div style={{display: 'flex', flexDirection: 'row'}}>
<Route exact path='/' render={() => <Search />}/>
<Route path='/favorites' render={() => <Favorites/>} />
</div>
</BrowserRouter>
);
}
}
Search.jsx
export default class Search extends React.Component {
render() {
return (
<div style = {{display: 'flex', flexDirection: 'row'}}>
<Sidebar/>
<div style = {{display: 'flex', flexDirection: 'column'}}>
<Header name = "Search Gems"/>
<Form/>
<SavedGems/>
</div>
</div>
)
}
}
SavedGems.jsx
export default class SavedGems extends React.Component {
constructor() {
super();
this.state = {saved_gems : []};
console.log(this.saved_gems);
}
componentDidMount() {
$.getJSON('/api/v1/saved_gems.json', (response) => { this.setState({ saved_gems: response }) });
}
render() {
var saved_gems= this.state.saved_gems.map((saved_gem) => {
return (
<div key={saved_gem.id}>
<h3>{saved_gem.name}</h3>
<h3>{saved_gem.info}</h3>
<h3>{saved_gem.dependencies}</h3>
</div>
)
});
return (
<div>
{saved_gems}
</div>
)
}
}
_form.jsx
export default class Form extends React.Component {
constructor() {
super();
this._handleClick = this._handleClick.bind(this);
this.state = {formBorderColor : "#5F5F5F"};
}
render() {
return (
<div>
<form>
<label>
<input ref='name'
type="text"
placeholder='Search'
style= {{fontFamily: 'Lato-Regular',
fontSize: 18,
height:89,
width: 780,
paddingLeft: 20,
backgroundColor: 'white',
border: '1px solid',
borderColor: this.state.formBorderColor,
borderRadius: 100}}/>
</label>
<input type="image" src='/assets/magnifying-glass.png'
style={{marginLeft: -70}}
onClick={this._handleClick}/>
</form>
</div>
)
}
_handleClick(event) {
const name = this.refs.name.value;
const info = '';
const dependencies = '';
$.ajax({
url: '/api/v1/saved_gems',
type: 'POST',
data: { saved_gem: { name, info, dependencies } },
success: (saved_gem) => {
this.props.handleSubmit(saved_gem);
this.refs.name.value = '';
this.refs.info.value = '';
this.refs.dependencies.value = '';
},
error: (xhr) => {
this.setState = ({formBorderColor : 'red'}).bind(this);
alert("Sorry! That is not a valid gem");
}
})
}
}

Resources