Search code examples
javascriptreactjstypescript

How to create editable textboxes


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!!


Solution

  • 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.