Hi everyone I am working on a project which I need to handle focus elements using the arrow keys.
Right now everything is ok and the functionality is working by click.
Unfortunately, when I am using the arrow keys the process work for the tr elements but it is not working for the input elements.
this is the app component:
import React from "react";
import "./App.css";
import RenderRowItem from "./RenderRow";
const keysMapNum={
LEFT:37,
UP:38,
RIGHT:39,
DOWN:40,
ENTER:13,
TAB:9,
CTRL:17
};
class App extends React.Component {
state = {
data: [],
formData: [],
selectedInput:[0,0]
};
componentDidMount() {
fetch("https://jsonplaceholder.typicode.com/users")
.then(response => response.json())
.then(json => {
const data = json.map(({ id, name, username, email }) => {
return {
id,
name,
username,
email
};
});
const mappedData=[];
data.forEach(item=>{
const rowRef=React.createRef();
const rowData=[];
Object.keys(item).forEach(innerItem=>{
let optimizedData = {
filedName:innerItem,
value:item[innerItem],
hint:false,
ref:React.createRef(),
editable:true,
visible:true,
focused:false
};
rowData.push(optimizedData);
});
const mappedDataItem = {
rowRef,
rowData,
focused:false
};
mappedData.push(mappedDataItem);
});
console.log(mappedData);
this.setState({
data:mappedData
},()=>{
console.log(this.state.data)
});
});
document.addEventListener("keydown",(e)=>{
// e.preventDefault();
const newState={...this.state};
console.log("doc",newState);
let [rowNum,columnNum]=[...newState.selectedInput];
const rowLength=newState.data.length;
const columnLength=newState.data[rowNum]["rowData"].length;
if(e.keyCode===keysMapNum.DOWN){
rowNum++;
if(rowNum>=rowLength){
rowNum=0;
}
newState.selectedInput[0]=rowNum;
this.selectedColumn(rowNum,columnNum);
this.setState({
...newState
},()=>{
this.selectedRow(rowNum,newState.data[rowNum].rowRef);
});
}else if(e.keyCode===keysMapNum.UP){
rowNum--;
if(rowNum<0){
rowNum=rowLength - 1;
}
newState.selectedInput[0]=rowNum;
this.selectedColumn(rowNum,columnNum)
this.setState({
...newState
},()=>{
this.selectedRow(rowNum,newState.data[rowNum].rowRef);
})
}else if(e.keyCode===keysMapNum.LEFT){
}else if(e.keyCode===keysMapNum.RIGHT){
}
this.setState({
...newState
})
});
}
selectedRow=(rowNum,ref)=>{
const newState={...this.state};
newState.data.forEach(item=>{
if(item.rowRef.current!==null){
item.rowRef.current.style.backgroundColor="";
item.rowRef.current.style.color="#333";
}
});
if(ref.current!==null){
ref.current.style.backgroundColor="gray";
ref.current.style.color="white";
}
// if(newState.data[rowNum]["rowData"][0].ref.current!==null){newState.data[rowNum]["rowData"][0].ref.current.focus();
// newState.data[rowNum]["rowData"][0].ref.current.select();}
};
selectedColumn=(rowNum,columnNum)=>{
const newState={...this.state};
let ref=newState.data[rowNum]["rowData"][columnNum].ref;
newState.data[rowNum]["rowData"][columnNum].ref.current.style.backgroundColor="tomato";
if(ref.current!==null){
ref.current.focus();
ref.current.select();
}
};
handleInputClick=(rowNum,columnNum)=>{
const newState={...this.state};
newState.selectedInput=[rowNum,columnNum];
newState.data[rowNum]["focused"]=true;
newState.data[rowNum]["rowData"][columnNum]["focused"]=true;
this.setState({
...newState
},()=>{
this.selectedColumn(rowNum,columnNum);
})
};
setRef=(ref)=> {
this.name = ref;
};
render() {
const { data } = this.state;
// { id, name, username, email }
return (
<div className="App">
<table>
<tbody>
{data.map((datum, rowNum) => {
return (
<tr key={rowNum} ref={datum.rowRef} onClick={()=>this.selectedRow(rowNum,datum.rowRef)}>
{datum.rowData.map((item,columnNum) => (
<RenderRowItem key={Math.random()} ref={item.ref} handleInputClick={this.handleInputClick} rowNum={rowNum} columnNum={columnNum} value={item.value} fieldName={item.filedName}/>
))}
</tr>
);
})}
</tbody>
</table>
</div>
);
}
}
export default App;
and this is the RowItem component:
import React from "react";
const RenderRow=React.forwardRef((props,ref)=>(
<>
<td onClick={()=>props.handleInputClick(props.rowNum,props.columnNum)} >{props.filedName!=="id"?<input type="text" value={props.value} ref={ref} />:props.value}</td>
</>
));
export default RenderRow;
this structure is just testing and I know that need to be optimized.
Please only use up and down arrow keys.
Here is the SandBox link
Related
components/App.js
import React from 'react';
import { connect } from 'react-redux'
import { add_task } from '../actions/index';
class App extends React.Component {
state = {
text: '',
time: ''
}
render_tasks = () => {
const { tasks } = this.props
return (
<table>
<tbody>
{tasks.map(task => {
return (
<tr key={task.id}>
<td>{task.text}</td>
<td>{task.time}</td>
</tr>
)
})
}
</tbody>
</table>
)
}
render() {
console.log('App props', this.props)
return (
<div className="App">
<input type='text'
onChange={(e) => this.setState({
text: e.target.value
})} />
<input type='time'
onChange={(e) => this.setState({
time: e.target.value
})} />
<button
onClick={() => this.props.add_task(
this.state.text, this.state.time)}>
Add
</button>
{this.render_tasks()}
</div>
);
}
}
function mapStateToProps(state) {
return {
tasks: state
}
}
function mapDispatchToProps(dispatch) {
return {
add_task : () => dispatch(add_task())
}
}
export default connect(mapStateToProps, mapDispatchToProps)(App);
actions/index.js
import { ADD_TASK } from './../types';
export const add_task = (text, time) => {
const action = {
type: ADD_TASK,
text,
time
}
console.log('Add action', action)
return action
}
reducers/index.js
import { ADD_TASK } from './../types';
const tasks = (state=[], action) => {
let tasks = [];
if (action.type === ADD_TASK) {
tasks = [...state, {
text: action.text,
time: action.time,
id: Math.random()
}]
console.log('Data from reducer', tasks)
return tasks
} else {
return state
}
}
export default tasks;
When I click the button I expect to get a table with the information I entered in the input fields, but nothing appears, I tried replacing the table part in render_tasks function in App.js with an unordered list and the map returns a list item including 2 spans one for the text and the other for the time but all I got is the dot of the list item!
In
add_task : () => dispatch(add_task())
You don't pass any arguments to add_task().
You can explicitly define the arguments:
add_task : (text, time) => dispatch(add_task(text, time))
In the project I watch, they work with class component, but I want to do these operations with functional component using hooks. How can you help me guys? I tried many times but couldn't do this translation. I'm still trying
My code (imported data is "ingredients"):
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
ingredients: [],
totalPrice: 0
}
this.addIngredients = this.addIngredients.bind(this)
this.removeIngredients = this.removeIngredients.bind(this)
this.calculateTotal = this.calculateTotal.bind(this)
}
addIngredients(product) {
this.setState({
ingredients: [...this.state.ingredients].concat([
{ ...product, displayId: Math.random() }
])
})
}
removeIngredients(product) {
const selectedProduct = this.state.ingredients.find((ingredient) => {
return ingredient.name === product.name
})
const targetId = selectedProduct.displayId
this.setState({
ingredients: this.state.ingredients.filter((ingredient) => {
return ingredient.displayId !== targetId
})
})
}
calculateTotal() {
let total = 4
this.state.ingredients.forEach((item) => {
total += item.price
})
return total.toFixed(2)
}
render() {
return (
<div>
<Hamburger ingredients={this.state.ingredients} />
<TotalPrice total={this.calculateTotal} />
<ItemList
items={ingrediends}
addIngredients={this.addIngredients}
removeIngredients={this.removeIngredients}
selectedIngredients={this.state.ingredients}
/>
</div>
)
}
}
export default App
Navarrro I hope this helps you! I couldn't test it but Is a good started for you, I use ES6 syntax...
import React, { useState } from 'react';
import { Hamburger, TotalPrice, ItemList } from './SuperComponents.jsx';
const App = () => {
const [ingredients, setIngredients] = useState([]);
// You are not using this state
// const [totalPrice, setTotalPrice] = useState(0);
const addIngredients = (product) => {
setIngredients([...ingredients, { ...product, displayId: Math.random() }]);
};
const removeIngredients = (product) => {
const selectedProduct = ingredients.find(
(ingredient) => ingredient.name === product.name
);
const { targetId } = selectedProduct;
setIngredients(
ingredients.filter((ingredient) => ingredient.displayId !== targetId)
);
};
const calculateTotal = () => {
let total = 4;
ingredients.forEach((item) => (total += item.price));
return total.toFixed(2);
};
return (
<>
<Hamburger ingredients={ingredients} />
<TotalPrice total={() => calculateTotal()} />
<ItemList
items={ingredients}
addIngredients={(i) => addIngredients(i)}
removeIngredients={(i) => removeIngredients(i)}
selectedIngredients={ingredients}
/>
</>
);
};
export default App;
I am facing such problem, i got my array of records fetched from an API, mapped it into single elements and outputting them as single components. I have function which changes state of parent Component, passes value to child component and child component should hide/show div content after button is clicked.
Of course. It is working, but partially - my all divs are being hidden/shown. I have set specific key to each child component but it doesn't work.
App.js
import React, { Component } from 'react';
import './App.css';
import axios from 'axios';
import countries from '../../countriesList';
import CitySearchForm from './CitySearchForm/CitySearchForm';
import CityOutput from './CityOutput/CityOutput';
import ErrorMessage from './ErrorMessage/ErrorMessage';
class App extends Component {
state = {
country: '',
error: false,
cities: [],
infoMessage: '',
visible: false
}
getCities = (e) => {
e.preventDefault();
const countryName = e.target.elements.country.value.charAt(0).toUpperCase() + e.target.elements.country.value.slice(1);
const countryUrl = 'https://api.openaq.org/v1/countries';
const wikiUrl ='https://en.wikipedia.org/w/api.php?action=query&prop=extracts&exintro&explaintext&format=json&category=city&redirects&origin=*&titles=';
const allowedCountries = new RegExp(/spain|germany|poland|france/, 'i');
if (allowedCountries.test(countryName)) {
axios
.get(countryUrl)
.then( response => {
const country = response.data.results.find(el => el.name === countryName);
return axios.get(`https://api.openaq.org/v1/cities?country=${country.code}&order_by=count&sort=desc&limit=10`)
})
.then( response => {
const cities = response.data.results.map(record => {
return { name: record.city };
});
cities.forEach(city => {
axios
.get(wikiUrl + city.name)
.then( response => {
let id;
for (let key in response.data.query.pages) {
id = key;
}
const description = response.data.query.pages[id].extract;
this.setState(prevState => ({
cities: [...prevState.cities, {city: `${city.name}`, description}],
infoMessage: prevState.infoMessage = ''
}))
})
})
})
.catch(error => {
console.log('oopsie, something went wrong', error)
})
} else {
this.setState(prevState => ({
infoMessage: prevState.infoMessage = 'This is demo version of our application and is working only for Spain, Poland, Germany and France',
cities: [...prevState.cities = []]
}))
}
}
descriptionTogglerHandler = () => {
this.setState((prevState) => {
return { visible: !prevState.visible};
});
};
render () {
return (
<div className="App">
<ErrorMessage error={this.state.infoMessage}/>
<div className="form-wrapper">
<CitySearchForm getCities={this.getCities} getInformation={this.getInformation} countries={countries}/>
</div>
{this.state.cities.map(({ city, description }) => (
<CityOutput
key={city}
city={city}
description={description}
show={this.state.visible}
descriptionToggler={this.descriptionTogglerHandler} />
))}
</div>
);
}
}
export default App;
CityOutput.js
import React, { Component } from 'react';
import './CityOutput.css';
class CityOutput extends Component {
render() {
const { city, descriptionToggler, description, show } = this.props;
let descriptionClasses = 'output-record description'
if (show) {
descriptionClasses = 'output-record description open';
}
return (
<div className="output">
<div className="output-record"><b>City:</b> {city}</div>
<button onClick={descriptionToggler}>Read more</button>
<div className={descriptionClasses}>{description}</div>
</div>
)
}
};
export default CityOutput;
Put the visible key and the toggle function in the CityOutput instead of having it in the parent
import React, { Component } from "react";
import "./CityOutput.css";
class CityOutput extends Component {
state = {
visible: true
};
descriptionTogglerHandler = () => {
this.setState({ visible: !this.state.visible });
};
render() {
const { city, description } = this.props;
let descriptionClasses = "output-record description";
if (this.state.visible) {
descriptionClasses = "output-record description open";
}
return (
<div className="output">
<div className="output-record">
<b>City:</b> {city}
</div>
<button onClick={() => this.descriptionTogglerHandler()}>Read more</button>
<div className={descriptionClasses}>{description}</div>
</div>
);
}
}
export default CityOutput;
There are two ways of how I would approach this,
The first one is setting in your state a key property and check and compare that key with the child keys like:
state = {
country: '',
error: false,
cities: [],
infoMessage: '',
visible: false.
currKey: 0
}
descriptionTogglerHandler = (key) => {
this.setState((prevState) => {
return { currKey: key, visible: !prevState.visible};
});
};
// then in your child component
class CityOutput extends Component {
render() {
const { city, descriptionToggler, description, show, currKey, elKey } = this.props;
let descriptionClasses = 'output-record description'
if (show && elKey === currKey) {
descriptionClasses = 'output-record description open';
}
return (
<div className="output">
<div className="output-record"><b>City:</b> {city}</div>
<button onClick={() => descriptionToggler(elKey)}>Read more</button>
<div className={descriptionClasses}>{description}</div>
</div>
)
}
};
The other way is to set an isolated state for every child component
class CityOutput extends Component {
constructor(props) {
this.state = {
show: false
}
}
function descriptionToggler() {
const {show} = this.state;
this.setState({
show: !show
})
}
render() {
const { city, descriptionToggler, description } = this.props;
let descriptionClasses = 'output-record description'
if (this.state.show) {
descriptionClasses = 'output-record description open';
}
return (
<div className="output">
<div className="output-record"><b>City:</b> {city}</div>
<button onClick={descriptionToggler}>Read more</button>
<div className={descriptionClasses}>{description}</div>
</div>
)
}
};
I hope this helps ;)
I am trying to setState to an event category for display inside of handleCategoryChange. The categories are rendered from the getCategories fetch point. I need to send a different value to the action fetch call in createEventHandler. The set state only happens once though and omits the second to send the first value of the state. Is there a work-around for this? or is this a limitation of react?
//... styles and imports
class NewEvent extends Component {
constructor(props) {
super(props);
this.state = {
event: {
category: ''
}
};
this.createEventHandler = this.createEventHandler.bind(this);
this.handleCategoryChange = this.handleCategoryChange.bind(this);
}
handleCategoryChange(evnt) {
this.setState({
event: {
...this.state.event,
category: evnt.target.value
}
});
}
componentWillMount() {
this.props.getCategories();
}
renderStepOne() {
const { event } = this.state;
const { categories } = this.props;
return (
<div style={styles.flexColumn}>
<Typography variant="title">Event</Typography>
<Select
value={event.category}
onChange={this.handleCategoryChange}
error={categoryError.length > 0}
>
{categories.map(category => (
<MenuItem key={category.id} value={category.name}>
{category.name}
</MenuItem>
))}
</Select>
</div>
);
}
createEventHandler() {
const { event } = this.state;
if (!error) {
let categoryId = this.props.categories.filter(e => {
if (e.name === event.category) {
return e;
}
});
categoryId = categoryId[0].id;
this.setState({
event: {
...event,
category: categoryId
}
});
this.props.createEvent(event, this.props.history);
}
}
render() {
const { step } = this.state;
const { isFetching, user, categories } = this.props;
return (
<ViewContainer title="New Event" isFetching={isFetching}>
<Paper style={styles.paper}>
<div style={styles.body}>{this.renderStepOne()}</div>
<MobileStepper
type="dots"
steps={0}
position="static"
nextButton={
<Button
variant="raised"
color="primary"
onClick={this.createEventHandler}
disabled={isFetching}
>
Submit
<KeyboardArrowRight />
</Button>
}
/>
</Paper>
</ViewContainer>
);
}
}
const mapStateToProps = state => ({
categories: state.events.categories
});
const mapDispatchToProps = dispatch => ({
createEvent: (event, history) => dispatch(createEvent(event, history)),
getCategories: () => dispatch(getCategories())
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(withRouter(NewEvent));
You could try using functional setState like so:
this.setState(() => ({
event: {
...this.state.event,
category: evnt.target.value
})
});
So that everything involving a setting of state happens together.
I'm using the Star Wars API to build a React JS project. The aim of my app is to be able to search for characters.
Here is my code for the search component in the my app.
At the moment I'm able to retrieve data the API and show the information on the page but I can't work out how to show this information when it's searched for.
Any ideas?
import React, { Component } from 'react';
class Search extends Component {
constructor(props){
super(props)
this.state = {
query:'',
peoples: [],
}
}
onChange (e){
this.setState({
query: e.target.value
})
if(this.state.query && this.state.query.length > 1) {
if(this.state.query.length % 2 === 0){
this.componentDidMount()
}
}
}
componentDidMount(){
const url = "https://swapi.co/api/people/";
fetch (url,{
method:'GET'
}).then(results => {
return results.json();
}).then(data => {
let peoples = data.results.map((people) => {
return(
<ul key={people.name}>
<li>{people.name}</li>
</ul>
)
})
this.setState({peoples: peoples});
console.log("state", peoples)
})
}
render() {
return (
<form>
<input
type="text"
className="search-box"
placeholder="Search for..."
onChange={this.onChange.bind(this)}
/>
{this.state.peoples}
</form>
)
}
}
export default Search
You could put your fetch in a separate function instead of in componentDidMount and call that when the component mounts and when your query changes.
Since you might be creating multiple requests if the user types quickly, you could use a debounce to only send one request, or use something that verifies that you always use the result of the latest request, like e.g. a token.
Example
class Search extends Component {
token = null;
state = {
query: "",
people: []
};
onChange = e => {
const { value } = e.target;
this.setState({
query: value
});
this.search(value);
};
search = query => {
const url = `https://swapi.co/api/people?search=${query}`;
const token = {};
this.token = token;
fetch(url)
.then(results => results.json())
.then(data => {
if (this.token === token) {
this.setState({ people: data.results });
}
});
};
componentDidMount() {
this.search("");
}
render() {
return (
<form>
<input
type="text"
className="search-box"
placeholder="Search for..."
onChange={this.onChange}
/>
{this.state.people.map(person => (
<ul key={person.name}>
<li>{person.name}</li>
</ul>
))}
</form>
);
}
}
You have to define it in diff function to manage easy.
import React, { Component } from 'react';
class Search extends Component {
constructor(props) {
super(props)
this.state = {
query: null,
peoples: [],
}
}
componentDidMount() {
this.serachPeople(this.state.query);
}
onChange(e) {
this.setState({ query: e.target.value }, () => {
if (this.state.query && this.state.query.length > 1) {
if (this.state.query.length % 2 === 0) {
this.serachPeople(this.state.query);
}
} else {
this.serachPeople(this.state.query);
}
})
}
serachPeople(query) {
const url = "https://swapi.co/api/people/";
if (query) {
// if get value ion query so filter the data based on the query.
fetch(url, {
method: 'GET'
}).then(results => {
return results.json();
}).then(data => {
let peoples = data.results.filter(people => people.name === query).map((people) => {
return (
<ul key={people.name}>
<li>{people.name}</li>
</ul>
)
})
this.setState({ peoples: peoples });
console.log("state", peoples)
})
} else {
fetch(url, {
method: 'GET'
}).then(results => {
return results.json();
}).then(data => {
let peoples = data.results.map((people) => {
return (
<ul key={people.name}>
<li>{people.name}</li>
</ul>
)
})
this.setState({ peoples: peoples });
console.log("state", peoples)
})
}
}
render() {
return (
<form>
<input
type="text"
className="search-box"
placeholder="Search for..."
onChange={this.onChange.bind(this)}
/>
{this.state.peoples}
</form>
)
}
}
export default Search;
I hope this will help for u. Let me know if u have any query.