티스토리 뷰

목차

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

 

이번 챕터에서는 해야 할 일들을 전체 완료 처리 및 남은 할 일 개수를 표시하는 부분을 구현하도록 하겠습니다.

1. todos 전체 완료 처리

우선 todos.js 상단에 전체 완료 처리를 위해 만들어둔 버튼을 querySelector를 통해 checkAllBtnElem이라 선언하겠습니다. 그 다음, init() 함수 안에서 해당 버튼에 대한 클릭 이벤트 리스너를 등록하고, 콜백 함수로 onClickCompleteAll() 이라는 함수를 호출하겠습니다.

const completeAllBtnElem = document.querySelector('.complete-all-btn');

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

    completeAllBtnElem.addEventListener('click',  onClickCompleteAll); // 전체 완료 처리 버튼에 클릭 이벤트 리스너 
}

onClickCompleteAll() 함수는 현재 todos의 완료 상태 여부를 파악하여, 전체 완료를 처리하며, 동작 방식은 다음과 같습니다.

let isAllCompleted = false; // 전체 todos 체크 여부

const setIsAllCompleted = (bool) => { isAllCompleted = bool};

const completeAll = () => {
    completeAllBtnElem.classList.add('checked');
    const newTodos = getAllTodos().map(todo => ({...todo, isCompleted: true }) )
    setTodos(newTodos)
}

const incompleteAll = () => {
    completeAllBtnElem.classList.remove('checked');
    const newTodos =  getAllTodos().map(todo => ({...todo, isCompleted: false }) );
    setTodos(newTodos)
}

// 전체 todos의 check 여부 (isCompleted)
const checkIsAllCompleted = () => {
    if(getAllTodos().length === getCompletedTodos().length ){
        setIsAllCompleted(true);
        completeAllBtnElem.classList.add('checked');
    }else {
        setIsAllCompleted(false);
        completeAllBtnElem.classList.remove('checked');
    }
}

const onClickCompleteAll = () => {
    if(!getAllTodos().length) return; // todos배열의 길이가 0이면 return;

    if(isAllCompleted) incompleteAll(); // isAllCompleted가 true이면 todos를 전체 미완료 처리 
    else completeAll(); // isAllCompleted가 false이면 todos를 전체 완료 처리 
    setIsAllCompleted(!isAllCompleted); // isAllCompleted 토글
    paintTodos(); // 새로운 todos를 렌더링
    setLeftItems()
}

onClickCompleteAll() 함수를 통해 isAllCompleted 상태를 토글 시켜주며, 기존의 todos를 배열의 isCompleted를 바뀌는 isAllChecked 상태에 맞춰서 바꿔줍니다. isAllCheked값이 false가 되면, 전체 todos배열의 isCompleted값을 false로 바꿔주며, isAllChecked값이 true가 되면, 전체 todos배열의 isCompleted값을 true로 바꿔줍니다. 그 후, paintTodos()함수를 통해 todos를 재 렌더링해줍니다.

completeAllBtnElem 요소는 클릭 될 때만 isAllCompleted상태 값이 변하는 것이 아니라, 각각의 할 일 들을 완료 처리 할때와 새로운 할 일이 추가 될때도  isAllCompleted의 상태가 변하고, HTML에 표시해주어야 합니다. 

 

이를 위해 checkIsAllCompleted()라는 함수를 만들어 줍니다. checkIsAllCompleted()함수는 현재 todos배열의 길이와, 완료된 todos배열의 길이를 비교한 후, isAllCompleted의 상태를 변경하고 completeAllBtnElem요소에 'checked' 클래스 네임을 추가 또는 삭제합니다.

const getCompletedTodos = () => {
    return todos.filter(todo => todo.isCompleted === true );
}

// 전체 todos의 isCompleted 여부를 체크하여, 처리
const checkIsAllCompleted = () => {
    if(getAllTodos().length === getCompletedTodos().length ){
        setIsAllCompleted(true);
        completeAllBtnElem.classList.add('checked');
    }else {
        setIsAllCompleted(false);
        completeAllBtnElem.classList.remove('checked');
    }
}

새롭게 만든 checkIsAllCompleted()함수를 completeTodo()함수와 appendTodos()함수에 추가해주면, 각각의 할 일이 완료 처리가 될 때와, 새로운 할 일이 추가 될때마다 전체 할 일의 완료 여부를 파악하여 이를 표현해 줄 수 있습니다.

const completeTodo = (todoId) => {
    const newTodos = getAllTodos().map(todo => todo.id === todoId ? {...todo,  isCompleted: !todo.isCompleted} : todo )
    setTodos(newTodos);
    paintTodos();
    checkIsAllCompleted(); // 전체 todos의 완료 상태를 파악하여 전체 완료 처리 버튼 CSS 반영
}

