I have a component call ExpenseList.js which does look like below. But my problem is when I tried to edit item and click save, setting isEditable inside "Save" button event handler does not trigger re-render.
import { useState } from "react";
import { useExpenses, useExpenseDispatch } from "./BudgetContext.js";
export default function ExpenseList() {
const expenses = useExpenses();
return (
<ul>
{expenses.map((expense) => (
<li key={expense.id}>
<Expense expense={expense} />
</li>
))}
</ul>
);
}
function Expense({ expense }) {
const [isEditing, setIsEditing] = useState(false);
const dispatch = useExpenseDispatch();
let content;
if (isEditing) {
content = (
<>
<input
value={expense.description}
onChange={(e) => {
dispatch({
type: "changed",
expense: {
...expense,
description: e.target.value
}
});
}}
/>
<input
value={expense.amount}
onChange={(e) => {
dispatch({
type: "changed",
expense: {
...expense,
amount: e.target.value
}
});
}}
/>
<button onClick={() => setIsEditing(false)}>Save</button>
</>
);
} else
content = (
<>
<span>{expense.description}</span> : <span>{expense.amount}</span>
<button onClick={() => setIsEditing(true)}>Edit</button>
</>
);
return (
<label>
{content}
<button
onClick={() => {
dispatch({
type: "deleted",
id: expense.id
});
}}
>
Delete
</button>
</label>
);
}
I was dabbling with this for hours, I think extra pair of eyes could point out what is going wrong?
Sandbox: https://codesandbox.io/s/clever-keller-l5z42e?file=%2FExpenseList.js%3A0-1614
Documentation reference
Content model:
Phrasing content, but with no descendant labelable elements unless it is the element's labeled control, and no descendant label elements.
As mentioned above, label has 2 different labelable elements unless it is the element's labeled control. When you are in edit mode, you have 3 different labelable elements (input-description, input-amount and button-save) which causes problems with event propagation.
But when you are not in edit mode, it just has 1 labelable element which is the edit button and hence, it works.
For solving your issue, you can swap the label at the root with something like div and then use labels explicitly for each of the inputs in content.
function Expense({ expense }) {
const [isEditing, setIsEditing] = useState(false);
let content;
if (isEditing) {
content = (
<>
<label>
Description:
<input
value={expense.description}
onChange={...}
/>
</label>
<label>
Amount:
<input
value={expense.amount}
onChange={...}
/>
</label>
<button onClick={() => setIsEditing(false)}>Save</button>
</>
);
} else
content = (
<>
<button onClick={() => setIsEditing(true)}>Edit</button>
</>
);
return (
<div>
{content}
<button>Delete</button>
</div>
);
}
Related
I have a list of users and I want to display in another component on the same page the user data in input fields for every user that I click on.
When no user is selected, I want the component to just render some text and a button to add a user. When the button is clicked the component renders the form with empty input fields so that we can add a new user.
I tried the following, but it's just showing the data for the first one I click on. It's not updating.
The main page:
const index = (props) => {
const [selectedUser, setSelectedUser] = useState(null);
const [state, setState] = useState("Index");
const onChange = (item) => {
setState("Edit");
setSelectedUser(item);
};
const onClick = (e, item) => {
if (e.type === "click" && e.clientX !== 0 && e.clientY !== 0) {
onChange(item);
} else {
console.log('prevented "onClick" on keypress');
}
};
const renderComponent = () => {
switch (state) {
case "Index":
return (
<>
<div className="btn" onClick={(e) => setState("Edit")}>
+ New Staff
</div>
<img src="/storage/illustrations/collaboration.svg" />
</>
);
case "Edit":
return (
<div>
<StaffForm profile={selectedUser} />
</div>
);
}
};
return (
<>
<div>
<div>
<h1>Staff</h1>
</div>
<div>
<div>
{profiles.map((item, index) => {
return (
<div key={index} onClick={(e) => onClick(e, item)}>
<input
type={"radio"}
name={"staff"}
checked={state === item}
onChange={(e) => onChange(item)}
/>
<span>{item.user.name}</span>
</div>
);
})}
</div>
<div>{renderComponent()}</div>
</div>
</div>
</>
);
};
The Staff Form Component:
const StaffForm = ({ profile }) => {
const { data, setData, post, processing, errors, reset } = useForm({
email: profile ? profile.user.email : "",
name: profile ? profile.user.name : "",
phone_number: profile ? profile.user.phone_number : "",
avatar: profile ? profile.user.avatar : "",
});
const [file, setFile] = useState(data.avatar);
const handleImageUpload = (e) => {
setFile(URL.createObjectURL(e.target.files[0]));
setData("avatar", e.target.files[0]);
};
const onHandleChange = (event) => {
setData(
event.target.name,
event.target.type === "checkbox"
? event.target.checked
: event.target.value
);
};
return (
<div>
<ImageUpload
name={data.name}
file={file}
handleImageUpload={handleImageUpload}
/>
<TextInput
type="text"
name="name"
value={data.name}
autoComplete="name"
isFocused={true}
onChange={onHandleChange}
placeholder={t("Name")}
required
/>
<TextInput
type="text"
name="phone_number"
value={data.phone_number}
autoComplete="phone_number"
placeholder={t("Phone Number")}
onChange={onHandleChange}
required
/>
<TextInput
type="email"
name="email"
value={data.email}
autoComplete="email"
onChange={onHandleChange}
placeholder={t("Email")}
required
/>
</div>
);
};
First of all something you should avoid is the renderComponent() call.Check here the first mistake mentioned in this video. This will most likely fix your problem but even if it doesn't the video explains why it should not be used.
Something else that caught my eye(possibly unrelated to your question but good to know) is the onChange function. When two pieces of state change together it is a potential source of problems, check out this article on when to use the useReducer hook.
Also be careful with naming React Components, they need to be capital case, this question contains appropriate answers explaining it.
(To only solve your problem stick to no.1 of this list, there are some improvements i'd do here overall for code quality and beauty, msg me for more details)
I need to access a method handleCancelEdit() defined in parent component. But, the matter here is that every child component will have its own cancelEdit state. Now, what is happening is, if I call handleCancelEdit() from one child component, every other of the same child components is taking the state and updating themselves(the method is not completely defined yet). That's why, I have defined the cancelEdit state in the child component, thinking that it belongs to this child component only.
Now, how do I make the handleCancelEdit() method make changes to the only child component which called it?
The parent:
function Parent() {
const handleCancelEdit = () => {
setCancelEdit(!cancelEdit); // defined in child component
setEdit(!edit); // defined in child component
...
};
return (
<div>
<ChildComponent
fieldName={"Email"}
value={email}
inputType={"text"}
placeHolder={"Enter email"}
name={"email"}
on_change={(e)=>setEmail(e.target.value)}
on_click={handleUserEmail}
/>
<ChildComponent
fieldName={"About"}
value={about}
inputType={"text"}
placeHolder={"Enter some details about yourself"}
name={"about"}
on_change={(e)=>setAbout(e.target.value)}
on_click={handleUserAbout}
/>
</div>
);
}
Child component:
function ChildComponent({fieldName, value, inputType, placeHolder, name, on_change, on_click}) {
const [ edit, setEdit ] = useState(false);
const [ cancelEdit, setCancelEdit ] = useState(false)
const isEdit = edit;
return (
<p>{fieldName}: {value === ''? (
<span>
<input type={inputType} placeholder={placeHolder}
name={name} onChange={on_change}
/>
<button type="submit" onClick={on_click}>Add</button>
</span>
) : ( !isEdit ? (<span> {value} <button onClick={e=>setEdit(!edit)}>Edit</button></span>) :
(<span>
<input type={inputType} value={value}
name={name} onChange={on_change}
/>
<button type="submit" onClick={on_click}>Save</button>
<button type="submit" onClick={handleCancelEdit}>Cancel</button>
</span>)
)}
</p>
);
};
I hope it could make it understandable that one child component should not make others to update. Now, how do I do it in this scenario?
EDIT
After making changes according to Linda Paiste:
The input field in the child component is not working even though the onChange in both parent and child is correct!
It is always more logical to pass state and data down rather than up. If the Parent needs to interact with the edit state then that state should live in the parent. Of course we want independent edit states for each child, so the parent can't just have one boolean. It needs a boolean for each child. I recommend an object keyed by the name property of the field.
In ChildComponent, we move isEdit and setEdit to props. handleCancelEdit is just () => setEdit(false) (does it also need to clear the state set by onChange?).
function ChildComponent({fieldName, value, inputType, placeHolder, name, onChange, onSubmit, isEdit, setEdit}) {
return (
<p>{fieldName}: {value === ''? (
<span>
<input type={inputType} placeholder={placeHolder}
name={name} onChange={onChange}
/>
<button type="submit" onClick={onSubmit}>Add</button>
</span>
) : ( !isEdit ? (<span> {value} <button onClick={() =>setEdit(true)}>Edit</button></span>) :
(<span>
<input type={inputType} value={value}
name={name} onChange={onChange}
/>
<button type="submit" onClick={onSubmit}>Save</button>
<button type="submit" onClick={() => setEdit(false)}>Cancel</button>
</span>)
)}
</p>
);
};
In Parent, we need to store those isEdit states and also create a setEdit function for each field.
function Parent() {
const [isEditFields, setIsEditFields] = useState({});
const handleSetEdit = (name, isEdit) => {
setIsEditFields((prev) => ({
...prev,
[name]: isEdit
}));
};
/* ... */
return (
<div>
<ChildComponent
fieldName={"Email"}
value={email}
inputType={"text"}
placeHolder={"Enter email"}
name={"email"}
onChange={(e) => setEmail(e.target.value)}
onSubmit={handleUserEmail}
isEdit={isEditFields.email}
setEdit={(isEdit) => handleSetEdit("email", isEdit)}
/>
<ChildComponent
fieldName={"About"}
value={about}
inputType={"text"}
placeHolder={"Enter some details about yourself"}
name={"about"}
onChange={(e) => setAbout(e.target.value)}
onSubmit={handleUserAbout}
isEdit={isEditFields.about}
setEdit={(isEdit) => handleSetEdit("about", isEdit)}
/>
</div>
);
}
You can clean up a lot of repeated code by storing the values as a single state rather than individual useState hooks. Now 5 of the props can be generated automatically just from the name.
function Parent() {
const [isEditFields, setIsEditFields] = useState({});
const [values, setValues] = useState({
about: '',
email: ''
});
const getProps = (name) => ({
name,
value: values[name],
onChange: (e) => setValues(prev => ({
...prev,
[name]: e.target.value
})),
isEdit: isEditFields[name],
setEdit: (isEdit) => setIsEditFields(prev => ({
...prev,
[name]: isEdit
}))
});
const handleUserEmail = console.log // placeholder
const handleUserAbout = console.log // placeholder
return (
<div>
<ChildComponent
fieldName={"Email"}
inputType={"text"}
placeHolder={"Enter email"}
onSubmit={handleUserEmail}
{...getProps("email")}
/>
<ChildComponent
fieldName={"About"}
inputType={"text"}
placeHolder={"Enter some details about yourself"}
onSubmit={handleUserAbout}
{...getProps("about")}
/>
</div>
);
}
So, the problem I've faced is that when I click delete button at any index it just deletes last element
for example, if I press first delete button, it should remove first input and the delete button, but what it does is that it deletes last element only... What could be wrong?
function App() {
const [names, setNames] = React.useState([
"First",
"Second",
"third",
"fourth"
]);
const onChange= (index: number, editedName: string) => {
const mutatedNames = [...names];
mutatedNames[index] = editedName;
setNames(mutatedNames);
};
function onDelete(index: number) {
const nameArr = [...names];
nameArr.splice(index, 1);
setNames(nameArr);
}
return (
<div>
{names.map((name, index) => (
<ChildComponent
key={index}
name={name}
index={index}
onChange={onChange}
onDelete={onDelete}
/>
))}
</div>
);
}
const Child = React.memo(
({ name, index, onChange, onDelete }) => {
return (
<div>
<input
onChange={(event) => onChange(index, event.target.value)}
/>
<button onClick={() => onDelete(index)}>delete</button>
</div>
);
}
);
You are using a partially controlled input, this is almost never a good idea.
Make it fully controlled like so:
<input
value={name}
onChange={(event) => onChange(index, event.target.value)} />
I suggest you read the official guidelines about Forms and Controlled Components and the article about the uncontrolled scenario.
I have a dynamic form. My input fields belong to different groups. I want to figure out a way to display them group-wise.
My state through which form input fields are generated is:
random:
{
name: "emp1", group: "employee",
name: "emp2", group: "employee",
name: "emp3", group: "employee",
name: "man1", group: "manager",
name: "man2", group: "manager"
}
My dynamic form is as follows:
const Form = (props) => {
return (
<div>
{props.items.map(item => (
name={item.key_name}
value={item.key_value}
onChange={e => props.handleChange(e)}
/>
)
)}
<button onClick={() => props.handleSubmit()} >
Submit
</button>
</div>
)
}
export default Form
right now my fields are generated side by side in a horizontal line
But I want to display them group wise; so something like this:
Employee: (3 input fields)
Manager: (2 input fields)
You just need to split your items by their group, then render them. I would make the group a component so it handles rendering the label and also the inputs
const Form = props => {
const itemsByGroup = {};
props.items.forEach(item => {
if (!itemsByGroup[item.group]) {
itemsByGroup[item.group] = [];
}
itemsByGroup[item.group].push(item);
});
return (
<div>
{Object.keys(itemsByGroup).map(groupName => (
<FormGroup
label={groupName}
fields={itemsByGroup[groupName]}
handleChange={props.handleChange}
key={groupName}
/>
))}
<button onClick={() => props.handleSubmit()}>Submit</button>
</div>
);
};
const FormGroup = ({ label, fields, handleChange }) => (
<div>
<label>{label}:</label>
{fields.map(field => (
<input
name={field.key_name}
value={field.key_value}
onChange={e => handleChange(e)}
/>
))}
</div>
);
See a live example here to play with
Edit
if you have to put it in one component you can just do this
const Form = props => {
const itemsByGroup = {};
props.items.forEach(item => {
if (!itemsByGroup[item.group]) {
itemsByGroup[item.group] = [];
}
itemsByGroup[item.group].push(item);
});
return (
<div>
{Object.keys(itemsByGroup).map(groupName => (
<div>
<label>{label}:</label>
{fields.map(field => (
<input
name={field.key_name}
value={field.key_value}
onChange={e => handleChange(e)}
/>
))}
</div>
))}
<button onClick={() => props.handleSubmit()}>Submit</button>
</div>
);
};
The use case is there will be add topic button which when clicked should show a form for adding the topic. When user fills the topic form and hits the save button, that topic should be shown in the input box with edit button instead of add. There can be multiple topics. For example, if I have 4 topics already or saved them after adding then they should be displayed with edit button. The way I am doing is not even triggering handleChange.
I have created a sandbox for this and here it is
https://codesandbox.io/s/koqqvz2307
The code
class FieldArray extends React.Component {
state = {
topics: [],
topic: ""
};
handleChange = e => {
console.log("handleChange", e);
this.setState({ topic: { ...this.state.topic, topic: e.target.value } });
};
handleSubmit = e => {
e.preventDefault();
console.log("state of topics array with multiple topics");
};
render() {
return (
<div>
<FieldArrayForm
topics={this.state.topics}
topic={this.state.topic}
handleChange={this.handleChange}
handleSubmit={this.handleSubmit}
/>
</div>
);
}
}
export default FieldArray;
const renderField = ({ input, label, type, meta: { touched, error } }) => (
<div>
<label>{label}</label>
<div>
<input {...input} type={type} placeholder={label} />
{touched && error && <span>{error}</span>}
</div>
</div>
);
const renderTopics = ({
fields,
meta: { error },
handleChange,
handleSubmit,
topic
}) => (
<ul>
<li>
<button type="button" onClick={() => fields.push()}>
Add Topic
</button>
</li>
{fields.map((topicName, index) => (
<li key={index}>
<span>
<Field
name={topicName}
type="text"
onChange={handleChange}
component={renderField}
label={`Topic #${index + 1}`}
/>
<span>
<button
type="button"
title="Remove Hobby"
onClick={() => fields.remove(index)}
>
Remove
</button>
{topic ? (
<button type="button" title="Add" onSubmit={handleSubmit}>
Edit
</button>
) : (
<button type="button" title="Add" onSubmit={handleSubmit}>
Add
</button>
)}
</span>
</span>
</li>
))}
{error && <li className="error">{error}</li>}
</ul>
);
const FieldArraysForm = props => {
const { handleSubmit, pristine, reset, submitting } = props;
return (
<form onSubmit={handleSubmit}>
<FieldArray name="topic" component={renderTopics} />
</form>
);
};
export default reduxForm({
form: "fieldArrays", // a unique identifier for this form
validate
})(FieldArraysForm);
How do i save and show multiple topics when using redux-form? I tried to take the concept from fieldarray but i could not do it yet.
Your handleChange is undefined, and this is why your function isn't being called.
If you are willing that renderTopics receive a handleChange function, you should pass the handleChange prop to the FieldArray component (according to redux-form docs):
const FieldArraysForm = props => {
const { handleChange, handleSubmit, pristine, reset, submitting } = props;
return (
<form onSubmit={handleSubmit}>
<FieldArray name="topic" component={renderTopics} handleChange={handleChange} />
</form>
);
};
Alternatively, you can simply pass all props from FieldArraysForm to the FieldArray component:
const FieldArraysForm = props => (
<form onSubmit={handleSubmit}>
<FieldArray name="topic" component={renderTopics} {...props} />
</form>
);