티스토리 뷰

목차

  1. 기능정의 및 HTML/CSS
  2. 할 일 추가하기
  3. 할 일 목록에서 할 일 삭제, 완료 처리 구현
  4. 할 일 수정하기
  5. 전체 완료 처리 및 남은 할 일 개수
  6. 하단 버튼 기능 구현

 

1. 더블 클릭 시 수정 모드 전환

아래 그림에서 표시된 영역을 더블 클릭하면 수정 모드로 전환하는 기능을 추가합니다.

이전에 만들었던 paintTodos() 함수에 todoElem이 만들어질 때, 더블 클릭에 대한 이벤트 리스너를 등록합니다. todoElem 요소에 더블 클릭 이벤트가 발생하면 콜백 함수로 onDbclickTodo()라는 함수가 호출됩니다.

const paintTodos = () => {
    todoListElem.innerHTML = ''; //todoListElem 요소 안의 HTML 초기화
	const allTodos = getAllTodos() // todos 배열 가져오기

    allTodos.forEach(todo => { 
 			...(생략)
    
        const todoElem = document.createElement('div');
        todoElem.classList.add('todo');
        todoElem.addEventListener('dblclick', (event) => onDbclickTodo(event, todo.id)) // 더블 클릭에 대한 이벤트 핸들러
        todoElem.innerText = todo.content;

			...(생략)
    })
}

onDbclickTodo() 함수는 두 개의 파라미터를 입력받습니다. 첫 번째 파라미터는 event객체이며, 두 번째 파라미터는 할 일의 id 입니다. onDbclickTodo() 함수를 통해 새로운 input 요소를 만들어 사용자가 수정할 수 있도록 하겠습니다.

document.createElement() 함수를 통해 inputElem이라는 input 요소를 만들고, inputElem의 value 값으로 event 객체의 target.innerText를 넣어줍니다. inputElem의 클래스 네임으로는 'edit-input'이라고 지정합니다. 이전의 css에서 'edit-input' 클래스 네임을 가지는 요소를 position: absolute로 정의하여 수정하고자 하는 todoItemElem영역을 완전히 가리도록 스타일링하였습니다.

const onDbclickTodo = (e, todoId) => {
    const todoElem = e.target;
    const inputText = e.target.innerText;
    const todoItemElem = todoElem.parentNode;
    const inputElem = document.createElement('input');
    inputElem.value = inputText;
    inputElem.classList.add('edit-input');

    inputElem.addEventListener('keypress', (e)=>{
        if(e.key === 'Enter') {
            updateTodo(e.target.value, todoId); // todo 수정
        }
    })
}
.edit-input {
    position: absolute;
    left: 0;
    top: 0;
    width: 590px;
    height: 2.8rem;
    margin: 0;
}

 

2. 수정하기

수정을 위해 만들어준 input 요소에서 'Enter' 키가 눌리면, 기존의 할 일 내용을 updateTodo() 함수를 통해 수정합니다. updateTodo()는 두 개의 파라미터를 갖습니다. 첫째는 text로 수정될 할 일의 내용이며, 두번째는 todoId로 수정 될 할 일의 id 입니다.

const newTodos = getAllTodos().map(todo => todo.id === todoId ? ({...todo, content: text}) : todo);

getAllTodos() 함수로 todos 배열을 가져와 map을 통해 id값을 비교하여할 일의 내용을 수정하는 새로운 todos 배열을 만듭니다. 그리고 setTodos() 함수를 통해 새로운 todos배열을 저장한 후, paintTodos() 함수를 통해, 변경된 todos 배열로 할 일 리스트를 다시 렌더링 합니다.

const updateTodo = (text, todoId) => {
    const newTodos = getAllTodos().map(todo => todo.id === todoId ? ({...todo, content: text}) : todo);
    setTodos(newTodos);
    paintTodos();
}

 

3. 수정 모드 종료

아래의 그림처럼 수정을 위한 입력창을 제외하고, 브라우저에서 클릭 이벤트가 발생 시, 수정 모드를 종료하도록 기능을 구현하겠습니다.

이를 위해 body 요소에 클릭에 대한 이벤트 리스너를 등록하며, 콜백 함수로 onClickBody() 함수를 만듭니다. onClickBody()함수는 event 객체를 인자로 받으며, event객체의 target이 수정 모드를 위해 생성한 inputElem이 아니라면, inputElem의 부모 요소인 todoElem에서 removeChild()를 사용하여 inputElem을 제거합니다. 그런 다음 body 요소에 등록된 클릭 이벤트를 제거해줍니다. 

이벤트 리스너는 이벤트 종류와 이벤트 리스너 함수 자체의 조합으로 식별되어 제거됩니다. 

또한 수정 완료, 즉 inputElem에서 'Enter'키를 눌려졌을 때도, body의 이벤트 리스너를 제거해주어야 합니다.

