React UseHookForm set focus when using useFieldArray - reactjs

We have a pretty basic form that has a dynamic number of inputs. The number of inputs is being passed in as a prop to our SerialsForm component. We would like to set focus on the first input when this form renders.
const SerialsForm = ({qty}) => {
const { handleSubmit, register, setFocus, control } = useForm();
const { fields, append, prepend, remove } = useFieldArray({ name: 'serials', control });
// Handle qty changes
useEffect(() => {
const oldVal = fields.length;
if (qty > oldVal) {
for (let i = oldVal; i < qty; i++) {
append({ value: '' });
}
} else {
for (let i = oldVal; i > qty; i--) {
remove(i - 1);
}
}
}, [qty, fields])
const handleFormSubmit = (data) => {
console.log(data)
}
return (
<Form onSubmit={handleSubmit(handleFormSubmit)}>
<div>
{fields.map((item, i) => (
<FormGroup key={item.id}>
<FormControl
placeholder={`Serial #${i + 1}`}
{...register(`serials.${i}.value`)}
/>
</FormGroup>
))}
<Button type='submit'>
<CheckCircle size={20} />
</Button>
</div>
</Form>
);
}
What We Have Tried:
adding { shouldFocus: true } and { shouldFocus: true, focusIndex: 0} as well as { shouldFocus: true, focusName: serials.0.value } as the second argument to append --> Nothing focuses
adding setFocus('serials.0.value'); at the end of useEffect. --> Error
adding autoFocus={i === 0} to the bootstrap FormControl (input) element. --> Nothing focuses
We have tried the same with prepend (we will likely end up using prepend because we want the first input focused)
Any idea of how to accomplish this would be greatly appreciated as we are not seeing a way to set focus outside of using a second argument to append, and that does not seem to be working.

What ended up working was adding a setTimeout to the end of the useEffect call.
useEffect(() => {
// other useEffect code
setTimeout(() => {
if (qty) {
setFocus('serials.0.value');
}
}, 0);
}, [qty, setFocus, ...yourOtherDeps])

Related

How to prepending before icon for text area for React component

