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=/ExpenseList.js:0-1614
CodePudding user response:
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 input
s 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>
);
}