const onDbclickTodo = (e, todoId) => {
    const todoElem = e.target;
    const inputText = e.target.innerText;
    const todoItemElem = todoElem.parentNode;
    const inputElem = document.createElement('input');
    inputElem.value = inputText;
    inputElem.classList.add('edit-input');

    inputElem.addEventListener('keypress', (e)=>{
        if(e.key === 'Enter') {
            updateTodo(e.target.value, todoId); // todo 수정
            document.body.removeEventListener('click', onClickBody ); // 이벤트리스너 제거
        }
    })

    todoItemElem.appendChild(inputElem); // todoItemElem 요소에 자식 요소로 inputElem 요소 추가
    
        // body에 클릭에 대한 이벤트 리스너 등록
    document.body.addEventListener('click', onClickBody)
    
    // todoItemElem 요소를 제외한 영역을 클릭 시, 수정모드 종료
    const onClickBody = (e) => {
        if(e.target !== inputElem)  {
            todoItemElem.removeChild(inputElem);
            document.body.removeEventListener('click', onClickBody );
        }
    }
}

 

 

[전체 소스 코드]

const todoInputElem = document.querySelector('.todo-input');
const todoListElem = document.querySelector('.todo-list');

let todos = [];
let id = 0;

const setTodos = (newTodos) => {
    todos = newTodos;
}

const getAllTodos = () => {
    return todos;
}

const appendTodos = (text) => {
    const newId = id++;
    const newTodos = getAllTodos().concat({id: newId, isCompleted: false, content: text })
    // const newTodos = [...getAllTodos(), {id: newId, isCompleted: false, content: text }]
    setTodos(newTodos)
    paintTodos();
}

const deleteTodo = (todoId) => {
    const newTodos = getAllTodos().filter(todo => todo.id !== todoId );
    setTodos(newTodos);
    paintTodos()
}

const completeTodo = (todoId) => {
    const newTodos = getAllTodos().map(todo => todo.id === todoId ? {...todo,  isCompleted: !todo.isCompleted} : todo )
    setTodos(newTodos);
    paintTodos();
}

const updateTodo = (text, todoId) => {
    const currentTodos = getAllTodos();
    const newTodos = currentTodos.map(todo => todo.id === todoId ? ({...todo, content: text}) : todo);
    setTodos(newTodos);
    paintTodos();
}

const onDbclickTodo = (e, todoId) => {
    const todoElem = e.target;
    const inputText = e.target.innerText;
    const todoItemElem = todoElem.parentNode;
    const inputElem = document.createElement('input');
    inputElem.value = inputText;
    inputElem.classList.add('edit-input');
    inputElem.addEventListener('keypress', (e)=>{
        if(e.key === 'Enter') {
            updateTodo(e.target.value, todoId);
            document.body.removeEventListener('click', onClickBody );
        }
    })

    const onClickBody = (e) => {
        if(e.target !== inputElem)  {
            todoItemElem.removeChild(inputElem);
            document.body.removeEventListener('click', onClickBody );
        }
    }
    
    document.body.addEventListener('click', onClickBody)
    todoItemElem.appendChild(inputElem);
}

const paintTodos = () => {
    todoListElem.innerHTML = ''; //todoListElem 요소 안의 HTML 초기화
	const allTodos = getAllTodos() // todos 배열 가져오기

    allTodos.forEach(todo => { 
        const todoItemElem = document.createElement('li');
        todoItemElem.classList.add('todo-item');

        todoItemElem.setAttribute('data-id', todo.id );

        const checkboxElem = document.createElement('div');
        checkboxElem.classList.add('checkbox');
        checkboxElem.addEventListener('click', () => completeTodo(todo.id))
    
        const todoElem = document.createElement('div');
        todoElem.classList.add('todo');
        todoElem.addEventListener('dblclick', (event) => onDbclickTodo(event, todo.id))
        todoElem.innerText = todo.content;
    
        const delBtnElem = document.createElement('button');
        delBtnElem.classList.add('delBtn');
        delBtnElem.addEventListener('click', () =>  deleteTodo(todo.id))
        delBtnElem.innerHTML = 'X';

        if(todo.isCompleted) {
            todoItemElem.classList.add('checked');
            checkboxElem.innerText = '✔';
        }

        todoItemElem.appendChild(checkboxElem);
        todoItemElem.appendChild(todoElem);
        todoItemElem.appendChild(delBtnElem);

        todoListElem.appendChild(todoItemElem);
    })
}

const init = () => {
    todoInputElem.addEventListener('keypress', (e) =>{
        if( e.key === 'Enter' ){
            appendTodos(e.target.value); todoInputElem.value ='';
        }
    })
}

init()

 

[참고 자료] 

 

EventTarget.removeEventListener() - Web API | MDN

EventTarget.removeEventListener() 메서드는 이전에EventTarget.addEventListener()로 EventTarget 에 등록했던 이벤트 리스너를 제거합니다. 이 이벤트 리스너는 이벤트 종류와 이벤트 리스너 함수 자체의 조합으

developer.mozilla.org

 

 

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함