I'm trying to implement React version of TextArea which appends "$" every-time I press Return/Enter.
I'm having hard time prepending a sign # or % or dollar every time someone presses enter. How can I go about this?
This is my basic attempt but I'm kind of lost:
const MyComponent = () => {
const [name, setName] = React.useState('');
return (
<div>
<textarea value={name} onChange={(e) => { setName(e.target.value) }} />
</div>
);
}
ReactDOM.render(<MyComponent />, document.getElementById('root'));
Ok so I had a bit of time on my hands and thought this could be a good learning experience. So I present to you: MoneyInputList
import './css/MoneyInputList.css'
import { useState, useEffect } from 'react';
let latestAdded = 'moneyInputList-input-0';
let lastSize = 0;
const MoneyInputList = () => {
const [recordList, setRecordList] = useState({data: ['']});
const handleFormSubmit = (e) => {
e.preventDefault();
if(recordList.data[recordList.data.length-1] !== ''){
setRecordList({data: [...recordList.data, '']})
}
};
useEffect(() => {
if(lastSize !== recordList.data.length)
document.getElementById(latestAdded).focus();
lastSize = recordList.data.length;
}, [recordList]);
return (
<form autoComplete='off' onSubmit={handleFormSubmit}>
<div className="main-container">
{recordList.data.length > 0 &&
recordList.data.map((record, iter) => {
latestAdded = "moneyInputList-input-"+iter;
return (
<div key={"moneyInputList-field-"+iter} className="record-field">
<div className="record-sign">$</div>
<input className="record-input" id={"moneyInputList-input-"+iter} value={recordList.data[iter]} onChange={(e) => {
if(e.target.value === '' && iter !== recordList.data.length-1){
let modifiedData = [];
recordList.data.forEach((e,i) => {
if(i !== iter)
modifiedData.push(e);
});
setRecordList({data: modifiedData});
return;
}
const filteredValue = e.target.value.split('').filter(e=>(e.charCodeAt() >= '0'.charCodeAt() && e.charCodeAt() <= '9'.charCodeAt()));
let formattedValue = [];
filteredValue.forEach((elem, i) => {
if((filteredValue.length - i) % 3 === 0 && i !== 0)
formattedValue.push(',');
formattedValue.push(elem);
});
formattedValue = formattedValue.join('');
e.target.value = formattedValue;
let myData = recordList.data;
myData[iter] = e.target.value;
setRecordList({data: myData});
}} type="text"/>
</div>
)})
}
</div>
<input style={{flex: 0, visibility: 'collapse', height: 0, width: 0, padding: 0, margin: 0}} type="submit"/>
</form>
)
}
export default MoneyInputList;
This component should do what you need it to do. It is not the best code but it works. You can see it working here. Of course you might still need to change some stuff in order for it to fit in your codebase and maybe implement redux, but the general idea is here. You use it by typing whatever number you want pressing enter will create a new line and deleting the content of a line will remove it.
I hope I understood correctly what you are trying to do, here is a super scuffed version of it. You might need to change the code a bit to fit your use case.
import { useState, useEffect } from "react";
export default function App() {
const [textValue, setTextValue] = useState("");
const [displayedTextValue, setDisplayedTextValue] = useState("");
useEffect(() => {
let splitTextValue = textValue.split("\n");
splitTextValue = splitTextValue.map((line, iter) => {
if (iter === splitTextValue.length - 1) return line;
if (line[0] !== "$") return "$ " + line;
return line;
});
setDisplayedTextValue(splitTextValue.join("\n"));
}, [textValue]);
return (
<div>
<textarea
value={displayedTextValue}
onChange={(e) => {
setTextValue(e.target.value);
}}
/>
</div>
);
}
Here is a version working with key event that I think is cleaner when handling thing depending on keys.
Here is the repro on Stackblitz and here is the code :
import React from 'react';
import { render } from 'react-dom';
const App = () => {
const enterKeyCode = 'Enter';
const backspaceKeyCode = 'Backspace';
const [val, setVal] = React.useState('$ ');
const [keyCodeEvent, setKeyCodeEvent] = React.useState();
React.useEffect(() => {
if (keyCodeEvent) {
// Handle numpad 'enter' key
if (keyCodeEvent[0].includes(enterKeyCode)) {
setVal(`${val}\n$ `);
} else if (keyCodeEvent[0] === backspaceKeyCode) {
setVal(`${val.slice(0, -1)}`);
} else {
setVal(`${val}${keyCodeEvent[1]}`);
}
}
}, [keyCodeEvent]);
return (
<div>
{/* Empty onChange to prevent warning in console */}
<textarea onKeyDown={e => setKeyCodeEvent([e.code, e.key])} value={val} onChange={() => {}} />
</div>
);
};
render(<App />, document.getElementById('root'));
I read your comments on Marko Borković 's answer so I handled the backspace but he is totally right when saying you should build a special component for this. It will be way easier to improve and cleaner. You are not safe from some others bugs if you want to add features to your component.

How do I fix an useEffect infinite loop with two properties bound that need to update at the same time?

I have an Autocomplete component:
Autocomplete
function Autocomplete() {
const [ matches, setMatches ] = useState([ 'game' ]);
const [ query, setQuery ] = useState('');
const [ menuState, setMenuState ] = useState(false);
useEffect(() => {
if(query !== ""){
updateQuery()
}
}, [query])
const updateQuery = async () => {
const data = await props.searchQuery(query);
if(data.length > 1 ){
setMenuState(true);
setMatches(data);
} else if (data.length < 1){
setMenuState(false);
setMatches([]);
}
}
return (
<div className={rootClassName(null, [props.className], classStates)} style={styles}>
<div className='Autocomplete__Trigger'>
<input
className='Input'
type='text'
placeholder={props.placeholder}
value={query}
onChange={ e => setQuery(e.target.value) }
onKeyDown={ handleKeyPress }
/>
</div>
<div className='Autocomplete__Menu' role='menu'>
{
//Custom if function
If(matches.length > 0, () => (
<div className='Autocomplete__Content'>
{
//Custom loop
For(matches, (item, index) => (
//loops through matches
))
}
</div>
)).EndIf()
}
</div>
</div>
);
}
export default Autocomplete;
Basically how it works is as follows:
When something is typed into the input a query string is set:
<input
onChange={ e => setQuery(e.target.value) }
/>
The query state is set immediately with useEffect and updateQuery() is called:
useEffect(() => {
//If input is empty
if(query !== ""){
updateQuery()
}
}, [query])
In updateQuery() all of the menu items in the autocomplete are requested from an API and the matches are set to be looped through and dictate whether a menu is open:
const updateQuery = async () => {
const data = await props.searchQuery(query);
if(data.length > 1 ){
setMenuState(true);
setMatches(data);
} else if (data.length < 1){
setMenuState(false);
setMatches([]);
}
}
The problem I'm having is the matches state lags behind, but if I add it to useEffect I get an infinite loop because updateQuery is always being called:
useEffect(() => {
//If input is empty
if(query !== ""){
updateQuery()
}
}, [query, matches])
How can I make it so matches and the query state are updated at the same time without an infinite loop?
You can put the dependent code INSIDE the useEffect with the call to it.
That way, the function isn't re-declared every render. It only changes when the effect is re-run.
This might be missing an await or async modifier somewhere, but to give the idea.
useEffect(() => {
const updateQuery = async () => {
const data = await props.searchQuery(query);
if(data.length > 1 ){
setMenuState(true);
setMatches(data);
} else if (data.length < 1){
setMenuState(false);
setMatches([]);
}
}
//If input is empty
if(query !== ""){
updateQuery()
}
}, [query])