const appendTodos = (text) => {
		...(중략)
    checkIsAllCompleted(); // 전체 완료처리 확인
    paintTodos();
}

 

2. 남은 할 일 개수 표시하기

남은 할 일 개수를 표시하는 요소를 querySelector를 사용하여 가져와 leftItemsElem이라 선언하겠습니다. 그리고 setLeftItems()라는 함수를 만들어 완료 처리가 되는 부분 마다 적용하여, 남은 할 일 개수를 갱신해주도록 하겠습니다.

const leftItemsElem = document.querySelector('.left-items')

// 현재 완료되지 않은 할 일 리스트를 반환한다.
const getActiveTodos = () => {
    return todos.filter(todo => todo.isCompleted === false);
}

const setLeftItems = () => {
    const leftTodos = getActiveTodos() 
    leftItemsElem.innerHTML = `${leftTodos.length} items left`
}

남은 할 일 개수를 표현하는 setLeftItems()함수를 todos의 배열의 길이와 완료상태가 변할 때 호출되는 함수[ init(), appendTodos(), deleteTodo(), completeTodo(), onClickCompleteAll() ]에 각각 적용해 주도록 합니다. 

const onClickCompleteAll = () => {
    if(!getAllTodos().length) return; // todos배열의 길이가 0이면 return;

    if(isAllCompleted) incompleteAll(); // isAllCompleted가 true이면 todos를 전체 미완료 처리 
    else completeAll(); // isAllCompleted가 false이면 todos를 전체 완료 처리 
    setIsAllCompleted(!isAllCompleted); // isAllCompleted 토글
    paintTodos(); // 새로운 todos를 렌더링
    setLeftItems(); // 남은 할 일 개수 표시
}

const appendTodos = (text) => {
		...(중략)
    checkIsAllCompleted(); // 전체 완료처리 확인
    setLeftItems(); // 남은 할 일 개수 표시
    paintTodos();
}

const deleteTodo = (todoId) => {
		...(중략)
    setLeftItems(); // 남은 할 일 개수 표시
    paintTodos()
}

const completeTodo = (todoId) => {
		...(중략)
    setLeftItems(); // 남은 할 일 개수 표시
    checkIsAllCompleted();
}

const init = () => {	
		...(중략)
    checkAllBtnElem.addEventListener('click',  onClickCheckAll); // 전체 완료 처리 버튼에 클릭
    setLeftItems() // 남은 할 일 개수 표시
}

 

 

[전체 소스 코드]

const todoInputElem = document.querySelector('.todo-input');
const todoListElem = document.querySelector('.todo-list');
const completeAllBtnElem = document.querySelector('.complete-all-btn');

const leftItemsElem = document.querySelector('.left-items')

const getActiveTodos = () => {
    return todos.filter(todo => todo.isCompleted === false);
}

const setLeftItems = () => {
    const leftTodos = getActiveTodos()
    leftItemsElem.innerHTML = `${leftTodos.length} items left`
}

let todos = [];
let id = 0;

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

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

const getCompletedTodos = () => {
    return todos.filter(todo => todo.isCompleted === true );
}

let isAllCompleted = ''; // 전체 todos 체크 여부

const setIsAllCompleted = (bool) => { isAllCompleted = bool};

const completeAll = () => {
    completeAllBtnElem.classList.add('checked');
    const newTodos = getAllTodos().map(todo => ({...todo, isCompleted: true }) )
    setTodos(newTodos)
}

const incompleteAll = () => {
    completeAllBtnElem.classList.remove('checked');
    const newTodos =  getAllTodos().map(todo => ({...todo, isCompleted: false }) );
    setTodos(newTodos)
}

// 전체 todos의 check 여부 (isCompleted)
const checkIsAllCompleted = () => {
    if(getAllTodos().length === getCompletedTodos().length ){
        setIsAllCompleted(true);
        completeAllBtnElem.classList.add('checked');
    }else {
        setIsAllCompleted(false);
        completeAllBtnElem.classList.remove('checked');
    }
}

const onClickCompleteAll = () => {
    if(!getAllTodos().length) return; // todos배열의 길이가 0이면 return;

    if(isAllCompleted) incompleteAll(); // isAllCompleted가 true이면 todos를 전체 미완료 처리 
    else completeAll(); // isAllCompleted가 false이면 todos를 전체 완료 처리 
    setIsAllCompleted(!isAllCompleted); // isAllCompleted 토글
    paintTodos(); // 새로운 todos를 렌더링
    setLeftItems()
}

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)
    setLeftItems()
    checkIsAllCompleted();
    paintTodos();
}

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

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

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 = null; //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 ='';
        }
    })

    completeAllBtnElem.addEventListener('click',  onClickCompleteAll)

    setLeftItems()
}

init()
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
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
글 보관함