So I have a fragment factory being passed into a Display component. The fragments have input elements. Inside Display I have an onChange handler that takes the value of the inputs and stores it in contentData[e.target.id]. This works, but switching which fragment is displayed erases their values and I'd rather it didn't. So I'm trying to set their value by passing in the state object to the factory. I'm doing it in this convoluted way to accomodate my testing framework. I need the fragments to be defined outside of any component and passed in to Display as props, and I need them all to share a state object.
My problem is setting the value. I can pass in the state object (contentData), but to make sure the value goes to the right key in the contentData data object I'm trying to hardcode it with the input's id. Except contentData doesn't exist where the fragments are defined, so I get an error about not being able to reference a particular key on an undefined dataObj.
I need to find a way to set the input values to contentData[e.target.id]. Thanks.
File where fragments are defined. Sadly not a component.
const fragments = (onChangeHandler, dataObj) => [
<Fragment key="1">
<input
type="text"
id="screen1_input1"
onChange={onChangeHandler}
value={dataObj['screen1_input1']} // this doesn't work
/>
one
</Fragment>,
<Fragment key="2">
<input
type="text"
id="screen2_input1"
onChange={onChangeHandler}
value={dataObj['screen2_input1']}
/>
two
</Fragment>
]
Display.js
const Display = ({ index, fragments }) => {
const [contentData, setContentData] = useState({})
const onChange = e => {
// set data
const newData = {
...contentData,
[e.target.id]: e.target.value
}
setContentData(newData)
};
return (
<Fragment>{fragments(onChange, contentData)[index]}</Fragment>
);
};
After conversing with you I decided to rework my response. The problem is mostly around the implementation others might provide in these arbitrary fragments.
You've said that you can define what props are passed in without restriction, that helps, what we need to do is take in these nodes that they pass in, and overwrite their onChange with ours, along with the value:
const RecursiveWrapper = props => {
const wrappedChildren = React.Children.map(
props.children,
child => {
if (child.props) {
return React.cloneElement(
child,
{
...child.props,
onChange: props.ids.includes(child.props.id) ? child.props.onChange ? (e) => {
child.props.onChange(e);
props.onChange(e);
} : props.onChange : child.props.onChange,
value: props.contentData[child.props.id] !== undefined ? props.contentData[child.props.id] : child.props.value,
},
child.props.children
? (
<RecursiveWrapper
ids={props.ids}
onChange={props.onChange}
contentData={props.contentData}
>
{child.props.children}
</RecursiveWrapper>
)
: undefined
)
}
return child
}
)
return (
<React.Fragment>
{wrappedChildren}
</React.Fragment>
)
}
const Display = ({ index, fragments, fragmentIDs }) => {
const [contentData, setContentData] = useState(fragmentIDs.reduce((acc, id) => ({
...acc, [id]: '' }), {}));
const onChange = e => {
setContentData({
...contentData,
[e.target.id]: e.target.value
})
};
const newChildren = fragments.map(fragment => <RecursiveWrapper onChange={onChange} ids={fragmentIDs} contentData={contentData}>{fragment}</RecursiveWrapper>);
return newChildren[index];
};
This code outlines the general idea. Here we are treating fragments like it is an array of nodes, not a function that produces them. Then we are taking fragments and mapping over it, and replacing the old nodes with nodes containing our desired props. Then we render them as planned.
Related
How to push element inside useState array AND deleting said object in a dynamic matter using React hooks (useState)?
I'm most likely not googling this issue correctly, but after a lot of research I haven't figured out the issue here, so bare with me on this one.
The situation:
I have a wrapper JSX component which holds my React hook (useState). In this WrapperComponent I have the array state which holds the objects I loop over and generate the child components in the JSX code. I pass down my onChangeUpHandler which gets called every time I want to delete a child component from the array.
Wrapper component:
export const WrapperComponent = ({ component }) => {
// ID for component
const { odmParameter } = component;
const [wrappedComponentsArray, setWrappedComponentsArray] = useState([]);
const deleteChildComponent = (uuid) => {
// Logs to array "before" itsself
console.log(wrappedComponentsArray);
/*
Output: [{"uuid":"acc0d4c-165c-7d70-f8e-d745dd361b5"},
{"uuid":"0ed3cc3-7cd-c647-25db-36ed78b5cbd8"]
*/
setWrappedComponentsArray(prevState => prevState.filter(item => item !== uuid));
// After
console.log(wrappedComponentsArray);
/*
Output: [{"uuid":"acc0d4c-165c-7d70-f8e-d745dd361b5",{"uuid":"0ed3cc3-
7cd-c647-25db-36ed78b5cbd8"]
*/
};
const onChangeUpHandler = (event) => {
const { value } = event;
const { uuid } = event;
switch (value) {
case 'delete':
// This method gets hit
deleteChildComponent(uuid);
break;
default:
break;
}
};
const addOnClick = () => {
const objToAdd = {
// Generate uuid for each component
uuid: uuid(),
onChangeOut: onChangeUpHandler,
};
setWrappedComponentsArray(wrappedComponentsArray => [...wrappedComponentsArray, objToAdd]);
// Have also tried this solution with no success
// setWrappedComponentsArray(wrappedComponentsArray.concat(objToAdd));
};
return (
<>
<div className='page-content'>
{/*Loop over useState array*/}
{
wrappedComponentsArray.length > 0 &&
<div>
{wrappedComponentsArray.map((props) => {
return <div className={'page-item'}>
<ChildComponent {...props} />
</div>;
})
}
</div>
}
{/*Add component btn*/}
{wrappedComponentsArray.length > 0 &&
<div className='page-button-container'>
<ButtonContainer
variant={'secondary'}
label={'Add new component'}
onClick={() => addOnClick()}
/>
</div>
}
</div>
</>
);
};
Child component:
export const ChildComponent = ({ uuid, onChangeOut }) => {
return (
<>
<div className={'row-box-item-wrapper'}>
<div className='row-box-item-input-container row-box-item-header'>
<Button
props={
type: 'delete',
info: 'Deletes the child component',
value: 'Delete',
uuid: uuid,
callback: onChangeOut
}
/>
</div>
<div>
{/* Displays generated uuid in the UI */}
{uuid}
</div>
</div>
</>
)
}
As you can see in my UI my adding logic works as expected (code not showing that the first element in the UI are not showing the delete button):
Here is my problem though:
Say I hit the add button on my WrapperComponent three times and adds three objects in my wrappedComponentsArray gets rendered in the UI via my mapping in the JSX in the WrapperComponent.
Then I hit the delete button on the third component and hit the deleteChildComponent() funtion in my parent component, where I console.log my wrappedComponentsArray from my useState.
The problem then occurs because I get this log:
(2) [{…}, {…}]
even though I know the array has three elements in it, and does not contain the third (and therefore get an undefined, when I try to filter it out, via the UUID key.
How do I solve this issue? Hope my code and explanation makes sense, and sorry if this question has already been posted, which I suspect it has.
You provided bad filter inside deleteChildComponent, rewrite to this:
setWrappedComponentsArray(prevState => prevState.filter(item => item.uuid !== uuid));
You did item !== uuid, instead of item.uuid !== uuid
Please try this, i hope this works
const deleteChildComponent = (uuid) => {
console.log(wrappedComponentsArray);
setWrappedComponentsArray(wrappedComponentsArray.filter(item => item !== uuid));
};
After update
const deleteChildComponent = (uuid) => {
console.log(wrappedComponentsArray);
setWrappedComponentsArray(wrappedComponentsArray.filter(item => item.uuid !== uuid)); // item replaced to item.uuid
};
Huge shoutout to #Jay Vaghasiya for the help.
Thanks to his expertise we managed to find the solution.
First of, I wasn't passing the uuid reference properly. The correct was, when making the objects, and pushing them to the array, we passed the uuid like this:
const addOnClick = () => {
const objToAdd = {
// Generate uuid for each component
uuid: uuid(),
parentOdmParameter: odmParameter,
onChangeOut: function(el) { onChangeUpHandler(el, this.uuid)}
};
setWrappedComponentsArray([...wrappedComponentsArray, objToAdd]);
};
When calling to delete function the function that worked for us, was the following:
const deleteChildComponent = (uuid) => {
setWrappedComponentsArray(item => item.filter(__item => __item.uuid !== uuid)); // item replaced to item.uuid
};
I am using React.js and in render method I have <Range/> component. What I want to do, is to be able to change the value and update the state's array accordingly. This is what I am talking about:
...
{
this.state.tableData.map((i, k) => {
return (
<>
<tr key={k}>
<td>
<div>
<Range
values={[i.s_rate]}
min={MIN}
max={MAX}
onChange={(values) => this.onSliderChangeS(values, k)}
...
So I trigger the onSliderChangeS function, which takes the new value of s_rate as values from the user, and it has to update and show my new this.state.tableData accordingly. I am trying to do it this way:
onSliderChangeS = (values, key) => {
this.setState({
valuesS: values,
tableData[key].s_rate: parseInt(values)
})
}
But this line this.state.tableData[key].s_rate: parseInt(values) doesn't seem to work at all. How can I do that?
setState takes an object with key/value entries. this.state.tableData[key].s_rate itself is not a key name in your state. See if this helps:
onSliderChangeS = (values, key) => {
const newTableData = [...this.state.tableData]
newTableData[key].s_rate = parseInt(values)
this.setState({
valuesS: values,
tableData: newTableData
})
}
Also note the missing comma as pointed out by #FaFa
I want to be able to visit the children <Textfield> of my form <Form> upon submit.
In each child hook object, I also want to trigger a certain function (eg., validate_field). Not sure if this possible in hooks? I do not want to use ref/useRef and forwardRef is a blurred concept to me yet (if that's of any help).
My scenario is the form has been submitted while the user did not touch/update any of the textfields so no errors were collected yet. Upon form submit, I want each child to validate itself based on certain constraints.
I tried looking at useImperativeHandle too but looks like this will not work on props.children?
Updated working code in:
https://stackblitz.com/edit/react-ts-jfbetn
submit_form(evt){
props.children.map(child=>{
// hypothetical method i would like to trigger.
// this is what i want to achieve
child.validate_field() // this will return "is not a function" error
})
}
<Form onSubmit={(e)=>submit_form(e)}
<Textfield validations={['email']}>
<Textfield />
<Textfield />
</Form>
Form.js
function submit_form(event){
event.preventDefault();
if(props.onSubmit){
props.onSubmit()
}
}
export default function Form(props){
return (
<form onSubmit={(e)=>submit_form(e)}>
{props.children}
</form>
)
}
So the Textfield would look like this
…
const [value, setValue] = useState(null);
const [errors, setErrors) = useState([]);
function validate_field(){
let errors = []; // reset the error list
props.validations.map(validation => {
if(validation === 'email'){
if(!some_email_format_validator(value)){
errors.push('Invalid email format')
}
}
// other validations (eg., length, allowed characters, etc)
})
setErrors(errors)
}
export default function Textfield(props){
render (
<input onChange={(evt)=>setValue(evt.target.value)} />
{
errors.length > 0
? errors.map(error => {
return (
<span style={{color:'red'}}>{error}</span>
)
})
: null
}
)
}
I would recommend moving your validation logic up to the Form component and making your inputs controlled. This way you can manage the form state in the parent of the input fields and passing in their values and onChange function by mapping over your children with React.cloneElement.
I don't believe what you're trying to do will work because you are trying to map over the children prop which is not the same as mapping over say an array of instantiated child elements. That is to say they don't have state, so calling any method on them wouldn't be able to give you what you wanted.
You could use a complicated system of refs to keep the state in your child input elements, but I really don't recommend doing that as it would get hairy very fast and you can just solve the issue by moving state up to the parent.
simplified code with parent state:
const Form = ({ children }) => {
const [formState, setFormState] = useState(children.reduce((prev, curr) => ({ ...prev, [curr.inputId]: '' }), {}));
const validate = (inputValue, validator) => {}
const onSubmit = () => {
Object.entries(formState).forEach(([inputId, inputValue]) => {
validate(
inputValue,
children.filter(c => c.inputId === inputId)[0].validator
)
})
}
const setFieldValue = (value, inputId) => {
setFormState({ ...formState, [inputId]: value });
};
const childrenWithValues = children.map((child) =>
React.cloneElement(child, {
value: formState[child.inputId],
onChange: (e) => {
setFieldValue(e.target.value, child.inputId);
},
}),
);
return (
<form onSubmit={onSubmit}>
{...childrenWithValues}
</form>
)
};
const App = () =>
<Form>
<MyInput validator="email" inputId="foo"/>
<MyInput validator="email" inputId="foo"/>
<MyInput validator="password" inputId="foo"/>
</Form>
I still don't love passing in the validator as a prop to the child, as pulling that out of filtered children is kinda jank. Might want to consider some sort of state management or pre-determined input list.
Got in a rather troublesome situation
I have an array of objects
[
{
"title":"placeholder",
"text":"placeholder"
},
{
"title":"test",
"text":"placeholder"
},
{
"title":"javascript",
"text":"placeholder"
}
]
I am displaying them in a div,but thats not important
I got an input field which users should type in title's and as they type the array should only show matching object.
Inputing java would show the javascript titled object
I need to somehow change the array so it doesnt display anything but the entered title and if the input is empty shows the whole array
I am using React but i can only use hooks
So i copy the json
var [arrayOfObjects, setArray] = useState(Json)
the Json is imported from a local file
arrayOfNotes is the array that i need to change
pointing out so its easier to understand
ty in advance
The array filter method is what you're looking for.
Here's what your component might looks like.
const List = ({ data }) => {
const [value, setValue] = useState('')
return (
<div>
<input
type="text"
value={value}
onChange={e => setValue(e.target.value)}
/>
{data
.filter(item => {
if (!value) return true
if (item.title.includes(value) || item.text.includes(value)) {
return true
}
})
.map(item => (
<div>
<h1>{item.title}</h1>
<p>{item.text}</p>
</div>
))
}
</div>
)
}
And you pass your json data to that component
<List data={Json} />
Here's a working example of the above code
You didn't share your component so I'll assume you know how to get the input value and call the corresponding variable input, besides you have your original array, from your example I judge it is called Json.
Then you can filter your value as follows:
const [arrayOfObjects, setArray] = useState(Json);
const filteredArray = input ?
originalArray.filter(item => item.title.includes(input) :
originalArray;
So that later you can render the filteredArray as follows:
<ul>
{filteredArray.map(item => (<li>{item.title}</li>))}
</ul>
Below is the dynamic form which I created. It works fine. but Inside "a" tag if i add some child element to "a" tag, onClick event on "a" tag does not execute properly and pass proper name.
import React, { Component } from "react";
import "./stylesheet/style.css";
export default class Main extends Component {
state = {
skills: [""]
};
dynamicInputHandler = (e, index) => {
var targetName = e.target.name;
console.log(targetName);
var values = [...this.state[targetName]];
values[index] = e.target.value;
this.setState({
[targetName]: values
});
};
addHandler = (e, index) => {
e.preventDefault();
let targetName = e.target.name;
let values = [...this.state[targetName]];
values.push("");
this.setState({
[targetName]: values
});
};
removeHandler = (e, index) => {
e.preventDefault();
let targetName = e.target.name;
console.log(e.target.name);
let values = [...this.state[targetName]];
values.splice(index, 1);
this.setState({
[targetName]: values
});
};
render() {
return (
<div>
<form className="form">
{this.state.skills.map((value, index) => {
return (
<div className="input-row row">
<div className="dynamic-input">
<input
type="text"
placeholder="Enter skill"
name="skills"
onChange={e => {
this.dynamicInputHandler(e, index);
}}
value={this.state.skills[index]}
/>
</div>
<div>
<span>
<a
name="skills"
className="close"
onClick={e => {
this.removeHandler(e, index);
}}
>
Remove
</a>
</span>
</div>
</div>
);
})}
<button
name="skills"
onClick={e => {
this.addHandler(e);
}}
>
Add
</button>
</form>
{this.state.skills[0]}
</div>
);
}
}
i want to add Icon inside "a" tag , after adding icon tag, forms breaks and gives error "TypeError: Invalid attempt to spread non-iterable instance"
This works -
<span>
<a
name="skills"
className="close"
onClick={e => {
this.removeHandler(e, index);
}}
>
Remove
</a>
</span>
This does not (after adding icon inside a tag)
<span>
<a
name="skills"
className="close"
onClick={e => {
this.removeHandler(e, index);
}}
>
<i>Remove</i>
</a>
</span>
Inside removeHandler let targetName = e.target.parentElement.name;. When you wrap Remove in another tag, the event target is now the i tag, for which name is undefined.
I think this mixture of DOM manipulation (getting target of an event) is okay, but in React, where data is preferred over DOM values, you could work entirely on the component state and touch the DOM element values only once in the input where the skill value is displayed. Like this:
class Main extends Component {
state = {
skills: [],
lastSkillAdded: 0
};
appendEmptySkill = () => {
this.setState(prevState => ({
...prevState,
skills: [...prevState.skills, { id: lastSkillAdded, value: "" }],
lastSkillAdded: prevState.lastSkillAdded + 1
}));
};
updateSkillById = (id, value) => {
this.setState(prevState => ({
...prevState,
skills: skills.map(skill =>
skill.id !== id
? skill
: {
id,
value
}
)
}));
};
removeSkillById = id => {
this.setState(prevState => ({
...prevState,
skills: prevState.skills.filter(skill => skill.id !== id)
}));
};
render() {
return (
<form>
{this.state.skills.map((skill, index) => (
<div key={skill.id}>
<input
type="text"
value={skill.value}
onChange={e => this.updateSkillById(skill.id, e.target.value)}
/>
<button onClick={() => this.removeSkillById(skill.id)}>
Remove
</button>
</div>
))}
<button onClick={() => this.appendEmptySkill()}>Add</button>
</form>
);
}
}
Let's deconstruct what's going on there.
First, the lastSkillAdded. This is a controversial thing, and I guess someone will correct me if I'm wrong, but
when we iterate over an array to render, say, list items, it's recommended that we provide key prop that represents the item key,
and the value of key prop is not recommended to be the index of the array element.
So we introduce an artificial counter that only goes up and never goes down, thus never repeating. That's a minor improvement though.
Next, we have the skills property of component state. It is where all the values are going to be stored. We agree that each skill is represented by an object of two properties: id and value. The id property is for React to mark array items property and optimize reconciliation; the value is actual text value of a skill.
In addition, we have three methods to work on the list of skills that are represented by three component methods:
appendEmptySkill to add an empty skill to the end of the state.skills array,
updateSkillById to update the value of a skill knowing id and new value of it,
and removeSkillById, to just remove the skill by its id from state.skills.
Let's walk through each of them.
The first one is where we just append a new empty skill:
appendEmptySkill = () => {
this.setState(prevState => ({
...prevState,
skills: [...prevState.skills, { id: lastSkillAdded, value: "" }],
lastSkillAdded: prevState.lastSkillAdded + 1
}));
};
Because appending a new skill never depends on any input values, this method takes zero arguments. It updates the skill property of component state:
[...prevState.skills, { id: lastSkillAdded, value: '' }]
which returns a new array with always the same empty skill. There, we also assign the value to id property of a skill to our unique counter.
The next one is a bit more interesting:
updateSkillById = (id, value) => {
this.setState(prevState => ({
...prevState,
skills: skills.map(skill =>
skill.id !== id
? skill
: {
id,
value
}
)
}));
};
We agree that in order to change a skill, we need to know its id and the new value. So our method needs to receive these two values. We then map through the skills, find the one with the right id, and change its value. A pattern
(id, value) => array.map(item => item.id !== id ? item : ({ ...item, value }));
is, I believe, a pretty common one and is useful on many different occasions.
Note that when we update a skill, we don't bother incrementing our skill id counter. Because we're not adding one, it makes sense to just keep it at its original value.
And finally, removing a skill:
removeSkillById = id => {
this.setState(prevState => ({
...prevState,
skills: prevState.skills.filter(skill => skill.id !== id)
}));
};
Here, we only need to know the id of a skill to remove it from the list of skills. We filter our state.skills array and remove the one skill that matches the id. This is also a pretty common pattern.
Finally, in render, four things are happening:
existing skills are iterated over, and for each skill, input and "Remove" button are rendered,
input listens to change event and refers to EventTarget#value attribute in updateSkillById function call, because that's the only way to feed data from DOM back into React, and
the "Remove" buttons refers to skill id and calls removeSkillById,
and outside the loop, the "Add" button listens to click events and calls appendEmptySkill whenever it happens without any arguments, because we don't need any.
So as you can see, this way you're in total control of the render and state updates and never depend on DOM structure. My general advice is, if you find yourself in a situation when DOM structure dictates the way you manage component or app state, just think about a way to separate DOM from state management. So hope this answer helps you solve the issue you're having. Cheers!