I am trying to create editable textboxes for my recipe blog. Here is the post form where users can add recipe instructions by typing in their step, clicking the plus add button and seeing their steps laid out underneath the textarea
. I am able to double click on the textboxes and get an input to update the text, however the I am running into issues with setting the updated value as the new text in the previous space. I will link my github page where you can view the full document if I have not included enough context here
const [instructionsList, setInstructionsList] = useState<Instruction[]>([]);
const [isEditMode, setIsEditMode] = useState(false);
const changeEditMode = () => {
setIsEditMode((prevState) => !prevState);
};
const updateEditMode = () => {
setIsEditMode(false);
};
const handleAddInstruction = (e: React.FormEvent) => {
e.preventDefault();
const instructionListObj = {
id: uniqid(),
index: instructionsList?.length + 1,
content: instructionRef.current!.value,
};
const allInstructions = [...instructionsList, instructionListObj];
setInstructionsList(allInstructions);
instructionRef.current!.value = "";
};
// RETURN SECTION
<section className={`${styles.section5} ${styles.section}`}>
<div className={styles.field}>
<div className={styles["input-field"]}>
<h1 className={styles.label}>instructions</h1>
<textarea
name="instruction"
id="instruction"
className={styles.textarea}
ref={instructionRef}
rows={8}
placeholder="add a step"
></textarea>
<button
onClick={handleAddInstruction}
className={styles.submit}
>
<i className="fa-solid fa-plus"></i>
</button>
</div>
// THIS IS WHERE THE STEPS ARE OUTPUTED
<ul className={styles["instructions-list"]}>
{instructionsList.map(
(instruction: {
id: string;
index: number;
content: string;
}) => (
<li
key={instruction.id}
className={styles["instructions-list-item"]}
onDoubleClick={changeEditMode}
>
<p className={styles.index}>step {instruction.index}</p>
<p className={styles.content}>
{isEditMode ? (
<span>
<input
className={styles.edit}
type="text"
defaultValue={instruction.content}
id="edit"
name="edit"
/>
<button onClick={updateEditMode} type="button">
OK
</button>
<button onClick={changeEditMode} type="button">
X
</button>
</span>
) : (
instruction.content
)}
</p>
</li>
)
)}
</ul>
</div>
</section>
Also I am noticing that when I double click on a textbox to edit the text, all textboxes change into inputs to be edited, where that is not really a problem aesthetically, I think maybe once I solve the updated value issue, that the it will change the value of all the texts when I am just trying to change the one. HELP!!
You're better off creating a component to house your instruction list items that can separate their isEditing
state and whatever draft edits are happening in there.
interface InstructionProps extends Instruction {
update: (instruction: Instruction) => void;
}
const InstructionComponent: React.FC<InstructionProps> = ({
content,
index,
id,
update
}) => {
const [isEditing, setIsEditing] = useState(false);
const [contentDraft, setContentDraft] = useState(content);
const updateContent = () => {
update({ id, index, content: contentDraft });
setIsEditing(false);
};
const toggleEdit = () => setIsEditing(editing => !editing);
return (
<li
className={styles["instructions-list-item"]}
onDoubleClick={toggleEdit}
>
<p className={styles.index}>step {index}</p>
<p className={styles.content}>
{isEditing ? (
<span>
<input
className={styles.edit}
type="text"
defaultValue={content}
value={contentDraft}
onChange={e => setContentDraft(e.target.value)}
id="edit"
name="edit"
/>
<button onClick={updateContent} type="button">
OK
</button>
<button onClick={toggleEdit} type="button">
X
</button>
</span>
) : content
}
</p>
</li>
);
}
const InstructionsContainer: React.FC = () => {
const [instructionsList, setInstructionsList] = useState<Instruction[]>([]);
const updateInstructionsList = (instruction: Instruction) => {
const instructionIndex = instructionsList.findIndex(i => i.id === instruction.id);
if(instructionIndex > -1) {
setInstructionList(instructionsList => {
instructionsList.splice(instructionIndex, 1, instruction);
return [...instructionsList];
});
}
}
return (
{/*...all your other stuff*/}
<ul className={styles["instructions-list"]}>
{instructionsList.map(
(instruction: Instruction) =>
<InstructionComponent
key={id}
update={updateInstructionsList}
{...instruction}
/>
}
</ul>
);
}
This allows you to separate draft state into its own component and only update the actual final state once you're ready to submit. Important thing to note here is that on editing the instructionsList
state we HAVE to return a totally new array to ensure that React understands that state has actually changed.