I have a form with key and value input fields. I want the user to be able to edit key value pairs. Value simply edits the value, but key needs to update the key to what ever he is typing. I got the value change working, but I am not sure how to edit the key as this needs to update, remove and keep track of the key changed.
So lets say this is the current state:
{"name":"Joe"}
Now the user wants to edit name to firstname in the textfield.
As he types state will look like this with each keypress:
{"f":"Joe"}
{"fi":"Joe"}
{"fir":"Joe"}
{"firs":"Joe"}
{"first":"Joe"}
{"firstn":"Joe"}
etc
Here is my code:
function KeyValuePair({ initialPair }) {
const [pairs, setPairs] = React.useState(initialPair ? initialPair : {});
const handleInputUpdate = e => {
const { name, value, id } = e.target;
if (name === "key") {
// ??
}
if (name === "value") {
setPairs({ ...pairs, [id]: value });
}
};
}
<Grid item lg={3}>
<TextField
name="key"
label="Key"
id={key}
value={key}
margin="normal"
onChange={handleInputUpdate}
fullWidth
/>
</Grid>
<Grid item lg={3}>
<TextField
name="value"
label="Value"
margin="normal"
id={key}
value={pairs[key]}
onChange={handleInputUpdate}
fullWidth
/>
</Grid>
UPDATE
I have got it working like this:
const handleInputUpdate = e => {
const { name, value, id } = e.target;
if (name === "key") {
const keyValue = pairs[id];
let oldState = pairs;
delete oldState[id];
setPairs({ ...oldState, [value]: keyValue });
}
if (name === "value") {
setPairs({ ...pairs, [id]: value });
}
};
But this loses focus when typing because the old element is removed and new key rendered. Need to keep focus so you can keep typing.
As your key is editable, you'll need to rely on another property that you can rely on whilst your key value pairs change - like a separate id, or their index position in an array.
Going down the array route, your entries would look something like this:
const initialEntries = [
{ key: "myKey", value: "myValue" },
{ key: "myOtherKey", value: "myOtherValue" },
{ key: "otherOtherKey", value: "otherOtherValue" }
];
If you map over your entries you can use the index:
{entries.map((entry, index) => (
<div>
<span>{index}</span>
<input
name="key"
onChange={ev => updateKey(index, ev)}
value={entry.key}
/>
<input
nanme="value"
onChange={ev => updateValue(index, ev)}
value={entry.value}
/>
</div>
))}
And then handle the updates:
const updateKey = (id, ev) => {
const newKeyValue = ev.target.value;
const updatedEntries = entries.map((entry, index) =>
index === id ? { ...entry, key: newKeyValue } : entry
);
setEntries(updatedEntries);
};
const updateValue = (id, ev) => {
const newValueValue = ev.target.value;
const updatedEntries = entries.map((entry, index) =>
index === id ? { ...entry, value: newValueValue } : entry
);
setEntries(updatedEntries);
};
Here's a working codesandbox that shows roughly how you can do it: https://codesandbox.io/s/sleepy-cherry-n0518
Related
I am trying to dynamically add and remove text fields for user entry.
When I click the button to remove a particular row, I want to modify the state object to remove the data of that row thereby causing the row to disappear. However I am unable to do that and I am getting an error in the render loop as the compiler is unable to find the value for the row.
The error is as follows
Cannot read properties of undefined (reading 'from')
I want it to look at the new state object and display the number of rows accordingly.
Here is the code for sandbox
import "./styles.css";
import React from "react";
import { Button, Grid, Paper } from "#mui/material";
import { TextField, Icon } from "#mui/material";
interface State {
serialInputObjects: any;
}
class SerialQRScanClass extends React.PureComponent<State> {
state = {
serialInputObjects: {
0: { from: "", to: "", except: "" }
}
};
//Delete the already registered scanned codes code here
handleAdd = () => {
const objectLength = Object.keys(this.state.serialInputObjects).length;
console.log(objectLength);
this.setState((prevState) => ({
...prevState,
serialInputObjects: {
...prevState.serialInputObjects,
[objectLength]: {
from: "",
to: "",
except: "",
fromError: "",
toError: ""
}
}
}));
console.log(this.state.serialInputObjects);
};
handleChangeFromSerials = (key: any, data: string) => {
this.setState((prevState) => ({
...prevState,
serialInputObjects: {
...prevState.serialInputObjects,
[key]: { ...prevState.serialInputObjects[key], from: data }
}
}));
console.log(this.state.serialInputObjects);
//this.calculation(key);
};
handleChangeToSerials = (key: any, data: string) => {
this.setState((prevState) => ({
...prevState,
serialInputObjects: {
...prevState.serialInputObjects,
[key]: { ...prevState.serialInputObjects[key], to: data }
}
}));
console.log(this.state.serialInputObjects);
//this.calculation(key);
};
handleRemove = (key) => {
console.log(this.state.serialInputObjects);
this.setState((prevState) => ({
...prevState,
serialInputObjects: { ...prevState.serialInputObjects, [key]: undefined }
}));
console.log(this.state.serialInputObjects);
};
render() {
return (
<Paper elevation={3} className="abc">
<Button onClick={this.handleAdd}>ADD NEW FIELD</Button>
{Object.keys(this.state.serialInputObjects).map((key) => (
<div key={key}>
<Grid container alignItems="flex-end">
<Grid item className="bcd">
<TextField
fullWidth
label={"FROM"}
placeholder={"Ex.100"}
value={this.state.serialInputObjects[key]["from"]}
onChange={(e) =>
this.handleChangeFromSerials(key, e.target.value)
}
error={
Boolean(this.state.serialInputObjects[key]["fromError"]) ||
false
}
helperText={this.state.serialInputObjects[key]["fromError"]}
margin="none"
size="small"
/>
</Grid>
<Grid item className="bcd">
<TextField
fullWidth
label={"To"}
placeholder={"Ex.100"}
value={this.state.serialInputObjects[key]["to"]}
onChange={(e) =>
this.handleChangeToSerials(key, e.target.value)
}
error={
Boolean(this.state.serialInputObjects[key]["toError"]) ||
false
}
helperText={this.state.serialInputObjects[key]["toError"]}
margin="none"
size="small"
/>
</Grid>
<Grid
item
className={"abc"}
style={{ paddingLeft: "10px" }}
></Grid>
<div style={{ display: key === "0" ? "none" : "block" }}>
<Button onClick={(e) => this.handleRemove(key)}>
<Icon fontSize="small">remove_circle</Icon>
</Button>
</div>
</Grid>
</div>
))}
</Paper>
);
}
}
export default function App() {
return (
<div className="App">
<SerialQRScanClass />
</div>
);
}
I want to modify the state object to remove the data of that row thereby causing the row to disappear.
If you want to cause the row to disappear you have to update the serialInputObjects object without the row you want to delete.
Right now you are just assigning the value undefined to the selected row so it still exists but it doesn't contain the property from anymore, and because you are referring to that property here:
value={this.state.serialInputObjects[key]["from"]}
Javascript tells you that it is undefined, beacuse it doesn't exists.
Now for this you will need destructuring assigment, this is the solution:
handleRemoveKey(key){
const { [key]: renamedKey, ...remainingRows } = this.state.serialInputObjects;
this.setState((prevState) => ({
...prevState,
serialInputObjects: { ...remainingRows }
}));
}
However if you want to know why that first line of the function works, follow me.
Let's say you have this object:
const myObj = {
a: '1',
b: '2',
c: '3',
}
And let's say we need to separate the c prop from the others, we do that like this:
const { c, ...otherProps } = myObj
This would result in the creation of 2 new const, the const c and the const otherProps:
The const c will contain the value '3'
The const otherProps will contain this object { a: '1', b: '2' }
But what happen if there is already a variable named c? Our newly created c const in the destructuring statement would be a duplicate which is not allowed of course, so what can we do? We rename our newly created c const while we are destructuring myObj, like this:
const { c: renamedC, ...otherProps } = obj
This way the newly created const would be renamedC and therefore there will be no conflict with the other c we just supposed for the example.
That is exactly we are doing here:
handleRemoveKey(key){ // <------ here is already a "variable" named key
const { [key]: renamedKey, ...remainingRows } = this.state.serialInputObjects;
// so we had to rename our newly created 'key' const to 'renamedKey' to avoid conflicts.
As a side note I would suggest serialInputObjects should be an array(of objects) and not an object because arrays are ordered while objects are not, this whole process would have been easier if serialInputObjects would have be an array.
Iam using multiple inputs inside maps i want to set focus to next input when i click enter in react Hooks.
With the help of refs
Iam using material ui text field for getting input
I tried in react class component wihtout ref it works with error but in hooks it not works
class compomnent code:
constructor(props) {
this.state = {}
}
inputRefs = [];
_handleKeyPress = e => {
const {currentTarget} = e;
let inputindex = this.inputRefs.indexOf(currentTarget)
if (inputindex < this.inputRefs.length - 1) {
this.inputRefs[inputindex + 1].focus()
}
else {
this.inputRefs[0].focus()
}
};
Inside render in added this within map function
this.state.data.map((data) => return (
<TextField
inputProps = {{onKeyPress:(e) => this.function1(e, data)}}
onChange={this.changevaluefunction}
inputRef={ref => this.inputRefs.push(ref)}
onFocus={this.handleFocus} ref={`input${id}`} /> ))
I have implemented the solution in a different way with the functional component. I have taken the 4 fields and seated its ref with the createRef hook.
I can see from your solution, you wanted to move focus to the next input element whenever you press Enter key on the current element.
I am passing the next target element argument in the onKeyUp handler along with the actual event and then detecting whether the Enter key is pressed or not. If Enter key is pressed and the targetElem is present then I am moving focus to the passed targetElem. By this way you have better control over the inputs.
You can see my solution here
https://codesandbox.io/s/friendly-leftpad-2nx91?file=/src/App.js
import React, { useRef } from "react";
import TextField from "#material-ui/core/TextField";
import "./styles.css";
const inputs = [
{
id: "fName",
label: "First Name"
},
{
id: "lName",
label: "Last Name"
},
{
id: "gender",
label: "Gender"
},
{
id: "address",
label: "Address"
}
];
export default function App() {
const myRefs = useRef([]);
const handleKeyUp = (e, targetElem) => {
if (e.key === "Enter" && targetElem) {
targetElem.focus();
}
};
return (
<div>
{inputs.map((ipt, i) => (
<TextField
onKeyUp={(e) =>
handleKeyUp(e, myRefs.current[i === inputs.length - 1 ? 0 : i + 1])
}
inputRef={(el) => (myRefs.current[i] = el)}
id={ipt.id}
fullWidth
style={{ marginBottom: 20 }}
label={ipt.label}
variant="outlined"
key={ipt.id}
/>
))}
</div>
);
}
You can convert this.inputRefs into a React ref so it persists through renders, and other than this you pretty much remove all references to any this object.
Example Component:
const LENGTH = 10;
const clamp = (min, max, val) => Math.max(min, Math.min(val, max));
export default function App() {
const [data] = useState([...Array(LENGTH).keys()]);
const inputRefs = useRef([]); // <-- ref to hold input refs
const handleKeyPress = index => () => { // <-- enclose in scope
const nextIndex = clamp(0, data.length - 1, index + 1); // <-- get next index
inputRefs.current[nextIndex].focus(); // <-- get ref and focus
};
return (
<div className="App">
{data.map((data, index) => (
<div key={index}>
<TextField
inputProps={{ onKeyPress: handleKeyPress(index) }} // <-- pass index
inputRef={(ref) => (inputRefs.current[index] = ref)} // <-- save input ref
/>
</div>
))}
</div>
);
}
If you are mapping the input field and want to focus on click, you can directly give the id attribute to the input and pass the array id.
After that, you can pass id inside a function as a parameter, and get it by document.getElementById(id).focus().
I am not seeing the solution to this issue which I am sure is obvious. Classic country/state issue
scenario:
redux form with two Form.Select (semantic ui-react) Fields
<FormSection name="addressInfo">
<Field
component={renderSelect}
label="Country"
fieldName="country"
name="country"
onChange={(newValue) =>
change("addressInfo.provState", renderProvinceState(newValue))
}
options={renderCountries()}
/>
<Field
component={renderSelect}
label="Province/State"
fieldName="provState"
name="provState"
multiple
options={renderProvinceState("Canada")}
/>
my option builders are the following:
const renderCountries = () => {
if (!countries) return null;
var options = [];
Object.keys(countries).map((key) => {
options.push({
key: key,
text: countries[key].country,
value: countries[key].country,
});
return null;
});
return options;
};
const renderProvinceState = (country) => {
if (!country) return null;
var states = countries.filter((s) => s.country == country)[0].states;
if (!states) return null;
var options = [];
Object.keys(states).map((key) => {
options.push({
key: key,
text: states[key],
value: states[key],
});
return null;
});
return options;
};
my dropdowns create successfully, I get a list of countries and provinces for canada. When I change the country I get the following errors:
index.js:1 Dropdown `value` must not be an array when `multiple` is not set. Either set `multiple={true}` or use a string or number value.
Warning: Failed prop type: Invalid prop `value` supplied to `Dropdown`.
I am new to react, react-redux and would appreciate any insight someone might have. I have tried adding value={[]} to the provState obj but that didn't fix anything.
the actual component I am working with looks like this:
export const renderSelect = ({
label,
meta,
input,
name,
options,
placeholder,
}) => {
return (
<Form.Select
className={`${meta.error && meta.touched ? "error" : ""}`}
label={label}
name={input.name}
onChange={(e, { value }) => input.onChange(value)}
options={options}
placeholder={placeholder}
value={input.value}
/>
);
};
My objective is to toggle switch off and on as per the respective id, but i couldn't able to toggle the switch.
i have tried in this way:
<Switch
checked={data.isShow}
onChange={this.handleChange}
color="primary"
name={data.isShow ? "On" : "Off"}
inputProps={{ "aria-label": "primary checkbox" }}
/>
And OnChange i have written in this way:
handleChange = () => {
this.setState({ isShow: !this.state.isShow });
};
Here is the sample
Can anyone help me in this query?
You should handleChange for specific element. Here I pass the id of the element, and toggle isShow of that element only
handleChange = (id) => {
this.setState({
info: this.state.info.map((i) => {
if (i.id === id) {
return {
...i,
isShow: !i.isShow
};
} else {
return i;
}
})
});
};
// ...
<Switch
checked={data.isShow}
onChange={() => this.handleChange(data.id)}
color="primary"
name={data.isShow ? "On" : "Off"}
inputProps={{ "aria-label": "primary checkbox" }}
/>
Forked demo
No need of state variable just update info array in handleChange(). Below are the changes in handleChange() method
handleChange = (singleRowData) => {
console.log(singleRowData);
const {info} = this.state;
const index = info.findIndex(data => data.id === singleRowData.id);
info[index].isShow = !info[index].isShow;
this.setState({ info: [...info] });
};
Change handleChange function to:
handleChange = (event) => {
let changeId = parseInt(event.target.id);
let info = this.state.info.map((data) => {
if (data.id === changeId) {
return Object.assign({}, data, {
isShow: !data.isShow
});
}
return data;
});
this.setState({info: info});
};
Include id as part of the Component params
<Switch
checked={data.isShow}
onChange={this.handleChange}
id={data.id}
color="primary"
name={data.isShow ? "On" : "Off"}
inputProps={{ "aria-label": "primary checkbox" }}
/>
Edit:
Made edits to the sample link you've provided - it's working here
You need to change the isShow property from the item in the info array (in your state).
you can use find() in order to find the item with the same id,
but before that you need to use slice() to copy info array, because you don't mutate your state.
Never mutate this.state directly, as calling setState() afterwards may replace the mutation you made. Treat this.state as if it were immutable.
(Referenced from React docs)
your handleChange() method should look like this then:
handleChange = (id) => {
const data = this.state.info.slice();
const item = data.find((item) => item.id === id);
item.isShow = !item.isShow;
this.setState(data);
};
Here is the full result:
https://codesandbox.io/s/async-bird-998ue?fontsize=14&hidenavigation=1&theme=dark
I have a bootstrap input which renders a list of value onChange of the input. And if a user selects a value from the list i want to show it in my input field. But it should have a ability to type again on the input field if necessary (to select another value). I'm using react-bootstrap and my approach is as follows
searchConsumer = (e) => {
const {consumer} = this.props;
let consumerValue = e.target.value;
const data = {
"filterText": consumerValue,
"page": 1,
"maxRecords": this.state.maxRecords
}
consumer(data);
}
selectConsumer(name){
this.setState({
selectedValue:name
})
}
renderConsumerList(){
const {consumers} = this.props;
if(consumers.consumerData.length > 0) {
return consumers.consumerData.map(item =>{
return (
<div className="consumer_search_item" onClick={()=>this.selectConsumer(item.name)}>{item.name}</div>
)
})
}
}
<Form.Control type="search" onChange={(e)=>this.searchConsumer(e)} value={consumers.consumerData.length > 0 ? selectedValue : ''} className="modal_input" placeholder="Search any consumer by name" required />
<div className="consumer_result_container">{this.renderConsumerList()}</div>
I can set the value successfully if i select a value from the list. But if i want to change it i cannot change it because the value is already set in the input field and does not let me delete or edit the value. How can i fix this issue?
I think what happens is when onChange (searchConsumer) is called, the new value that is typed is not updating state:
searchConsumer = ( e ) => {
const { consumer } = this.props;
let consumerValue = e.target.value;
const data = {
"filterText": consumerValue,
"page": 1,
"maxRecords": this.state.maxRecords
}
consumer( data );
// Update state because new value is typed
this.setState({
inputValue: consumerValue
})
}
Form.Control value should reflect the state:
<Form.Control type="search" onChange={ ( e ) => this.searchConsumer( e ) } value={ consumers.consumerData.length > 0 ? this.state.inputValue : '' } />