How can I create a real time search populate in real time with react

I am trying to create a real time search and real time populating users.
Here is how my page look like:
Right now my search function is I have to click search icon to make it appear the result so I want to change that to real time search. When I type the name in input it will auto starting appear the user card for me
Here is my code in SearchForMembers.js:
const SearchForMembers = ({ teamId, fetchTeamData }) => {
// State
const [userResults, setUserResults] = useState([]);
const [userSkillResults, setUsersSkillsResults] = useState([]);
const [showResultsList, setShowResultsList] = useState(false);
const [showResultsMsg, toggleResultsMsg] = useState(false);
// Submit search query
const searchForTerm = async (term) => {
try {
if (term !== undefined) {
// Clear results
setUserResults([]);
setUsersSkillsResults([]);
// Perform search
const res = await axios.get(`/api/v1/search/users/${term}`);
// Check response
// Show message if no results were found
if (res.data[0].length === 0 && res.data[1].length === 0) {
toggleResultsMsg(true);
} else {
toggleResultsMsg(false);
// Set users results
setUserResults(res.data[0]);
// Set skills results
setUsersSkillsResults(res.data[1]);
}
}
} catch (err) {
throw new Error(err);
}
};
useEffect(() => {
if (userResults.length > 0 || userSkillResults.length > 0) {
setShowResultsList(true);
} else {
setShowResultsList(false);
}
}, [userResults, userSkillResults]);
return (
<div className="container--search_for_members">
{/* Search bar */}
<SearchBar
linkToPage={false}
searchForTerm={searchForTerm}
placeHolderText="Search for a name, skill or affiliation"
/>
{/* Results list */}
{showResultsList && (
<SearchForMembersResults
usersFound={userResults}
userSkillResults={userSkillResults}
teamId={teamId}
fetchTeamData={fetchTeamData}
/>
)}
{showResultsMsg && (
<>
<p className="text--message-small">No results were found</p>
<AddNewProfile
teamId={teamId}
fetchTeamData={fetchTeamData}
variant="not in table"
/>
</>
)}
</div>
);
};
And here is my code in SearchBar,js :
const SearchBar = ({
linktoPage,
searchForTerm,
placeHolderText = "Search members or teams",
}) => {
// Search state
const [searchTerm, setSearchTerm] = useState("");
const handleChange = (event) => {
// Update state with input text
event.preventDefault();
setSearchTerm(event.target.value);
};
const handleSubmit = async (event) => {
try {
event.preventDefault();
if (linktoPage) {
// Go to page and pass query
goToPage();
} else {
// Don't change pages, but pass term to search method
searchForTerm(searchTerm);
}
} catch (err) {
throw new Error(err);
}
};
const goToPage = () => {
// Go to search page and pass along the search term.
Router.push({
pathname: "/search",
query: { term: `${searchTerm}` },
});
};
return (
<form className="form--search_wrapper" method="GET" onSubmit={handleSubmit}>
<input
className="input input--search_input"
type="search"
name="q"
placeholder={placeHolderText}
aria-label="Search bar"
onInput={handleChange}
pattern="^[a-zA-Z0-9 ]+"
required
/>
<Button className="input input--search" style={{ color: "white", backgroundColor: "#00B790" }} type="submit" >
<SearchRoundedIcon />
</Button>
</form>
);
};
I read about the live search with axios. Here is the link: https://www.digitalocean.com/community/tutorials/react-live-search-with-axios
How can I use .filter in my code ?
You can directly call the body of your handleSubmit function in the handleChange function, you probably want to debounce it so you don't call your api for every keystroke though
const handleChange = (event) => {
// Update state with input text
event.preventDefault();
setSearchTerm(event.target.value);
try {
if (linktoPage) {
// Go to page and pass query
goToPage();
} else {
// Don't change pages, but pass term to search method
searchForTerm(event.target.value);
}
} catch (err) {
throw new Error(err);
}
};

