TIL 02 | filter todo 리뷰

Project Overview

filter-todo page
filter-todo gitHub

이번에는 수정 기능을 넣지 않고, 밑에 버튼을 누르면 상태에 따른 아이템들이 보여지게 투두 리스트를 만들었다.

작업 기간
2021.07.10 ~ 2021.07.21

기술 스택

  • HTML/CSS
  • JavaScript(ES6+)
  • Git

구현 사항

  • 사용자가 할 일을 등록할 때는 최소 한 글자 이상을 입력해야 등록할 수 있다.
  • 등록된 일은 할 일 목록에 보여지며, 체크박스의 표시 여부로 할 일에 대한 상태를 알 수 있다.
  • 또한 미완료 된 일은 삭제 할 수 있으며, 완료된 일은 미완료로 상태에 대한 다시 값을 변경할 수 있다.
  • 아래의 버튼을 누르면 버튼에 이름에 맞는 리스트 아이템을 보여준다.

결과화면

할 일 등록

삭제 및 할 일 상태 변경

버튼을 누르면 목록 이동

Project Review

느낀점

  • <input type="checkbox">부분 에서 웹 접근성 준수하여 <label>에 디자인 하는게 너무나 힘들었지만 position 을 적절히 사용하여 마무리 할 수 있었다 😅

  • 버튼을 눌러주면 아이템들을 지우고 다시 그렸다 하는 부분이 생각보다 어려웠다. 다음부터 HTML 설계할 때 조금 더 생각하고 설계를 해야겠다고 느꼈다.

  • 기능을 다 완성하고 나서, 이벤트를 위임하는 것보다 각 버튼에 맞는게 이벤트를 실행하는 게 맞는거 같아서 수정하였다. 코드를 완성해도 리팩터링은 해야되는게 맞다고 생각한다 !

HTML Code

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>todo list</title>
  <link rel="stylesheet" href="css/reset.css">
  <link rel="stylesheet" href="css/style.css">

  <script src="https://kit.fontawesome.com/db184c2112.js" crossorigin="anonymous"></script>
  <script defer src="js/todo.js"></script>
</head>

<body>
  <section class="wrapper">

    <form class="add-form">
      <div>
        <label for="addInput" class="a11y-hidden">할 일 내용</label>
        <input type="text" class="add-input" id="addInput" />
        <button type="submit" class="add-btn">Add</button>
      </div>
    </form>

    <ul class="todo-list__all">
    </ul>

    <ul class="todo-list__complete">
    </ul>

    <ul class="todo-list__incomplete">
    </ul>

    <div class="btn-group">
      <button type="button" class="all-btn">All</button>
      <button type="button" class="complete-btn">Complete</button>
      <button type="button" class="incomplete-btn">Incomplete</button>
    </div>
  </section>

</body>

</html>

css(rest.css) Code

@charset "utf-8";

body, 
section,
form, 
div, 
span, 
label, 
input, 
button, 
ul,
li,
i{
  margin: 0;
  padding: 0;
}

button{
  border: none;
  outline: none;
  background: transparent;
  cursor: pointer;
}

ul,
li {
  list-style: none;
}

.a11y-hidden {
  overflow: hidden;
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  clip: rect(0 0 0 0);
  clip: rect(0, 0, 0, 0);
}

css(style.css) Code

@charset "utf-8";

* {
  box-sizing: border-box;
}

body {
  overflow: scroll;
  background: rgb(152, 250, 168, 1);
}

.wrapper {
  position: relative;
  width: 21.88rem;
  min-height: 18.25rem;
  padding: 1.25rem;
  margin: 9.375rem auto;
  border-radius: 0.625rem;
  background: #fff;
  color: #fff;
}

.add-form > div {
  display: flex;
  justify-content: space-around;
}

.add-input {
  width: calc(100% - 115px);
  height: 1.75rem;
  padding: 0.1875rem 0.625rem;
  line-height: 1.75rem;
  border: 0.0625rem solid rgb(194, 190, 190);
  border-radius: 0.9375rem;
  font-size: 0.9375rem;
}

.add-input:focus{
  border: 0.0625rem solid #4fc08d;
  outline: none;
}

.add-input[required = false]{
  border: 0.0625rem solid red;
}

input:hover {
  outline: none;
}

.add-btn {
  padding: 0.5rem 1.25rem;
  border: 0.0625rem solid #4fc08d;
  border-radius: 0.9375rem;
  color: #4fc08d;
}

