Allow Downshift useCombobox to select items not in the list - react-hook-form

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}
...
/>

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!

React: Trying to set selection to drop down option

I setup a component that is basically a drop down and I am trying to figure out how to set it to where when I submit the form....its set on that one option. When I submit it now, it sends all the options to the backend instead of just the one I selected.
Here is my Category component
import React, { Component } from 'react'
class Categories extends Component{
handleCatChange = (event) => {
this.setState({category: event.target.value}) <------this should set the state to whatever is selected
}
render(){
let categories = this.props.category
let value = this.props.value
let optionItems = categories.map((cat,index) => {
return <option key={index} value={value}>{cat.category}</option>
})
return (
<div>
<select onchange={this.handleCatChange} value={this.props.category}>
{this.props.category ? optionItems : <p>Loading....</p>}
</select>
</div>
)
}
}
export default Categories
And here is RecipeInput Component with form
import React, { Component } from 'react'
import Categories from './Categories.js'
class RecipeInput extends Component{
constructor(props){
super(props)
this.state = {
category: [],
name:'',
ingredients: '',
chef_name: '',
origin: ''
}
}
componentDidMount(){
let initialCats = [];
const BASE_URL = `http://localhost:10524`
const CATEGORIES_URL =`${BASE_URL}/categories`
fetch(CATEGORIES_URL)
.then(resp => resp.json())
.then(data => {
initialCats = data.map((category) => {
return category
})
this.setState({
category: initialCats
})
});
}
handleSubmit = (event) =>{
event.preventDefault();
this.props.postRecipes(this.state)
this.setState({
name:'',
ingredients: '',
chef_name: '',
origin: ''
})
}
render(){
return(
<div>
<form onSubmit={this.handleSubmit}>
<Categories category={this.state.category} value={this.state.category}/>
<div>
<label for='name'>Recipe Name:</label>
<input type='text' value={this.state.name} onChange={this.handleNameChange} />
</div>
<div>
<label for='name'>Country Origin:</label>
<input type='text' value={this.state.origin} onChange={this.handleOriginChange} />
</div>
<div>
<label for='name'>Chef Name:</label>
<input type='text' value={this.state.chef_name} onChange={this.handleChefChange} />
</div>
<div>
<label for='name'>Ingredients:</label>
<textarea value={this.state.ingredients} onChange={this.handleIngChange} />
</div>
<input value='submit' type='submit'/>
</form>
</div>
)
}
}
export default RecipeInput
And here is the error that is produced on submission(Its Rails btw)
I tired a few ways but haven't quite wrapped my head around using a component as a dropdown. What do I need to do?
Here is my backend code that creates the record on the api
def create
recipe = Recipe.create(recipe_params)
if recipe.save
render json: recipe
else
render json: { error: "Couldn't save" }
end
end
private
def recipe_params
params.permit(:category_id,:name,:ingredients,:chef_name,:origin,category_attribute:[:category])
end
Also my postRecipe function
export const postRecipes = (recipe)=>{
const BASE_URL = `http://localhost:10524`
const RECIPES_URL =`${BASE_URL}/recipes`
const config = {
method: "POST",
body:JSON.stringify(recipe),
headers: {
"Accept": "application/json",
"Content-type": "application/json"
}
}
//category field
return(dispatch)=>{
fetch(RECIPES_URL,config)
.then(response => response.json())
.then(resp => {
dispatch({
type: 'Add_Recipe',
payload:{
// category:resp.category,
name: resp.name,
ingredients: resp.ingredients,
chef_name: resp.chef_name,
origin: resp.origin,
categoryId: resp.categoryId
}
})
})
//.then(response => <Recipe />)
.catch((error) => console.log.error(error))
}
}
Code Edit due to change in question:
Access selectedValue while sending to the server
class Categories extends Component {
render() {
...
let optionItems = categories.map((cat, index) => {
return (
<option key={index} value={index}>
{cat.category}
</option>
);
});
...
}
}
class RecipeInput extends Component{
constructor(props){
super(props)
this.state = {
category: [],
name:'',
ingredients: '',
chef_name: '',
origin: ''
selectedValue: {}
}
}
handleSubmit(id){
this.setState({
selectedValue: this.state.category[id]
)}
}
...
}
You're passing onChange and value from Input Component but you're not using them in Categories Component.
Add onChange and value property to tag.
here is reference
import "./styles.css";
import React, { Component } from "react";
class Categories extends Component {
render() {
let categories = this.props.category;
let onChange = this.props.onChange;
let optionItems = categories.map((cat, index) => {
return (
<option key={index} value={cat.category}>
{cat.category}
</option>
);
});
return (
<div>
<select onChange={(e) => onChange(e.target.value)}>
{this.props.category.length ? optionItems : null}
</select>
</div>
);
}
}
export default function App() {
const onChange = (value) => {
console.log(value);
};
return (
<Categories
onChange={onChange}
category={[{ category: "1st" }, { category: "2nd" }]}
/>
);
}
I've updated the code.
If you need to try it online you can refer my Sandbox
https://codesandbox.io/s/stackoverflow-qno-65730813-j32ce

