TIL 02 | filter todo 리뷰
Project Overview
이번에는 수정 기능을 넣지 않고, 밑에 버튼을 누르면 상태에 따른 아이템들이 보여지게 투두 리스트를 만들었다.
작업 기간
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();
Author And Source
이 문제에 관하여(TIL 02 | filter todo 리뷰), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@vi2920va/filter-todo-리뷰저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)