.todo-list__all li,
.todo-list__complete li,
.todo-list__incomplete li {
  position: relative;
  display: flex;
  justify-content: space-between;
  margin: 0 1.125rem 1.563rem 1.125rem;
  font-size: 1rem;
}

.todo-list__all li:first-child,
.todo-list__complete li:first-child,
.todo-list__incomplete li:first-child {
  margin-top: 1.25rem;
}

.todo-list__all li:last-child,
.todo-list__complete li:last-child,
.todo-list__incomplete li:last-child {
  margin-bottom: 3.25rem;
}

li.todo-incomplete label {
  color: #4fc08d;
}

li.todo-incomplete label:hover {
  text-decoration: underline;
}

li.todo-complete label {
  color: lightgreen;
}

li.todo-complete label:hover {
  text-decoration: underline;
}

li input[type="checkbox"]{
  position: absolute;
  top: 0.25rem;
  left: 0;
  width: 0.9375rem;
  height: 0.9375rem;
  line-height: 0.9375rem;
}

li input[type="checkbox"] + label{
  display: inline-block;
  position: relative;
  padding: 0.1875rem 0 0.1875rem 1.875rem;
  line-height: 0.9375rem;
}

li input[type="checkbox"] + label::before{
  display: inline-block;
  position: absolute;
  top: 0.1875rem;
  left: -0.0625rem;
  width: 0.9375rem;
  height: 0.9375rem;
  padding-left: 0.0625rem;
  line-height: 0.9375rem;
  border: 0.0625rem solid #4fc08d;
  border-radius: 0.125rem;
  background: #fff;
  content: "";
} 

li input[type="checkbox"]:checked + label::before{
  font-size: 1.063rem;
  line-height: 1.063rem;
  text-align: center;
  font-weight: 800;
  background: #4fc08d;
  color: #fff;
  content: "\2713";
}

li button {
  width: 1.5rem;
  height: 1.5rem;
  line-height: 1.5rem;
  border: 1px solid #4fc08d;
  border-radius: 50%;
  font-size: 0.8125rem;
  text-align: center;
  color: #4fc08d;
  cursor: pointer;
}

li button.deleBtn:hover {
  background: #4fc08d;
  color: #fff;
}

li button.checkBtn {
  color: lightgreen;
  border: 0.0625rem solid lightgreen;
}

li button.checkBtn:hover {
  background: lightgreen;
  color: #fff;
}

.btn-group {
  position: absolute;
  bottom: 0.625rem;
  display: flex;
  justify-content: space-around;
}

.btn-group button {
  margin-left: 0.625rem;
  padding: 0.5rem 1.5rem;
  border: 0.0625rem solid #4fc08d;
  border-radius: 0.9375rem;
  color: #4fc08d;
}

.btn-group button:first-child {
  margin-left: 0;
}

button:hover,
button:focus {
  background: #4fc08d;
  color: #fff;
}

i{
  pointer-events: none;
}
'use strict';

let todos = [];
const loaded_todos = localStorage.getItem('TODOS');
const add_input = document.querySelector('.add-input');
const add_form = document.querySelector('.add-form');
const add_btn = document.querySelector('.add-btn');
const todo_list_all = document.querySelector('.todo-list__all');
const todo_list_complete = document.querySelector('.todo-list__complete');
const todo_list_incomplete = document.querySelector('.todo-list__incomplete');
const btn_group = document.querySelector('.btn-group');
const list = document.querySelectorAll('ul');

// 1. 초기화 실행.
function init() {
  loadTodos();
  addEvent();
  render();
}

// 2. 로컬 스토리지에서 데이터를 조회.
function loadTodos() {
  if (loaded_todos !== null) {
    todos = JSON.parse(loaded_todos);
    return todos;
  }
}

// 3. 화면 그리기.
function render(state = 'all') {
  blankTemplate();
  let join, item;
  let list = [...todos].reverse();

  switch (state) {
    case 'all':
      join = list.map(itemTemplate).join('');
      todo_list_all.innerHTML = join;
      break;
    case 'incomplete':
      item = list.filter(incompleteFilter);
      join = item.map(itemTemplate).join('');
      todo_list_incomplete.innerHTML = join;
      break;
    case 'complete':
      item = list.filter(completeFilter);
      join = item.map(itemTemplate).join('');
      todo_list_complete.innerHTML = join;
      break;
    default:
      console.log('It is a state that does not exist.');
  }
}