Using useComboBox from DownShift with 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())
)
);
}
});

A form is not submitted when the form is used as a title prop of TreeNode (andt design)

In this code we use Button with Modal in TreeNode. There is the form in Modal and Ok Button is bound to form with using form ('myForm') attribute. But form is not submitted with button. It looks like submition doesn't work at all.
According to my investiagation it is related with Tree and TreeNode components.
The example of my code:
import React, { useState, useMemo } from "react";
import ReactDOM from "react-dom";
import "antd/dist/antd.css";
import "./index.css";
import { useForm, Controller } from "react-hook-form";
import { Modal, Button, Input, Tree } from "antd";
const { TreeNode } = Tree;
const renderForm = (handleSubmit, onSubmit, control) => (
<form id="myForm" onSubmit={handleSubmit(onSubmit)}>
<Controller
as={<Input placeholder="Type" />}
control={control}
name="title"
/>
</form>
);
const Comp = () => {
const { handleSubmit, control } = useForm();
const [visible, setVisible] = useState(false);
const showModal = () => {
setVisible(true);
};
const handleOk = e => {
setVisible(false);
};
const handleCancel = e => {
setVisible(false);
};
const onSubmit = ({ title }) => {
console.log(title);
};
return (
<div>
<Button type="primary" onClick={showModal}>
Open Modal
</Button>
<Modal
title="Basic Modal"
visible={visible}
onOk={handleOk}
onCancel={handleCancel}
okButtonProps={{
htmlType: "submit",
form: "myForm"
}}
>
{useMemo(() => renderForm(handleSubmit, onSubmit, control), [
control,
handleSubmit
])}
</Modal>
</div>
);
};
const App = () => (
<Tree>
<TreeNode key="ert" title={<Comp />} />
</Tree>
);
ReactDOM.render(<App />, document.getElementById("container"));

Cannot read property id of undefined, error found in the map function (react-rails)

I'm trying to make my submit button add on to the list of items, my seed data in rails gets rendered fine but when I try to add a new item when clicking submit, I get this error Cannot read property id of undefined which is found at the map function. The post request works fine, and only after I refresh the page, the item I add gets rendered on the page. Any help would be appreciated!
class TodoList extends React.Component {
render() {
const {todos} = this.props
var todoItems = todos.map(title => <TodoItem key={title.id} title={title}/>) //here
return (
<ListGroup className="my-2">
<h2 className="text-center">Items</h2>
{todoItems}
</ListGroup>
)
}
}
export default TodoList
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
todos: [],
stuff: ''
}
this.handleChange = this.handleChange.bind(this)
this.handleSubmit = this.handleSubmit.bind(this)
this.addNewTodo = this.addNewTodo.bind(this)
}
handleChange = event => {
this.setState({
stuff:event.target.value
})
}
handleSubmit = event => {
event.preventDefault()
let body = JSON.stringify({todo: {item: this.state.stuff} })
fetch('http://localhost:3000/api/v1/todos', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: body,
})
.then(response => {response.json()})
.then(todo => {this.addNewTodo(todo)})
}
addNewTodo(todo){
this.setState({
todos: this.state.todos.concat(todo)
})
}
render() {
return (
<div className="container">
<div className="row">
<div className="col-10 mx-auto mt-4">
<h1 className="text-center">Todo List</h1>
<TodoInput
stuff={this.state.stuff}
handleChange={this.handleChange}
handleSubmit={this.handleSubmit}
/>
<TodoList
todos={this.state.todos}
/>
</div>
</div>
</div>
)
}
}
when your component is renders first "todos" won't have any value, it will be undefined. try below code
class TodoList extends React.Component {
render() {
const { todos } = this.props;
if (todos) {
return (
<ListGroup className="my-2">
<h2 className="text-center">Items</h2>
{todos.map(title => (
<TodoItem key={title.id} title={title} />
))}
</ListGroup>
);
}
return ""; } }
export default TodoList;

Resources