CKEditor updating React state

I am using CKEditor5 with React.
Functionality: User creates multiple questions with answer options.
Simplified version of code:
function QuestionComponent() {
const [questions, setQuestions] = useState([{ question: null, choices: [] }]);
const [index, setIndex] = useState(0);
function previous(e) {
e.preventDefault();
setIndex(index - 1);
}
function next(e) {
e.preventDefault();
// Adding new empty question
if (questions.length <= index + 1) {
setQuestions([...questions, { question: null, choices: [] }]);
}
setIndex(index + 1);
}
function handleQuestionInput(value) {
setQuestions(
questions.map((el, i) => {
if (i === index) {
el.question = value;
}
return el;
})
);
}
function handleChoiceInput(value, choiceIndex) {
setQuestions(
questions.map((el, i) => {
if (i === index) {
el.choices[choiceIndex].text = value;
}
return el;
})
);
}
return (
<>
<CKEditor
editor={Editor}
config={EditorConfig}
data={questions[index].question || ""}
onChange={(event, editor) => {
const data = editor?.getData();
handleQuestionInput(data);}}
/>
<div className="choices-box">
{questions[index].choices.map((el, i) => (
<CKEditor
editor={Editor}
config={EditorConfig}
data={el.text || ""}
onChange={(event, editor) => {
const data = editor?.getData();
handleChoiceInput(data, i);
}}
/>))
}
</div>
<button
type="submit"
label="Previous"
onClick={previous}
/>
<button
type="submit"
label="Next"
onClick={next}
/>
}
Problem: Whenever Next or Previous is clicked current question (questions[index].question) value is cleared and set to "" (or default value from data={questions[index].question || ""}) . However, choices keep their state and work well.
I tested that this code works well if I change CKEditor with simple input
I am also wondering why it is working with choices, but not question
Thanks in advance

Setstate not updating in CustomInput type='checkbox' handler

This is the render method, how i am calling the handler and setting the reactstrap checkbox.
this.state.dishes.map(
(dish, i) => (
<div key={i}>
<CustomInput
type="checkbox"
id={i}
label={<strong>Dish Ready</strong>}
value={dish.ready}
checked={dish.ready}
onClick={e => this.onDishReady(i, e)}
/>
</div>))
The handler for the onClick listener, I've tried with onchange as well but it apears that onchange doesnt do anything, idk why?
onDishReady = (id, e) => {
console.log(e.target.value)
var tempArray = this.state.dishes.map((dish, i) => {
if (i === id) {
var temp = dish;
temp.ready = !e.target.value
return temp
}
else {
return dish
}
})
console.log(tempArray)
this.setState({
dishes: tempArray
});
}
The event.target.value isn't the "toggled" value of an input checkbox, but rather event.target.checked is.
onDishReady = index => e => {
const { checked } = e.target;
this.setState(prevState => {
const newDishes = [...prevState.dishes]; // spread in previous state
newDishes[index].ready = checked; // update index
return { dishes: newDishes };
});
};
The rendered CustomInput reduces to
<CustomInput
checked={dish.ready}
id={i}
label={<strong>DishReady</strong>}
onChange={this.onDishReady(i)}
type="checkbox"
/>
No need to pass in a value prop since it never changes.
Note: Although an onClick handler does appear to work, semantically it isn't quite the correct event, you want the logic to respond to the checked value of the checkbox changing versus a user clicking on its element.
You can do it this way:
this.setState(function (state) {
const dishes = [...state.dishes];
dishes[id].ready = !e.target.value;
return dishes;
});

Resources