Pre-populate Text Input With Object Values React - javascript

I'm trying to pre-populate text fields with pre-existing data in an object. I'm using a component renderer which is built from a switch statement like so (I'll remove the other cases to make it more readable):
switch (data.type) {
case 'text':
return (
<div
key={data.index}
data-name={selection}
>
<FieldLabel
htmlFor={data.name}
label={data.value}
/>
<FormInput
type={data.type}
name={data.name}
placeholder={data.placeholder}
onChange={
e => props.onCommentsChange(
e,
data.structure,
data.rules
)}
value={selection}
err={props.error === 1
&& props.validationErrors !== null
&& data.name in props.validationErrors
? 1
: 0
}
/>
</div>
)
}
The value of the input is the selection variable which is set like so:
let selection;
const data = {
type: props.type,
options: props.options,
name: props.name,
value: props.value,
structure: props.structure,
rules: props.rules,
index: props.index,
placeholder: props.placeholder,
order: props.order,
date: props.date,
onDateChange: props.onDateChange,
onFocusChangeHandler: props.onFocusChangeHandler,
focused: props.focused,
};
const section = Object.entries(data.structure)[0][0];
From this, I'm trying to use an if statement which loops through like so and sets the selection variable to the value:
if (props.usersCurrentSelection !== null && props.usersCurrentSelection !== undefined) {
console.log(props.usersCurrentSelection.data);
console.log(section);
console.log(props.usersCurrentSelection.data[section]);
if (section in props.usersCurrentSelection.data) {
Object.keys(props.usersCurrentSelection.data[section]).forEach(
item => {
let value = props.usersCurrentSelection.data[section][item];
console.log(value);
selection = value;
}
);
}
}
the console logs return the following:
As you can see it's finding the value relevant to it's key in the loop. And since the text component is taking the selection variable as its value I'm unsure as to why this isn't populating?
Thanks for the help.

This is some pseudo code to show the approach I would probably take, based on your recent comment:
const Input = props => {
// your switch statement goes here
}
const SectionFields = props => {
const { type, options, name, value, structure = {}, rules, index, placeholder, order, date, onDateChange, onFocusChangeHandler, focused } = props
const usersCurrentSelection = props.usersCurrentSelection || { data: {} }
const section = Object.keys(structure)[0]
const data = { type, options, name, value, structure, rules, index, placeholder, order, date, onDateChange, onFocusChangeHandler, focused }
const itemsInSection = Object.values(usersCurrentSelection)
return itemsInSection.length > 0
? (
<>
itemsInSection.map(([key, val]) => (
<Input data={data} item={val} />
))
</>
)
: null
}
const Form = props => (
<>
<SectionFields /*{props.section1 here}*/ />
<SectionFields /*{props.section2 here}*/ />
<SectionFields /*{props.section3 here}*/ />
</>
)
This is probably too simplistic for your needs, but it might point you in the right direction. It's still a little hard to be certain what the entire, finished form is supposed to look like, and how the underlying data is organized.

Related

Input loses focus when javascript map method is used to render it dynamically

const HeaderField = ({
handleHeaderChange,
removeHeader,
index,
header,
}: any) => (
<HeaderContainer>
<TextField
label="Key"
value={header.key}
onChange={e => handleHeaderChange(e.target.value, 'key', index)}
/>
<TextField
label="Value"
value={header.value}
onChange={e => handleHeaderChange(e.target.value, 'value', index)}
/>
);
const CustomHeadersField = ({ handleChange, fieldKey, categoryKey }: any) => {
const [headers, setHeaders] = React.useState<headersType[] | null>(null);
const addHeader = () => {
const newHeaders = headers ? [...headers] : [];
newHeaders.push({ key: '', value: '' });
setHeaders(newHeaders);
};
const handleHeaderChange = (
value: string,
type: 'key' | 'value',
index: number,
) => {
const newHeaders: any = headers ? [...headers] : [];
if (type === 'key') {
newHeaders[index].key = value;
} else {
newHeaders[index].value = value;
}
setHeaders(newHeaders);
handleChange(newHeaders, fieldKey, categoryKey);
};
return (
<CustomHeadersContainer>
{headers?.map((header, index) => (
<HeaderField
key={Math.random()}
handleHeaderChange={handleHeaderChange}
removeHeader={removeHeader}
index={index}
header={header}
/>
))}
<Button
onClick={addHeader}
variant="contained"
sx={{ width: 'fit-content' }}
>
Add Header
</Button>
</CustomHeadersContainer>
);
};
Add Header button adds a new key-value pair to the headers state, adding two new fields in the form.
Now header.map destroys the old components and renders new one for every key press causing it to lose focus.
I tried moving HeaderField component out of CustomHeadersField. It didn't work.
Thank you for the help!
According to react documentation:
Keys should be stable, predictable, and unique. Unstable keys (like those produced by Math.random()) will cause many component instances and DOM nodes to be unnecessarily recreated, which can cause performance degradation and lost state in child components.
Passing Math.random() as a key is the issue. You can use index as a key if you are not reordering the list. Refer this answer

Unable to handle multiple input type checkbox fields in React

So I have this component where I have around 15 input type checkboxes, which upon checked/unchecked show/hide images. All fields have an onChange handler which takes the category name of the images. The category type is then checked in a conditional and calls another helper function. All of them follows the same steps.
Now I'm trying to attach a local state to each of the checkboxes(all 15 of them) so that when one of them is checked it will show the respective category image and when unchecked it will hide the image.
Right now it is working but in reverse. I mean when it is checked it hides the image and shows it when unchecked. Also, on the first click on the checkbox, there is no response (no hide or show).
Here's the component. For simplicity's sake I'm only showing 3 fields. But there are around 15:
const Images= () => {
const [ isNatureChecked, setIsNatureChecked ] = useState( false );
const [ isCityChecked, setIsCityChecked ] = useState( false );
const [ isAbstractChecked, setIsAbstractChecked ] = useState( false );
const handleImages = ( e, type ) => {
if ( type === 'nature' ) {
setIsNatureChecked( !isNatureChecked );
showImage( 0, isNatureChecked && new Image.Nature() );
} else if ( type === 'city' ) {
setIsCityChecked( !isCityChecked );
showImage( 1, isCityChecked && new Image.City() );
} else if ( type === 'abstract' ) {
setIsAbstractChecked( !isAbstractChecked);
showImage( 2, isAbstractChecked && new Image.Abstract() );
}
}
return (
<div>
<input type="checkbox" checked={isNatureChecked} onChange={( e ) => handleImages( e, 'nature' )} />
<input type="checkbox" checked={isCityChecked} onClick={( e ) => handleImages( e, 'city' )} />
<input type="checkbox" checked={isAbstractChecked} onClick={( e ) => handleImages( e, 'abstract' )} />
</div>
)
}
The first parameter in showImage func is the index.
How can I make the checkbox work in the right way? I mean when it will be checked, it will show image, and when it will be unchecked it will hide it.
Moreover, please suggest if this code can be refactored. 15 input fields will have 15 states and 15 conditionals. That's too many repetitive code which I want to avoid. So please suggest me the correct way.
your handleImages function doesn't have an updated value which is why you don't see a change on the first click. You need to use the useEffect hook to dynamically show image and set the state value as a dependency.
A better way to approach this would be to create an object state e.g.
const [checkedImages, setCheckedImages] = useState({ nature: false, city: false, abstract: false });
and then your handler can switch the boolean states without any if else conditions.
const handleChange = (e, type) => {
setCheckedImages(prevState => ({...prevState, [type]: !checkedImages[type]}))
}
you will need a useEffect block to dynamically show images
lastly, the input fields can be mapped without having to write them one by one
Try This
const Images= () => {
const [imagesArr,setImagesArr] = useState([{name:'Nature',isChecked:false,index:0},{name:'City',isChecked:false,index:1},{name:'Substract',isChecked:false,index:2},])
const handleImages = ( e, imageData,imageDataIndex ) => {
setImagesArr(arr=>{
let arr2 = [...arr]
arr2[imageDataIndex] = {...arr2[imageDataIndex],isChecked:e.target.checked}
return arr2
});
showImage( imageData.index, e.target.checked && new Image[imageData.name]() );
}
return (
<div>
{
imagesArr && imagesArr.length &&
imagesArr.map((data,index)=>{
return (
<input type="checkbox" checked={data.isChecked} onChange={( e ) => handleImages( e, data,index )} />
)
}
}
</div>
)
}

ReactJS - Can i save specific rendered mapped data, to a variable?

I have data mapped in a component like this:
import React from "react";
import { useState } from "react";
import { useEffect } from "react";
import { get } from "lodash";
const Products = ({ data }) => {
return (
data.map((item, index) = > (
<div id={index}>
<img src={item.img} /> <br />
{item.name} <br />
{get(moreData, `[${item.name.toLowerCase()}].info[0]`)}
{get(moreData, `[${item.name.toLowerCase()}].info[1]`)}
{get(moreData, `[${item.name.toLowerCase()}].info[2]`)}
</div>
I want to be able to store this data:
{item.name}
{get(moreData, `[${item.name.toLowerCase()}].info[0]`)}
{get(moreData, `[${item.name.toLowerCase()}].info[1]`)}
{get(moreData, `[${item.name.toLowerCase()}].info[2]`)}
in a string, like string = {item.name},{moreData.item.name.toLowerCase().info[0]},...//etc
However you cannot declare variables inside of a component (as far as i know, still new to this).
I've tried .concat() - after each line and .push() with array instead of string:
{item.name} <br />
{dataString.concat(item.name)}
{dataArr.push(item.name)}
{get(moreData, `[${item.name.toLowerCase()}].info[0]`)}
{get(moreData, `[${item.name.toLowerCase()}].info[1]`)}
{get(moreData, `[${item.name.toLowerCase()}].info[2]`)}
I was going to use DOM, but i've been told it's bad practice to use DOM in react.
I've also tried using state in the same way:
const [dataString, setDataString] = useState("");
...
{item.name}
{setDataString((dataString += item.name))}
But nothing seems to work as intended for me, and i'm out of ideas.
Edit:
I want to be able to copy the 'string/text' to clipboard eventually. So it can be imported to another site. Their required format is Item1, item1-info1, item1-info2, item1-info3, item2, item2-info1, item2-info2, item2-info3...etc
here is an example of how you can use the data object outside of the mapped objects.
also an example how to convert the data to an string in the format required.
because I don't know the structure of the data object of yours I just created an one, take this code and change it to the data structure
const data = [
{
name: "name1",
info1: "info1-1",
info2: "info1-2",
info3: "info1-3",
},
{
name: "name2",
info1: "info2-1",
info2: "info2-2",
info3: "info2-3",
},
{
name: "name3",
info1: "info3-1",
info2: "info3-2",
info3: "info3-3",
},
];
change this function to one that fits your needs.
const getStringOfData = (data) => {
let stringArray = [];
data.map((item) => {
stringArray.push(item.name);
stringArray.push(item.info1);
stringArray.push(item.info2);
stringArray.push(item.info3);
});
let stringResult = stringArray.join(",");
return stringResult;
};
useEffect(() => {
console.log("onMounte with useEffect");
let stringResult = getStringOfData(data)
console.log(stringResult);
}, []);
you can also call this function on onClick function depend in your requirement , the data object is available almost everywhere in your component
Continuing our discussion from the comments, it looks like there are two different things you are trying to do here:
display the UI to the user in a certain way
to be able to copy the information in a specific stringified format
I would advice to split these two behaviors, because they are totally unrelated to each other in terms of logic.
Start by aligning the information in your components how you need it.
const Product = ({ item, index }) => {
// do whatever you need to do with the information
// to display it correctly
const identifier = name.toLowerCase()
const infos = (moreData[identifier] && moreData[identifier].info) || {}
return (
<div>
{ info[0] && <p> info[0] </p>}
{ info[1] && <p> info[1] </p>}
{ info[1] && <p> info[2] </p>}
</div>
)
}
const Products = ({ data }) => {
const onClick = useCallback(
async () => {
// parse data and copy it using a separate
// util method.
const dataToString = /* actual implementation of the formatting to a string representation */
copyTextToClipBoard(dataToString)
},
[data]
)
return (
<div>
<button onClick={onClick} />
<div>
data.map((item, index) => (
<Product item={item} index={index} key={item.id} />
))
</div>
</div>
)
};
Now this would be your UI, the copyToClipboard method, is a bit of a hack, and looks like so:
const copyTextToClipBoard = (text) => {
const element = document.createElement('textarea');
element.style = { display: 'none' }; // eslint-disable-line
element.value = text;
document.body.appendChild(element);
element.select();
document.execCommand('copy');
document.body.removeChild(element);
};

how to update material ui textfield correctly onChange and put value into Json object?

I am trying to update the title and body objects within my array with the value provided from the input fields that mapped to the array length. The body object updates perfectly but the title updates with only the current input value. For instance, if I were to try and type "the".
The title object would change as follows "title: "t"" => "title: "h""
=> "title: "e".
Desired output would be "title: "t"" => "title: "th"" => "title:
"the".
This works with the body so I am confused why it is not with the title maybe I have missed something.
export const NewsArticlesPage = () => {
const [newsArticlesJson, setNewsArticlesJson] = useContext(
NewsArticlesContext
)
const [numberOfSteps, setNumberOfSteps] = useState(0)
const stepsMap = Array.apply(
null,
Array(numberOfSteps).fill({ title: '', body: '' })
)
let stepsArray = { ...stepsMap } as StepsMapProps
const updateTileArray = (index: number, titleEventData: string) => {
const stepsArrayData = { ...stepsArray }
stepsArray = {
...stepsArrayData,
[index]: { ...stepsArrayData[index], title: titleEventData },
}
console.log(stepsArray[index])
console.log(stepsArray[index]?.title)
}
const updateRichTextArray = (index: number, richTextEventData: string) => {
const stepsArrayData = { ...stepsArray }
stepsArray = {
...stepsArrayData,
[index]: { ...stepsArrayData[index], body: richTextEventData },
}
console.log(stepsArray[index])
console.log(stepsArray[index]?.body)
}
return (
<NewsArticlesWrapper>
<TextField
type="number"
label="Number of steps"
value={numberOfSteps}
InputProps={{ inputProps: { min: 0 } }}
onChange={(e) => setNumberOfSteps(Number(e.target.value))}
/>
{stepsMap.map((n, index) => (
<>
<Typography key={'heading' + index}>Step: {index + 1}</Typography>
<TextField
key={'title' + index}
type="text"
label="Title"
value={stepsArray[index]?.title}
onChange={(titleEventData) =>
updateTileArray(index, titleEventData.target.value)
}
/>
<ReactQuill
key={'quill' + index}
theme="snow"
value={stepsArray[index]?.body}
modules={modules}
onChange={(richTextEventData) =>
updateRichTextArray(index, richTextEventData)
}
/>
</>
))}
<Button
variant="contained"
colour="primary"
size="medium"
onClick={updateNewsArticleJson}
>
Submit Article
</Button>
</NewsArticlesWrapper>
)
}
If any extra information is needed to help me please ask.
Code Sandbox set up to replicate issue: https://codesandbox.io/embed/kind-buck-shg2t?fontsize=14&hidenavigation=1&theme=dark
<TextField /> and <ReactQuill /> input components are defined in a ‘controlled component’ way. The object they’re setting / getting values to is stepsArray which you have created as a normal object
let stepsArray = { ...stepsMap } as StepsMapProps
The onChange handler on both these components are changing values on this object
stepsArray = {
...stepsArrayData,
[index]: { ...stepsArrayData[index], title: titleEventData },
}
but, there isn't a re-render, because React re-renders when state changes. stepsArray is a normal js object, and although the value is changing, it is not causing a re-render. This is the reason, you cannot see the value you type on Title <TextField />component.
To solve this, make the object where you’re setting/getting values to, with useState, and use the set(State) function provided by the useState hook accordingly to update values inside the object. This will cause a re-render whenever input's value changes, because now you will be changing the object created through useState's setter function and your UI will be in sync with the state.

Prevent API returned data from overwriting empty input onFocus

So, I have a series of input fields when onBlur (focusOut) fires an API call posting the data within each input. My issue is, when I leave (onBlur) one input field and go into another, I set the new state of the input to ''.
This works great until the previous onBlur call returns the response which updates the total. When the response comes, it overwrites that state of the focused input with the previous value. Is this preventable?
updateTotal = async ( id, inputValue) => {
const {dispatch, programKey, calculatorKey} = this.props;
const goals = this.getActiveGoals();
const timePeriodKey = this.getActiveTimePeriod().key
const getFromState = goals.filter(goal => (goal.key !== id && (goal.key in this.state.goals))).map(goal => ({metricKey: goal.key, value: this.state.goals[goal.key]}))
const getFromGoals = goals.filter(goal => (goal.key !== id && !(goal.key in this.state.goals))).map(goal => ({metricKey: goal.key, value: goal.goalValue}));
const filteredFromGoals = goals.filter(goal => (goal.key === id)).map(goal => ({metricKey: id, value: inputValue}));
const combinedGoals = getFromGoals.concat(filteredFromGoals, getFromState)
await dispatch(
metricApiActions.createCalculatorCalculateByCalculatorKey(
{ programKey, calculatorKey, timePeriodKey, body: combinedGoals }
)
)
dispatch(
metricApiActions.createCalculatorGoalsCalculateByCalculatorKey(
{programKey, calculatorKey, body: []}
)
)
this.postGoals(programKey, calculatorKey, timePeriodKey, combinedGoals)
}
handleFocus = (e, key) => {
return (
e.target.value = ''
)
}
handleChange = (key, e) => {
const verifyPositiveNumber = Math.abs(e.target.value)
this.setState({goals:{...this.state.goals, [key]: verifyPositiveNumber}});
}
render() {
const total = this.getActiveTotal();
const goals = this.getActiveGoals();
return (
<Modal show={this.props.show} onHide={this.props.onHide}>
<ModalHeader closeButton={true} />
<ModalBody>
{goals.sort((a, b) => a.sortOrder < b.sortOrder ? -1 : 1).map((item, i) => (
<GoalItemContainer key={item.key} className="col-md-24">
<GoalItem className="col-md-offset-1 col-sm-offset-1 col-xs-offset-1 col-md-20 col-sm-20 col-xs-20 "><Text>{item.label}</Text></GoalItem>
<GoalItemInput
id={item.key}
type="number"
min='0'
className="col-md-2 col-sm-2 col-xs-2 col-md-pull-1 col-sm-pull-1 col-xs-pull-1"
name={item.key}
onBlur={(e) => {this.updateTotal(item.key, e.target.value)}}
onChange={(e) => this.handleChange(item.key, event)}
onFocus={(e) => this.handleFocus(event, item.key)}
value={this.state.goals[item.key] !== undefined ? this.state.goals[item.key] : item.goalValue && item.goalValue === null ? '0' : item.goalValue && item.goalValue !== null ? item.goalValue : ''}
/>
</GoalItemContainer>
))
}
</ModalBody>
</Modal>
);
}
}
Ok. I'm mapping over my input fields and getting 4 inputs back. I set the input values and updateTotal fires. This essentially maps over current state, returned values and the input value arrays and constructs 1 array I'm passing into the post called combinedValues. This takes all 4 input values and sends them off for a total. When this comes back, it then injects the original input values back into the 4 inputs, although I've already clicked in another firing the function handleFocus which sets the e.target.value to ''.
I have setState on handleFocus which does correct the issue of being overrode, however, it introduces another issue of setting a new value of '' to the input if I click out versus returning the original value. I hope this helps clarify.

Categories

Resources