이어서 투두리스트 수정 기능을 만들어보자.
완성된 코드이다.
// TodoItem.js
import styles from "./TodoItem.module.css";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faTrashCan, faPenToSquare } from "@fortawesome/free-regular-svg-icons";
import { faCheck, faXmark} from "@fortawesome/free-solid-svg-icons";
import { useState, useRef } from "react";
const TodoItem = (props) => {
const editedText = useRef(null);
const [isEditClicked, setIsEditClicked] = useState(false);
const [isDeleteClicked, setIsDeleteClicked] = useState(false);
const [isChecked, setIsChecked] = useState(false);
const [updatedText, setUpdatedText] = useState('');
const submitEditedContent = () => {
if (updatedText === '') {
setIsEditClicked(false);
return;
}
props.submitEditedContent(updatedText, props.todo.id);
setUpdatedText('');
setIsEditClicked(false);
}
const openEdit = () => {
setIsEditClicked(true);
editedText.current.focus();
}
const openDelete = () => {
setIsDeleteClicked(true);
};
const cancelEdit = () => {
setUpdatedText('');
setIsEditClicked(false);
}
const cancelDelete = () => {
setIsDeleteClicked(false);
};
const onDelete = (id) => {
props.onDelete(id);
};
return (
<div className={styles.textAndDeteteBtn}>
<div className={styles.checkboxAndText}>
<label>
<input type="checkbox" onClick={() => setIsChecked(!isChecked)} />
<div>
<FontAwesomeIcon icon={faCheck} color="#1a202c" className={styles.checkIcon} />
</div>
</label>
{isEditClicked
? (
<div className={isChecked? `${styles.text} ${styles.checked}` : `${styles.text}`}>
<input
value={updatedText}
onChange={(e) => setUpdatedText(e.target.value)}
ref={editedText}
/>
</div>
) : (
<div className={isChecked? `${styles.text} ${styles.checked}` : `${styles.text}`}>
{props.todo.text}
</div>)
}
</div>
<div>
<div className={styles.editBtnAndDeleteBtn}>
{(isEditClicked && !isDeleteClicked) ? (
<div>
<button className={styles.submitIcon} onClick={submitEditedContent}>
<FontAwesomeIcon icon={faCheck} size="2x" color="white" />
</button>
<button className={styles.cancelIcon} onClick={cancelEdit}>
<FontAwesomeIcon icon={faXmark} size="2x" color="white" />
</button>
</div>
) : ((!isEditClicked && isDeleteClicked) ? (
<div>
<button
className={styles.submitIcon}
onClick={() => onDelete(props.todo.id)}
>
<FontAwesomeIcon icon={faCheck} size="2x" color="white" />
</button>
<button className={styles.cancelIcon} onClick={cancelDelete}>
<FontAwesomeIcon icon={faXmark} size="2x" color="white" />
</button>
</div>
) : (
<div>
<button className={styles.editIcon} onClick={openEdit}>
<FontAwesomeIcon icon={faPenToSquare} size="2x" color="white" />
</button>
<button className={styles.delteIcon} onClick={openDelete}>
<FontAwesomeIcon icon={faTrashCan} size="2x" color="white" />
</button>
</div>
))}
</div>
</div>
</div>
);
};
export default TodoItem;
// Todo.js
import { useState } from 'react';
import TodoInput from '../TodoInput/TodoInput';
import TodoList from '../TodoList/TodoList';
import styles from './Todo.module.css';
const Todo = () => {
const [displayInputs, setDisplayInputs] = useState([]);
const submitEditedContent = (updatedText, id) => {
setDisplayInputs(displayInputs.map(todo => {
if (todo.id === id) {
return {
...todo,
text: updatedText
};
}
return todo;
}))
}
const onSaveGoal = (goal) => {
setDisplayInputs([...displayInputs, goal]);
}
const onDelete = (id) => {
setDisplayInputs(displayInputs.filter((todo) => todo.id !== id));
}
return (
<div className={styles.container}>
<h1 className={styles.title}>목표를 이루기 위해 <br/>해야 할 것들을 적어주세요!</h1>
<TodoInput onSaveGoal={onSaveGoal} />
<TodoList item={displayInputs} onDelete={onDelete} submitEditedContent={submitEditedContent} />
</div>
);
};
export default Todo;
먼저 UI를 만들기 위해 삭제 버튼 왼쪽에 수정 버튼을 추가해줬다.
// TodoItem.js
<button className={styles.editIcon} onClick={openEdit}>
<FontAwesomeIcon icon={faPenToSquare} size="2x" color="white" />
</button>
그리고 flexbox를 설정해서 이렇게 가로로 놓이게 했다.
맨 처음에는 투두의 오른쪽에 이렇게 수정버튼/삭제버튼이 있었다.
이때 수정버튼을 누르면 수정버튼과 삭제버튼이 사라지고, 수정확인/수정취소 버튼이 나타나게 하고 싶었다.(동일하게 삭제버튼을 누르면 수정버튼과 삭제버튼이 사라지고 삭제확인/삭제취소 버튼이 나타나도록)
처음에는 이렇게 됐다. 하지만 내가 원하는 것은 수정버튼을 눌렀을 때 삭제에 관련한 버튼들이 모두 사라지고, 삭제 버튼을 누르면 수정 버튼에 관한 버튼들이 모두 사라지는 것이다.
그래서 일단 그렇게 하는 것에 성공했다! 하지만 이중 삼항 연산자를 이용해서....😂
이중 삼항 연산자를 이용하여 구현하려고 한 것을 의사코드로 나타내면 다음과 같다.
(수정 버튼이 클릭되고 && 삭제 버튼이 클릭되지 않으면)
? 수정확인/수정취소 버튼을 표시하고
: (
(수정버튼이 클릭되지 않고 && 삭제 버튼이 클릭되면)
? 삭제확인/삭제취소 버튼을 표시하고
: 아무것도 클릭되지 않으면(평소에) 수정 버튼과 삭제 버튼을 표시한다
)
그리고 코드로 구현하면 아래와 같다.
// TodoItem.js
{(isEditClicked && !isDeleteClicked) ? (
<div>
<button className={styles.submitIcon} onClick={submitEditedContent}>
<FontAwesomeIcon icon={faCheck} size="2x" color="white" />
</button>
<button className={styles.cancelIcon} onClick={cancelEdit}>
<FontAwesomeIcon icon={faXmark} size="2x" color="white" />
</button>
</div>
) : ((!isEditClicked && isDeleteClicked) ? (
<div>
<button
className={styles.submitIcon}
onClick={() => onDelete(props.todo.id)}
>
<FontAwesomeIcon icon={faCheck} size="2x" color="white" />
</button>
<button className={styles.cancelIcon} onClick={cancelDelete}>
<FontAwesomeIcon icon={faXmark} size="2x" color="white" />
</button>
</div>
) : (
<div>
<button className={styles.editIcon} onClick={openEdit}>
<FontAwesomeIcon icon={faPenToSquare} size="2x" color="white" />
</button>
<button className={styles.delteIcon} onClick={openDelete}>
<FontAwesomeIcon icon={faTrashCan} size="2x" color="white" />
</button>
</div>
))}
수정하는 걸 어떻게 표현할까 고민했는데, 따로 모달로 띄워서 수정 후 투두에 반영하는 방법은 클릭을 여러번 해야해서 불편할 것 같았다.
그래서 수정 버튼을 누르면 수정하려는 투두 자체가 input창으로 바뀌어서 그곳에서 수정을 할 수 있게끔 하기로 했다.
먼저 완성된 코드는 아래와 같다.
import styles from "./TodoItem.module.css";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faTrashCan, faPenToSquare } from "@fortawesome/free-regular-svg-icons";
import { faCheck, faXmark} from "@fortawesome/free-solid-svg-icons";
import { useState, useRef } from "react";
const TodoItem = (props) => {
const editedText = useRef(null);
const [isEditClicked, setIsEditClicked] = useState(false);
const [isDeleteClicked, setIsDeleteClicked] = useState(false);
const [isChecked, setIsChecked] = useState(false);
const [updatedText, setUpdatedText] = useState('');
const submitEditedContent = () => {
if (updatedText === '') {
setIsEditClicked(false);
return;
}
props.submitEditedContent(updatedText, props.todo.id);
setUpdatedText('');
setIsEditClicked(false);
}
const openEdit = () => {
setIsEditClicked(true);
editedText.current.focus();
}
const openDelete = () => {
setIsDeleteClicked(true);
};
const cancelEdit = () => {
setUpdatedText('');
setIsEditClicked(false);
}
const cancelDelete = () => {
setIsDeleteClicked(false);
};
const onDelete = (id) => {
props.onDelete(id);
};
return (
<div className={styles.textAndDeteteBtn}>
<div className={styles.checkboxAndText}>
<label>
<input type="checkbox" onClick={() => setIsChecked(!isChecked)} />
<div>
<FontAwesomeIcon icon={faCheck} color="#1a202c" className={styles.checkIcon} />
</div>
</label>
{isEditClicked
? (
<div className={isChecked? `${styles.text} ${styles.checked}` : `${styles.text}`}>
<input
value={updatedText}
onChange={(e) => setUpdatedText(e.target.value)}
ref={editedText}
/>
</div>
) : (
<div className={isChecked? `${styles.text} ${styles.checked}` : `${styles.text}`}>
{props.todo.text}
</div>)
}
</div>
<div>
<div className={styles.editBtnAndDeleteBtn}>
{(isEditClicked && !isDeleteClicked) ? (
<div>
<button className={styles.submitIcon} onClick={submitEditedContent}>
<FontAwesomeIcon icon={faCheck} size="2x" color="white" />
</button>
<button className={styles.cancelIcon} onClick={cancelEdit}>
<FontAwesomeIcon icon={faXmark} size="2x" color="white" />
</button>
</div>
) : ((!isEditClicked && isDeleteClicked) ? (
<div>
<button
className={styles.submitIcon}
onClick={() => onDelete(props.todo.id)}
>
<FontAwesomeIcon icon={faCheck} size="2x" color="white" />
</button>
<button className={styles.cancelIcon} onClick={cancelDelete}>
<FontAwesomeIcon icon={faXmark} size="2x" color="white" />
</button>
</div>
) : (
<div>
<button className={styles.editIcon} onClick={openEdit}>
<FontAwesomeIcon icon={faPenToSquare} size="2x" color="white" />
</button>
<button className={styles.delteIcon} onClick={openDelete}>
<FontAwesomeIcon icon={faTrashCan} size="2x" color="white" />
</button>
</div>
))}
</div>
</div>
</div>
);
};
export default TodoItem;
두 개의 state를 만든다.
isEditClicked
는 수정 버튼을 클릭했을 때와 클릭하지 않았을 때의 상태를 의미한다.
수정 버튼을 클릭하면 해당 투두를 div에서 input으로 바꾸고/수정 버튼과 삭제 버튼을 없애고/수정확인&수정취소 버튼을 보여줘야 하기 때문에 사용했다.
updatedText
는 수정된 투두이다. 수정된 내용을 상위에 있는 컴포넌트 Todo.js에 올려주어 화면에 표시될 투두리스트들을 담은 displayInputs
를 변경하기 위해 사용했다.
const [isEditClicked, setIsEditClicked] = useState(false);
const [updatedText, setUpdatedText] = useState('');
수정 버튼을 클릭하면 openEdit
이라는 함수가 실행된다.
이 함수는 isEditClicked
를 true
로 변경한다.
const openEdit = () => {
setIsEditClicked(true);
}
isEditClicked
가 true
가 되면 아래의 요소가 렌더링되는데, 평소에는 div에 투두 문자열을 표시하고 수정 버튼이 클릭되면 div가 문자열을 입력할 수 있는 input으로 바뀌어 수정할 수 있다. value
와 onChange
속성을 통해 입력한 문자열이 input창에 반영된다.
{isEditClicked
? (
<div className={isChecked? `${styles.text} ${styles.checked}` : `${styles.text}`}>
<input
value={updatedText}
onChange={(e) => setUpdatedText(e.target.value)}
/>
</div>
) : (
<div className={isChecked? `${styles.text} ${styles.checked}` : `${styles.text}`}>
{props.todo.text}
</div>)
}
수정 후 수정확인 버튼을 누르면 submitEditedContent
함수가 실행되고, 수정취소 버튼을 누르면 cancelEdit
함수가 실행된다.
<div>
<button className={styles.submitIcon} onClick={submitEditedContent}>
<FontAwesomeIcon icon={faCheck} size="2x" color="white" />
</button>
<button className={styles.cancelIcon} onClick={cancelEdit}>
<FontAwesomeIcon icon={faXmark} size="2x" color="white" />
</button>
</div>
cancelEdit
함수는 수정취소를 눌렀을 때 실행되는 함수이다. 따라서 투두를 수정했더라도 마지막엔 수정취소를 했으므로 updatedText
를 빈 문자열로 초기화해준다. 그리고 isEditClicked
를 false
로 설정해준다.
const cancelEdit = () => {
setUpdatedText('');
setIsEditClicked(false);
}
submitEditedContent
함수는 수정 확인 버튼을 눌렀을 때 실행되는 함수이다.
- 빈 문자열을 입력하고 확인을 누르면 반영되지 않게끔 return했다.
- 이때 입력창을 닫아주기 위해 isEditClicked
를 false
로 설정했다.
- 상위에서 맨 처음에 추가했을 때 input으로 입력하여 추가한 투두들을 displayInputs
라는 state에 모두 저장해서 map으로 하나씩 꺼내었었다. displayInputs
에 수정한 텍스트를 반영하기 위해 이 state가 선언된 todo.js라는 상위 컴포넌트로 데이터들을 올렸다(수정된 텍스트와 그 텍스트가 있는 투두의 id)
- 마지막으로 입력창을 닫아주기 위해 마지막에 isEditClicked
를 false
로 설정했다.
const submitEditedContent = () => {
if (updatedText === '') {
setIsEditClicked(false);
return;
}
props.submitEditedContent(updatedText, props.todo.id);
setUpdatedText('');
setIsEditClicked(false);
}
실행될 submitEditedContent
함수에 가보자. 모든 투두가 저장된 displayInputs
를 map으로 펼친 후 거기서 수정될 투두를 찾아 기존의 text를 수정한 텍스트인 updatedText
로 바꿨다.
const [displayInputs, setDisplayInputs] = useState([]);
const submitEditedContent = (updatedText, id) => {
setDisplayInputs(displayInputs.map(todo => {
if (todo.id === id) {
return {
...todo,
text: updatedText
};
}
return todo;
}))
}
수정 기능을 완성했다~!!
수정 버튼을 누르면 div에서 input으로 바뀐 투두에 바로 입력할 수 있도록 포커스를 두게 하려고 했는데, 잘 되지 않았다.
useRef
를 써서 해보려고 했으나, 이렇게 수정 버튼을 눌러서 openEdit
함수를 실행하고 editedText
부분에 focus를 둬!라고 해도
const editedText = useRef(null);
const openEdit = () => {
setIsEditClicked(true);
editedText.current.focus();
}
조건부렌더링이라서ref={editedText}
가 있는 input이 렌더링되기도 전에 editedText.current.focus()
가 실행되어 계속 undefined
가 나오는 것 같다. (맞는지 확실히 모르겠지만) 그래서 해결하면 따로 포스팅을 하려고 한다!
{isEditClicked
? (
<div className={isChecked? `${styles.text} ${styles.checked}` : `${styles.text}`}>
<input
value={updatedText}
onChange={(e) => setUpdatedText(e.target.value)}
ref={editedText}
/>
</div>
) : (
<div className={isChecked? `${styles.text} ${styles.checked}` : `${styles.text}`}>
{props.todo.text}
</div>)
}
이건 수정 기능과 관련된 건 아니지만, 개선하고 싶은 부분이 생겨 기록하려고 한다.
기존에 input에 아무것도 입력하지 않고 입력 버튼을 누르면 input이 전체적으로 빨간 색으로 변하면서 경고를 하는 기능을 만든 적이 있다.
이 기능은 현재 어떤 문제가 있는데, 다른 곳을 클릭했다가 다시 input 창을 클릭했을 때 경고가 사라지지 않는다.
빈 문자열을 입력하고 입력 버튼을 눌렀을 때만 경고를 표시하고, 다른 곳을 클릭하면 input창이 원래대로 돌아갔으면 한다. 하지만 어떻게 해야할지 모르겠어서 일단 기록만 남겨두려고 한다.
작년에 투두리스트를 만들어본 적 있는데(첫 번째 투두리스트), 수정 기능을 만들다가 포기했었어서 😂 이번에 성공한 것이 더욱 기뻤던 것 같다.
이 투두리스트는 딱 2주만 만들고 배포를 하려고 한다. 같은 실력으로 만들기만 계속 하는 것보다 기간을 잡고 만들고, 강의를 들으면서 공부를 하고 그 후에 더 아는게 많아진 머리로 뭘 또 만들고... 를 반복하는 것이 좋을 것 같아서이다.
그래서 만족하지 못하더라도 모레인 22.2.24에 배포를 하려고 한다! (이후에는 다시 강의를 들으면서 지식을 쌓고, 더 넓어진 시야로 나중에 리팩토링을 하러 올 것이다😊)
그 전까지 추가하고 싶은 기능을 열심히 만들어보자!!
오~~! 햄쥑쥑님의 강화 투두리스트 잘 보고 가요 ' - ' 설명이 넘 보기좋네요
더 진화하길 기대할게요