// 3-1. 리스트에 아이템을 추가하기 전 템플릿을 초기화.
function blankTemplate() {
  todo_list_all.innerHTML = '';
  todo_list_incomplete.innerHTML = '';
  todo_list_complete.innerHTML = '';
}

// 3-2. 아이템의 상태가 미완료 여부 필터.
function incompleteFilter(todo) {
  return !todo.isCompleted;
}

// 3-3. 아이템의 상태가 완료 여부 필터.
function completeFilter(todo) {
  return todo.isCompleted;
}

// 3-4. 아이템을 완료 여부 상태에 따라서 맞는 li를 요소를 만들기.
function itemTemplate(todo) {
  return `
  <li class = ${todo.isCompleted ? 'todo-complete' : 'todo-incomplete'} id = ${
    todo.id
  }>
    <input type="checkbox" id=${todo.id} value=${todo.isCompleted} ${
    todo.isCompleted ? 'checked' : 'unchecked'
  }>    
    <label for=${todo.id} class=${
    todo.isCompleted ? 'complete__item--content' : 'incomplete__item--content'
  }>
      ${todo.content}
    </label>
    <button type="button" class=${todo.isCompleted ? 'checkBtn' : 'deleteBtn'}>
      <i class="fas ${todo.isCompleted ? 'fa-check' : 'fa-times'}"></i>
      <span class="a11y-hidden">${todo.isCompleted ? '체크' : '삭제'}</span>
    </button>
  </li>`;
}

// 2. 이벤트.
function addEvent() {
  // 2-1. add_form에서 'Enter'를 누르면 추가.
  add_form.addEventListener('keydown', (e) => {
    return e.key.includes('Enter') && addFormSubmit(e);
  });

  // 2-2. add_form에서 'add-btn'를 누르면 추가.
  add_btn.addEventListener('click', (e) => {
    return addFormSubmit(e);
  });

  // 2-3.btn-group안에서 각각의 버튼을 누르면 화면 이동.
  btn_group.addEventListener('click', (e) => {
    switch (e.target.className) {
      case 'all-btn':
        render('all', e);
        break;
      case 'incomplete-btn':
        render('incomplete');
        break;
      case 'complete-btn':
        render('complete');
        break;
      default:
        console.log('The button does not exist.');
    }
  });

  // 2-4. ul tag안에서 각각 li에 있는 버튼을 누르면 이벤트 발생.

  list.forEach((item) => {
    item.addEventListener('click', (e) => {
      switch (e.target.className) {
        case 'deleteBtn':
          itemAction(e, 'delete');
          break;
        case 'checkBtn':
        case 'incomplete__item--content':
        case 'complete__item--content':
          itemAction(e, 'change');
          break;
        default:
          console.log('The button does not exist.');
      }
    });
  });
}

// 4. form 유효성 검사.
function addFormSubmit(e) {
  e.preventDefault();

  if (add_input.value === '') {
    return add_input.setAttribute('required', false);
  }

  addTodoItem(add_input.value);
  add_input.setAttribute('required', true);
  add_input.value = '';
}

// 4-1. 아이템 추가.
function addTodoItem(value) {
  todos.push({
    id: Math.floor(Math.random() * 999),
    content: value,
    isCompleted: false,
  });
  saveTodos();
  render();
}

// 5. 클릭한 대상과 아이템이 일치하는지 검사.
function matchingID(id, item) {
  return id === item.id;
}

// 6. li에서 어떤 버튼을 눌렀는지 상태에 따라서 맞는 행동.
function itemAction(e, state) {
  console.log('click');
  let todo;
  let name = e.currentTarget.className.substring(11);
  let list = [...todos];
  let li = e.target.parentNode;
  let id = Number(li.id);
  let item = list.find(matchingID.bind(todo, id));
  let index = list.findIndex(matchingID.bind(todo, id));

  switch (state) {
    case 'delete':
      list.splice(index, 1);
      break;
    case 'change':
      item.isCompleted = !item.isCompleted;
      list.splice(index, 0);
      break;
    default:
      console.log('It is a state that does not exist.');
  }
  todos = list;
  saveTodos();
  render(name);
}

// 7. 로컬 스토리지 데이터 저장
function saveTodos() {
  localStorage.setItem('TODOS', JSON.stringify(todos));
}

init();

좋은 웹페이지 즐겨찾기