I am using React JS and I have a text field which is supposed to change its content as the user clicks on different UI components. I also want to be able to edit the text in that text field (and later I would like to send that text to the UI component, but that is another story). So far I got this code
import React, { useContext } from 'react'
import './ContextualMenu.css'
import { EditorContext } from '../../EditorBase'
const ContextualMenu = props => {
const editorContext = useContext(EditorContext)
const handleUpdates = (event) => {
console.log(event.target.value)
}
const displayNodeAttr = () => {
return (
<>
<div className="menu-title">{editorContext.selectedNode.nodeType}</div>
<div>
<div className="menu-item">
<div className="menu-item-label">Name</div>
<div className="menu-item-value">
<input
className="menu-item-input"
type="text"
value={editorContext.selectedNode.nodeAttr.name}
onChange={handleUpdates}
/>
</div>
</div>
</div>
</>
)
}
return (
<div id="c-contextual-menu">
{editorContext.selectedNode.nodeAttr && displayNodeAttr()}
</div>
)
}
export default ContextualMenu
This makes the text always return to the original text that was set when the user clicked on the component. If i replace line 21 (value={editorContext.selectedNode.nodeAttr.name}) with placeholder={editorContext.selectedNode.nodeAttr.name} then the hint always shows the correct text as the user click on UI components but it shows it as a hint and i would like to have it as text.
It seems to me that the input text field detects a change (has a listener on the change event or something like that) and it immediately reverts the text to the original text, which makes it basically uneditable. Any ideas?
Update:
After the answers by #alireza and #Juviro I changed the code due to the fact that initially the selected node is null and as the user selects the node then it becomes not null. So the code now looks like this (it is just the relevant part):
const ContextualMenu = props => {
const editorContext = useContext(EditorContext)
const val = editorContext.selectedNode && editorContext.selectedNode.nodeAttr ? editorContext.selectedNode.nodeAttr.name : ''
const [value, setValue] = useState(val)
const handleUpdates = (event) => {
setValue(event.target.value)
console.log(event.target.value)
}
const displayNodeAttr = () => {
return (
<>
<div className="menu-title">{editorContext.selectedNode.nodeType}</div>
<div>
<div className="menu-item">
<div className="menu-item-label">Name</div>
<div className="menu-item-value">
<input
className="menu-item-input"
type="text"
value={value}
onChange={handleUpdates}
/>
</div>
</div>
</div>
</>
)
}
return (
<div id="c-contextual-menu">
{editorContext.selectedNode.nodeAttr && displayNodeAttr()}
</div>
)
}
The problem now is that the input field is never set to any value when the user clicks on the UI components (nodes). It is as if the value is set on page load and then never updated as the user selects components (nodes). If now I use val instead of value like this: value={val} then the input field is updated correctly but then i get back to the old problem of not being able to edit its content.
You can use the effect hook to call the setValue function when the value of val changes
import React, { useEffect, useState, useContext } from 'react'
const ContextualMenu = props => {
const editorContext = useContext(EditorContext)
const val = editorContext.selectedNode && editorContext.selectedNode.nodeAttr ? editorContext.selectedNode.nodeAttr.name : ''
const [value, setValue] = useState(val)
useEffect(() => {
setValue(val)
}, [val])
const handleUpdates = (event) => {
setValue(event.target.value)
console.log(event.target.value)
}
const displayNodeAttr = () => {
return (
<>
<div className="menu-title">{editorContext.selectedNode.nodeType}</div>
<div>
<div className="menu-item">
<div className="menu-item-label">Name</div>
<div className="menu-item-value">
<input
className="menu-item-input"
type="text"
value={value}
onChange={handleUpdates}
/>
</div>
</div>
</div>
</>
)
}
return (
<div id="c-contextual-menu">
{editorContext.selectedNode.nodeAttr && displayNodeAttr()}
</div>
)
}
The solution from #alireza looks good, just replace
useState(event.target.value)
with
setValue(event.target.value)
This component always shows the same value as editorContext.selectedNode.nodeAttr.name
you should use state in the component to handle it's value.
import React, { useContext, useState } from 'react'
import './ContextualMenu.css'
import { EditorContext } from '../../EditorBase'
const ContextualMenu = props => {
const editorContext = useContext(EditorContext)
const [value, setValue] = useState(editorContext.selectedNode.nodeAttr.name)
const handleUpdates = (event) => {
setValue(event.target.value)
console.log(event.target.value)
}
const displayNodeAttr = () => {
return (
<>
<div className="menu-title">{editorContext.selectedNode.nodeType}</div>
<div>
<div className="menu-item">
<div className="menu-item-label">Name</div>
<div className="menu-item-value">
<input
className="menu-item-input"
type="text"
value={value}
onChange={handleUpdates}
/>
</div>
</div>
</div>
</>
)
}
return (
<div id="c-contextual-menu">
{editorContext.selectedNode.nodeAttr && displayNodeAttr()}
</div>
)
}
I have not tested but it should work.
update
To update the value based on the values coming from props, you should use useEffect.
import React, { useContext, useEffect, useState } from 'react'
import './ContextualMenu.css'
import { EditorContext } from '../../EditorBase'
const ContextualMenu = props => {
const editorContext = useContext(EditorContext)
const val = editorContext.selectedNode && editorContext.selectedNode.nodeAttr.name ? editorContext.selectedNode.nodeAttr.name : ''
const [value, setValue] = useState(val)
const handleUpdates = (event) => {
setValue(event.target.value)
console.log(event.target.value)
}
useEffect(()=>{
if( val !== value) // Prevent redundant updates
setValue(val)
})
const displayNodeAttr = () => {
return (
<>
<div className="menu-title">{editorContext.selectedNode.nodeType}</div>
<div>
<div className="menu-item">
<div className="menu-item-label">Name</div>
<div className="menu-item-value">
<input
className="menu-item-input"
type="text"
value={value}
onChange={handleUpdates}
/>
</div>
</div>
</div>
</>
)
}
return (
<div id="c-contextual-menu">
{editorContext.selectedNode.nodeAttr && displayNodeAttr()}
</div>
)
Related
I have the following React component:
import React from "react";
import { useState, useEffect } from "react";
import { TailSpin } from "react-loader-spinner";
function Pokemon({ name, url }) {
const [data, setData] = useState(null);
useEffect(() => {
fetch(url)
.then((r) => r.json())
.then(setData);
}, [url]);
const onClickButtonChange = () => {
let cardMore = document.querySelector(".card_more");
let cardMain = document.querySelector(".card_main");
cardMore.style.display = "block";
cardMain.style.display = "none";
};
return (
<div>
{data ? (
<div>
<div className="card card_main">
<div className="animate__animated animate__bounceInUp">
<div className="card-image">
<img src={data.sprites.front_default} alt="pokemon_img" />
<span className="card-title">{name}</span>
<button onClick={onClickButtonChange}>More</button>
</div>
<div className="card-content">
{data.abilities.map((n, index) => (
<p key={index}>{n.ability.name}</p>
))}
</div>
</div>
</div>
<div className="card card_more">
<p>{data.height}</p>
<p>{data.weight}</p>
</div>
</div>
) : (
<div>
<TailSpin type="Puff" color="purple" height={100} width={100} />
</div>
)}
</div>
);
}
export { Pokemon };
My implementation of the More button needs to display additional features (the card_more block). Right now this function only works on the very first element. I understand that in React this can most likely be done more correctly, but I don’t know how, so I use CSS styles.
P.S Edited:
I tried to use React features, maybe someone can tell me or does it make sense?
import React from "react";
import { useState, useEffect } from "react";
import { TailSpin } from "react-loader-spinner";
function Pokemon({ name, url }) {
const [data, setData] = useState(null);
const [show, setShow] = useState(false);
useEffect(() => {
fetch(url)
.then((r) => r.json())
.then(setData);
}, [url]);
const handleMore = async () => {
if (show === true) {
setShow(false);
} else if (show === false || !data) {
const r = await fetch(url);
const newData = await r.json();
setData(newData);
setShow(true);
}
};
return (
<div>
{data && show ? (
<div>
<div className="card card_main">
<div className="animate__animated animate__bounceInUp">
<div className="card-image">
<img src={data.sprites.front_default} alt="pokemon_img" />
<span className="card-title">{name}</span>
</div>
<div className="card-content">
{data.abilities.map((n, index) => (
<p key={index}>{n.ability.name}</p>
))}
</div>
</div>
<button onClick={handleMore}>More</button>
</div>
<div className="card card_more">
<p>{data.height}</p>
<p>{data.weight}</p>
</div>
</div>
) : (
<div>
<TailSpin type="Puff" color="purple" height={100} width={100} />
</div>
)}
</div>
);
}
export { Pokemon };
Youre right, this isn't the way you should do it in React. But your problem in your onClickButtonChange-Function is that youre only getting one element with document.querySelector(".card_more") and everytime you call it you get the same element back (No matter on which card you call it)
What you need to do is: Identify the single component elements. Thats most likely solved by passing a id/key value down via props and then putting this id on a parent-element (e.g. div.card) and you give it an id:
<div className="card card_main" id={props.keyvalue}>
....
</div>
And then in your onClickButtonChange-Function you call:
let cardMore = document.querySelector(`#${props.keyvalue} .card_more`);
...
This should give you the right element.
I'm new to react and doing a todo list tutorial. I decided to add an "updateTask" function myself.
When you click the edit button, it turns the task text into an input field and it grabs the value of that input field along with the ID and runs "updateTask" function in App.js
It's basically the same code as "handleToggler" in App.Js (right under updateTask) which updates the state of the checkbox being checked or not.
However, I am getting an error that says "const tasksLeft = tadoState.filter(tado => !tado.completed) is not a function. This is right above the return statement in app.js and it is unrelated to the change I made so I've kind of been stuck.
I console.logged tadoState and a bunch of other stuff I changed to try and see where i messed up but it seems like the state is indeed changing just like I want it to. So I don't know what is causing the error.
APP.js
import Tasklist from './Tasklist.js';
import './index.css';
import { useState, useRef, useEffect } from 'react';
import { v4 as uuidv4 } from 'uuid';
function App() {
// State of Task List
const [tadoState, setTadostate] = useState([]);
const LOCAL_STORAGE_KEY = 'tadoApp.tados'
// GET Local Storage and setState
useEffect(() => {
const tadoStateLS = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY))
if (tadoStateLS) setTadostate(tadoStateLS)
},[])
// SAVE to Local Storage when there is a change to tadoState
useEffect(() => {
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(tadoState))
}, [tadoState])
// Reference to Input Value
const addTodoRef = useRef();
// Add new Task
const addNewTadoHandler = (ref) => {
// Grab value from input field
const tadoValue = ref.current.value;
// Check for user input
if (tadoValue === '') return
// setState
setTadostate(prevTadoState => {
return [...prevTadoState, { id: uuidv4(), name: tadoValue, completed: false }]
})
ref.current.value = null;
}
// Update Existing Task
const updateTask = (id, value) => {
const newTadoState = [...tadoState]
const updatedTask = newTadoState.find(tado => tado.id === id)
updatedTask.name = value
setTadostate(updatedTask)
}
// Enter to Add
const enterToAddHandler = (e) => {
if (e.key === "Enter") {
addNewTadoHandler(addTodoRef);
}
}
// Checkbox Toggler Function
const handleToggler = (id) => {
// Make sure to clone state and then modify. Spread original state. Find item, toggle, and then setStates
const newTadoState = [...tadoState];
const tadoToggle = newTadoState.find(tado => tado.id === id)
tadoToggle.completed = !tadoToggle.completed;
setTadostate(newTadoState);
}
// Clear Completed Tasks
const clearTasksHandler = () => {
const newTadoState = tadoState.filter(tado => tado.completed === false);
setTadostate(newTadoState);
}
const clearAllTasksHandler = () => {
setTadostate([]);
}
const clearTaskHandler = (id) => {
const newTadoState = tadoState.filter(tado => tado.id !== id);
setTadostate(newTadoState);
}
// # of tasks left
const tasksLeft = tadoState.filter(tado => !tado.completed);
return (
<>
<div className="container-sm" style={{textAlign:'center', marginTop:"100px"}}>
<div className="row">
<div className="col-md-6 m-auto">
<h1 className="fw-bolder">TADOSKY</h1>
<p className="fw-light">All Tasks</p>
<div className="container">
<div className="row align-items-center">
{/* INPUT FIELD */}
<div className="col-10">
<input onKeyDown={enterToAddHandler} ref={addTodoRef} type="text" className="form-control" style={{width:"107%"}} />
</div>
{/* ADD BUTTON */}
<div className="col-2">
<button onClick={() => addNewTadoHandler(addTodoRef)} className="btn bg-black text-white fw-bold m-2">+</button>
</div>
</div>
</div>
{/* CLEAR COMPLETED BUTTON */}
<button onClick={clearTasksHandler} className="btn btn-outline-dark fw-lighter m-2 px-3">Clear Completed Tasks</button>
{/* CLEAR ALL */}
<button onClick={clearAllTasksHandler} className="btn btn-purple text-white m-2">Clear All</button>
{/* # of Tasks left */}
<p className="mt-3 ">{tasksLeft.length} Tasks tado!</p>
{/* TASKLIST */}
<Tasklist tadoState={tadoState} updateTask={updateTask} handleToggler={handleToggler} clearTaskHandler={clearTaskHandler} />
</div>
</div>
</div>
</>
);
}
export default App;
TadoInput.js
import React, { useState, useEffect, useRef } from 'react'
export default function TadoInput({tado, editableState, updateTask}) {
const editRef = useRef()
const newInputHandler = (e) => {
const value = editRef.current.value
if (e.key === 'Enter') {
updateTask(tado.id, value)
}
}
if (editableState === false) {
return (
<div className="card-title">
{tado.name}
</div>
)
} else {
return (
<div className="card-title">
<input onKeyDown={newInputHandler} ref={editRef} type="text" style={{width: "300px"}} />
</div>
)
}
}
Is there anything wrong in the code you can spot that I'm missing?
Most tutorials I found on this subject were outdated. So here I am.
What I expect to do with this app is, input text into the field and filter the results based on what you input. Currently I'm stuck and I've been through so many array methods such as filter, indexOf etc. I'm sure I am overthinking the issue so I need help. Here's the code I have currently:
import React, { useState, useEffect } from "react";
import axios from "axios";
const ITEMS_API_URL = "https://restcountries.eu/rest/v2/all";
function Autocomplete() {
const [countryArr, setCountryArr] = useState([]);
useEffect(() => {
axios.get(ITEMS_API_URL).then((res) => {
setCountryArr(() => {
let arr = res.data;
arr.splice(10, arr.length);
return arr;
});
console.log(countryArr);
});
}, []);
const onChange = e => {
const inputValue = e.target.value
const filteredSuggestions = countryArr.find(arr => arr.name == inputValue)
setCountryArr(filteredSuggestions)
}
return (
<div className="wrapper">
<div className="control">
<input type="text" className="input" onChange={onChange} />
</div>
<div className="list is-hoverable" />
{countryArr.map((country) => {
return (
<ul key={country.numericCode}>
{country.name}
</ul>
)
})}
</div>
);
}
export default Autocomplete;
You should not change to actual data source (countryArr) otherwise it reset and store last filtered on that. so I create state variable filteredCountryArr for filter and setting up filtered valued on that.
import React, { useState, useEffect } from "react";
import axios from "axios";
const ITEMS_API_URL = "https://restcountries.eu/rest/v2/all";
function Autocomplete() {
const [countryArr, setCountryArr] = useState([]);
const [filteredCountryArr, setFilteredCountryArr] = useState([]);
useEffect(() => {
axios.get(ITEMS_API_URL).then(res => {
setCountryArr(() => {
let arr = res.data;
arr.splice(10, arr.length);
return arr;
});
console.log(countryArr);
});
}, []);
const onChange = e => {
const inputValue = e.target.value;
const filteredSuggestions = countryArr.find(arr => arr.name == inputValue);
setFilteredCountryArr(filteredSuggestions);
};
return (
<div className="wrapper">
<div className="control">
<input type="text" className="input" onChange={onChange} />
</div>
<div className="list is-hoverable" />
{filteredCountryArr && filteredCountryArr.length > 0 && filteredCountryArr.map(country => {
return <ul key={country.numericCode}>{country.name}</ul>;
})}
</div>
);
}
export default Autocomplete;
I have multiple variants in my one component, and I want that if I click on any variant it should add active class to it and removes active class from another. But I am stuck how it can happen using state in multiple variants loop.
Here is my code:
import { useState } from "react";
const Variants = () => {
// Sample Variants Object - But in real it's coming from Wordpress back-end GraphQL
const vats = {
"Tech": ['3G', '5G'],
"Color": ['Red', 'Gray']
}
const [selectedTech, techToChange] = useState(null);
return (
<div>
{vats && (
<div>
{Object.keys(vats).map((key, value) => (
<div key={key}>
<div><b>{key}</b></div>
{vats[key].map((val) => (
<div key={val} onClick={() => techToChange('active')}>
<label
className="cursor-pointer bg-yellow-100"
>
{val} - {selectedTech}
</label>
</div>
))}
<hr/>
</div>
))}
</div>
)}
<hr />
</div>
)
}
You can see in my code, there are 2 variants named Tech & Color, For example; If I click on 3G of tech, it should add active class to it and removes active class from 5G & If I click on Red it should add active class to it and removes active class from Gray. Can someone please help me to do it? I am stuck
You're just setting selectedTech to 'active'–this is just a string and doesn't create any sort of relationship between the tech clicked and the selectedTech in state.
To fix this, you need to set the selectedTech to the actual val of the one you clicked. To add the variant separation you want, the state can mimic the shape of your variants and be an object. So instead of setting selectedTech directly, you can set selectedTech[variant] to the value you clicked.
And then, with a little evaluation, you can print out the string, active when you click on one.
import { useState } from "react";
export default () => {
// Sample Variants Object - But in real it's coming from Wordpress back-end GraphQL
const vats = {
Tech: ["3G", "5G"],
Color: ["Red", "Gray"]
};
const initialState = Object.fromEntries(Object.keys(vats).map((key)=> [key, null])); const [selectedTech, techToChange] = useState(initialState);
return (
<div>
{vats && (
<div>
{Object.keys(vats).map((key, value) => (
<div key={key}>
<div>
<b>{key}</b>
</div>
{vats[key].map((val) => (
<div key={val} onClick={() => techToChange((c) => ({...c, [key]: val}))}>
<label className="cursor-pointer bg-yellow-100">
{val} - {selectedTech[key] === val ? "active" : null}
</label>
</div>
))}
<hr />
</div>
))}
</div>
)}
<hr />
</div>
);
};
CodeSandbox: https://codesandbox.io/s/inspiring-kare-hsphp?file=/src/App.js
useMap from react-use would fit nicely in to your existing code:
const [vats, {set, setAll, remove, reset}] = useMap({
"Tech": ['3G', '5G'],
"Color": ['Red', 'Gray']
}
...
{Object.keys(vats).map((key, value) => (
...
<div key={val} onClick={() => set(key, val)}>
Here is the return code. You need to pass the val to set the state of selectedTech state variable and then compare the actual value with it to have the active class set.
const [selectedTech, techToChange] = useState([]);
const selectOps = (key,val) => {
let existing = selectedTech;
let idx = existing.findIndex(i => i.key===key);
console.log(idx);
if(idx > -1){
existing.splice(idx,1);
console.log(idx, existing);
}
techToChange([...existing, {key,val}]);
}
return (
<div>
{vats && (
<div>
{Object.keys(vats).map((key, value) => (
<div key={key}>
<div><b>{key}</b></div>
{vats[key].map((val) => (
<div key={val} onClick={() => selectOps(key,val)}>
<label
className={`cursor-pointer bg-yellow-100 ${selectedTech.some(s => s.val===val) ? 'active' : undefined}`}
>
{val}
</label>
</div>
))}
<hr/>
</div>
))}
</div>
)}
<hr />
</div>
)
This is an example of using useRef and some other stuff that you might find useful:
import React, { useState, useRef, useEffect} from "react";
const Variants = () => {
// Sample Variants Object - But in real it's coming from Wordpress back-
end GraphQL
const vats = {
"Tech": ['3G', '5G'],
"Color": ['Red', 'Gray']
}
const labelRef = useRef();
const [selectedTech, techToChange] = useState("");
const [selectedColor, colorToChange] = useState("");
useEffect(()=>{
console.log(selectedColor);
if(labelRef.current.innerHTML.trim() === selectedColor) {
labelRef.current.className ="other class";
}
else {
labelRef.current.className ="cursor-pointer bg-yellow-100";
}
},
[selectedColor, labelRef])
useEffect(()=>{
console.log(selectedTech);
if(labelRef.current.innerHTML.trim() === selectedTech) {
labelRef.current.className ="other class";
}
else {
labelRef.current.className ="cursor-pointer bg-yellow-100";
}
},
[selectedTech, labelRef])
return (
<div>
{vats && (
<div>
{Object.keys(vats).map((key, value) => (
<div key={key}>
<div><b>{key}</b></div>
{vats[key].map((val) => (
<div key={val} onClick={(e) => {
if(key==="Tech") {
//console.log(e.target.innerHTML);
techToChange(e.target.innerHTML.trim());
labelRef.current = e.target;
//console.log(val.trim(),selectedTech.trim());
}
else if(key==="Color")
{
colorToChange(e.target.innerHTML.trim())
labelRef.current = e.target;
}
}
}>
<label ref={labelRef}
className="cursor-pointer bg-yellow-100"
>{val}
</label>
</div>
))}
<hr/>
</div>
))}
</div>
)}
<hr />
</div>
)
}
export default Variants;
I've created a very simplified code version of my problem to understand the REACT rendering using typescript. When I click a button which changes state in the lowest child element all parent elements are updated by the renderer and their children on other forks. How can I change the below so it doesn't do that.
import * as React from 'react';
import { connect } from 'react-redux';
import './Grid.css';
const RenderPopup = (key: number) => {
const open = () => setShowDialog(true);
const [showDialog, setShowDialog] = React.useState(false);
const close = () => setShowDialog(false);
if (!showDialog) {
return (
<div>
<button onClick={open}>do it</button>
</div>
)
}
else {
return (
<div>
<button onClick={close}>close
</button>
</div>
)
}
}
function Cell(key:number) {
return (
<div key={key}>
{key}
{RenderPopup(key)}
</div>
)
}
const Header = () => {
return (
<div className="gridRow">
{Cell(0)}
{Cell(1)}
{Cell(2)}
</div>
)
}
const Person = (rowNum: number) => {
return (
<div key={rowNum} className="gridRow">
{Cell(0)}
{Cell(1)}
{Cell(2)}
</div>
)
}
const Persons = () => {
return (
<div>
{Person(1)}
{Person(2)}
{Person(3)}
</div>
)
}
const Grid = () => {
return (
<div>
<Header />
<Persons />
</div>
);
}
export default connect()(Grid);