라이브러리를 사용하지 않고 순수하게 자바스크립트 언어로만 프로그래밍을 하고 싶어서 투두리스트를 선택하게 만들게 되었다.
작업 기간
2021.06.28 ~ 2021.07.05
기술 스택
구현 사항
할 일 등록
삭제
할 일 수정
목록 이동
javascript는 react처럼 실시간으로 화면 대한 변화를 렌더링 하지 않기 때문에 따로 렌더링 하는 함수를 구현해서 사용해야 된다.
// render
const renderTodo = () => {
let reverse, item, join;
reverse = [...todoList];
item = reverse.filter(incomplete);
join = item.map(incompleteTemplate).join('');
INCOMPLETE.innerHTML = join;
reverse = [...completedList];
item = reverse.filter(completed);
join = item.map(completedTemplate).join('');
COMPLETED.innerHTML = join;
};
appendChild()
와 비슷해보이지만, inrestBefore()
는 전달 인자가 두 개이다. 첫 번째 전달인자 newNode는 삽입할 노드, 두 번째 전달 인자 referenceNode는 기준점 노드이다.
let insertedNode = parentNode.insertBefore(newNode, referenceNode);
할 일 입력하고 난 후 엔터(enter)를 누르면 등록이 되게 해야되는데 그 부분을 처음부터 생각하지 못한게 아쉬웠다.
글로 봤을 때는 잘 와닿지 않는 코드 부분들을 실제 내가 작성하면서 사용되니까 더 빠른 이해가 되었다. 예를 들면 func.bind()
, node.insertBefore()
등등
돌다리도 두드려보고 건너기, 코드가 완성 되었다고 방심하지 말고 리팩토링하여 검토해야겠다.
<!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>vanilla todo</title>
<link rel="stylesheet" href="css/reset.css">
<link rel="stylesheet" href="css/style.css">
<script defer src="js/todo.js"></script>
</head>
<body>
<main class="todoContent">
<header>
<h1 class="title">todo list</h1>
</header>
<!-- add item -->
<section>
<h2>add item</h2>
<form class="add-form">
<div>
<label for="addInput" class="a11y-hidden">할 일 추가</label>
<input type="text" id="addInput" class="add-input" required />
<button type="submit" class="add-btn">add</button>
</div>
</form>
</section>
<!-- // add item -->
<!-- todo list -->
<section class="todo">
<h2>todo</h2>
<ul class="incomplete">
</ul>
</section>
<!-- // todo list -->
<!-- completed list -->
<section class="todo">
<h2>completed</h2>
<ul class="completed">
</ul>
</section>
<!-- completed list -->
</main>
</body>
</html>
@charset "utf-8";
/* margin, padding */
body,
main,
div,
span,
section,
label,
input,
form,
button,
ul,
h1,
h2,
h3,
p,
i{
margin: 0;
padding: 0;
}
/* label */
label{
background: transparent;
}
/* input */
input{
border: none;
outline:none;
background: transparent;
}
input[type="checkbox"]{
cursor: pointer;
}
/* button */
button{
border: none;
outline: none;
text-transform: capitalize;
background: transparent;
cursor: pointer;
}
/* list */
ul,
li{
list-style:none;
}
/* title */
h1,
h2,
h3{
font-weight: normal;
text-transform: uppercase;
}
i {
font-style: normal;
}
body,
input,
button,
div,
label,
span,
p,
h1,
h2,
h3,
ul,
li{
font-family: 'Roboto Condensed', sans-serif;
font-size: 1rem;
line-height: 1.5;
color: #fff;;
}
.a11y-hidden {
overflow: hidden;
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
clip: rect(0 0 0 0);
clip: rect(0, 0, 0, 0);
}
@charset "utf-8";
body {
overflow: scroll;
background:#f5f3e9 ;
}
main{
width: 28.13rem;
margin: 6.25rem auto;
padding: 1.25rem;
box-shadow: -1.25rem -1.25rem 0px 0px rgb(100 100 100 / 10%);
background: #98c0b7;
color: #fff;
}
h1{
font-size: 1.667rem;
text-align: center;
}
h2{
font-size: 1.375rem;
margin-top: 0.625rem;
border-bottom: 1px solid #fff;
}
/* add-form */
.add-form > div{
display: flex;
justify-content: space-between;
}
/* add-form input */
.add-input{
width: calc(100% - 70px);
height: 2.5rem;
margin: 0.625rem 0;
padding: 0 0.625rem;
font-size: 1.667rem;
line-height: 2.5rem;
background: #fff;
color: black;
}
/* add-form button */
.add-btn{
font-size: 1.25rem;
}
/* todo - ul li */
.todo ul li{
display: flex;
justify-content: space-between;
border-bottom: 1px solid #a2cec4;
}
/* todo - ul li checkbox */
.todo li input[type="checkbox"]{
width: 1rem;
height: 1rem;
margin: 0.8125rem 0.625rem 0 0;
line-height: 1rem;
}
/* todo -ul.incomple li label */
.todo .incomplete li label{
width: calc(100% - 165px);
margin : 0.5625rem 0;
height:1.563rem;
line-height: 1.563rem;
font-size: 1.5rem;
}
/* todo -ul.incomple li input(edit) */
.todo .incomplete li input[type="text"]{
width: calc(100% - 165px);
height: 1.563rem;
margin: 0.5rem 0;
border: 1px solid #91d4c5;
line-height: 1.563rem;
font-size: 1.5rem;
}
/* todo -ul.completed li label */
.todo .completed li label{
width: calc(100% - 85px);
height: 1.563rem;
margin: 0.5625rem 0;
line-height: 1.563rem;
font-size: 1.5rem;
}
/* todo - li butgrop buttons */
.todo-btngroup button{
padding: 0 0.625rem 0 0;
margin: 0.5rem 0.625rem 0 0;
font-size: 1.25rem;
}
/* todo - li btngroup saveBtn(edit) */
.todo-btngroup .save-btn{
padding: 0;
}
.todo-btngroup button:last-child{
padding: 0;
}
(() => {
let todoList = [];
let completedList = [];
const LOADED_TODOLIST = localStorage.getItem('TODO');
const COMPLETED_LIST = localStorage.getItem('COMPLETED');
const TODO_CONTENT = document.querySelector('.todoContent');
const ADD_INPUT = document.querySelector('.add-input');
const INCOMPLETE = document.querySelector('.incomplete');
const COMPLETED = document.querySelector('.completed');
// initialize and execute
const init = () => {
addEvent();
loadTodoList();
loadCompletedList();
renderTodo();
};
// loading todoList
const loadTodoList = () => {
if (LOADED_TODOLIST !== null) {
todoList = JSON.parse(LOADED_TODOLIST);
return todoList;
}
};
// loading compltedList
const loadCompletedList = () => {
if (COMPLETED_LIST !== null) {
completedList = JSON.parse(COMPLETED_LIST);
return completedList;
}
};
// event listener
const addEvent = () => {
let li, id, index, todo, list, checkbox, item;
let label, input, save_btn, edit_btn;
TODO_CONTENT.addEventListener('click', (e) => {
// add button
if (e.target.className === 'add-btn') {
addFormSubmit();
// delete button
} else if (e.target.className === 'delete-btn') {
li = e.target.parentNode.parentNode;
id = Number(li.getAttribute('data-id'));
checkbox = li.querySelector('input[type="checkbox"]').value;
list = checkbox === 'true' ? [...completedList] : [...todoList];
index = list.findIndex(matchingID.bind(todo, id));
list.splice(index, 1);
list = checkbox === 'true' ? (completedList = list) : (todoList = list);
renderTodo();
saveTodoList();
saveCompletedList();
// edit button
} else if (e.target.className === 'edit-btn') {
list = [...todoList];
li = e.target.parentNode.parentNode;
id = Number(li.getAttribute('data-id'));
item = todoList.find(matchingID.bind(todo, id));
label = li.querySelector('label');
label.classList.add('a11y-hidden');
save_btn = li.querySelector('.save-btn');
save_btn.classList.remove('a11y-hidden');
edit_btn = li.querySelector('.edit-btn');
edit_btn.classList.add('a11y-hidden');
input = document.createElement('input');
input.setAttribute('type', 'text');
input.setAttribute('id', id);
input.setAttribute('value', item.contents);
li.insertBefore(input, label.nextSibling);
// save button
} else if (e.target.className === 'save-btn') {
list = [...todoList];
li = e.target.parentNode.parentNode;
id = Number(li.getAttribute('data-id'));
item = list.find(matchingID.bind(todo, id));
index = list.findIndex(matchingID.bind(todo, id));
input = li.querySelector('input[type="text"]');
item.contents = input.value;
save_btn = li.querySelector('.save-btn');
save_btn.classList.add('a11y-hidden');
edit_btn = li.querySelector('.edit-btn');
edit_btn.classList.remove('a11y-hidden');
list.splice(index, 0);
todoList = list;
saveTodoList();
renderTodo();
// incomplete if checkbox value is false
} else if (e.target.type === 'checkbox' && e.target.value === 'false') {
list = [...todoList];
id = Number(e.target.id);
index = list.findIndex(matchingID.bind(todo, id));
item = list.find(matchingID.bind(todo, id));
item.isCompleted = !item.isCompleted;
completedList.push(item);
list.splice(index, 1);
todoList = list;
renderTodo();
saveTodoList();
saveCompletedList();
// complete if checkbox value is true
} else if (e.target.type === 'checkbox' && e.target.value === 'true') {
list = [...completedList];
id = Number(e.target.id);
index = list.findIndex(matchingID.bind(todo, id));
item = list.find(matchingID.bind(todo, id));
item.isCompleted = !item.isCompleted;
todoList.push(item);
list.splice(index, 1);
completedList = list;
renderTodo();
saveTodoList();
saveCompletedList();
}
});
};
// add item form validation
const addFormSubmit = () => {
let VALUE = ADD_INPUT.value;
if (VALUE === '') {
return false;
}
ADD_INPUT.value = '';
addTodoDate(VALUE);
};
// add an item to the list
const addTodoDate = (value) => {
todoList.push({
id: Math.floor(Math.random() * 999),
contents: value,
isCompleted: false,
});
renderTodo();
saveTodoList();
};
// incompleteTemplete
const incompleteTemplate = (item) => {
const INCOMPLETE_ITEM = `
<li data-id=${item.id}>
<input id=${item.id} type="checkbox" value=${item.isCompleted} />
<label for=${item.id}>${item.contents}</label>
<div class="todo-btngroup">
<button type="button" class="edit-btn">edit</button>
<button type="button" class="a11y-hidden save-btn">save</button>
<button type="button" class="delete-btn">delete</button>
</div>
</li>
`;
return INCOMPLETE_ITEM;
};
const incomplete = (item) => {
return !item.isCompleted;
};
// completed template
const completedTemplate = (item) => {
const COMPLETED_ITEM = `
<li data-id=${item.id}>
<input id=${item.id} type="checkbox" value=${item.isCompleted} />
<label for=${item.id}>${item.contents}</label>
<div class="todo-btngroup">
<button type="button" class="delete-btn">delete</button>
</div>
</li>
`;
return COMPLETED_ITEM;
};
const completed = (item) => {
return item.isCompleted;
};
// matching item
const matchingID = (id, item) => {
return item.id === id;
};
// render
const renderTodo = () => {
let reverse, item, join;
// todoList
reverse = [...todoList];
item = reverse.filter(incomplete);
join = item.map(incompleteTemplate).join('');
INCOMPLETE.innerHTML = join;
reverse = [...completedList];
item = reverse.filter(completed);
join = item.map(completedTemplate).join('');
COMPLETED.innerHTML = join;
};
// save to todoList LocalStorage
const saveTodoList = () => {
localStorage.setItem('TODO', JSON.stringify(todoList));
};
// save to completedList LocalStorage
const saveCompletedList = () => {
localStorage.setItem('COMPLETED', JSON.stringify(completedList));
};
init();
})();