자바스크립트로 투두리스트 (todo-list) 만들기 1탄입니다.
강의를 보고 난 후 혼자 정리하는 느낌이라.. 조금 조잡하고 설명이 정확하지 않을 수 있습니다
HTML,CSS는 강의자료로 받았고, 기능만 구현했습니다.
어떻게 기능을 구현했는지를 보시는게 좋을 것 같습니다. (개발환경이 다를 것 같기에)
완성본
기능 설명 (1탄)
1. 할일 추가
2. 할일 수정
3. 할일 지우기
4. 할일 카테고리별로 필터링
HTML 과 CSS
강의 자료이기때문에, 참고용으로 보시라고 캡쳐본으로 올립니다
* 모듈 번들러는 rollup 을 사용했습니다. 커스텀 설정은 제 github 에 있습니다.
기초 작업
스크립트는 ES6 클래스 문법을 사용했습니다. ( 저도 아직 ES6 문법에 익숙치 않아서 기능 구현에 중점을 맞춰서 설명합니다 )
사용할 엘리먼트 변수 지정
inputContainerEl; // 할일 추가와 카테고리들을 묶고 있는 요소
inputAreaEl; // 할일을 작성하고 추가하는 요소들을 묶고 있는 요소
todoInputEl; // 할일을 작성하는 input
addBtnEl; // 할일을 추가하는 button
todoContainerEl; // 투두리스트 container
todoListEl; // 할일 들을 추가 할 공간의 부모 요소
radioAreaEl; // 카테고리들을 묶고 있는 부모 요소
filterRadioBtnEls; // 라디오 버튼으로 형성되어있는 카테고리 input 들
Element 가져오기
assignElement() {
this.inputContainerEl = document.getElementById("input-container");
this.inputAreaEl = this.inputContainerEl.querySelector("#input-area");
this.todoInputEl = this.inputAreaEl.querySelector("#todo-input");
this.addBtnEl = this.inputAreaEl.querySelector("#add-btn");
this.todoContainerEl = document.getElementById("todo-container");
this.todoListEl = this.todoContainerEl.querySelector("#todo-list");
this.radioAreaEl = this.inputContainerEl.querySelector("#radio-area");
this.filterRadioBtnEls = this.radioAreaEl.querySelectorAll(
'input[name="filter"]'
);
}
addEventListener 함수를 사용하는 코드 묶기
addEvent(){ 추후에 들어갈 addEventListener 함수 )
constructor 에 클래스 필드를 선언하고 초기화 해주기
constructor(storage) {
this.assignElement();
this.addEvent();
// 추후 더 추가될 예정임
}
할 일 추가 기능
이벤트리스너 함수
addEvent() {
this.addBtnEl.addEventListener("click", this.onClickAddBtn.bind(this));
}
onClickAddBtn () - 추가 버튼을 클릭 시 발생하는 이벤트
onClickAddBtn() {
if (this.todoInputEl.value.length === 0) {
alert("내용을 입력해주세요");
return;
}
const id = Date.now();
// this.storage.saveTodo(id, this.todoInputEl.value); 3탄에서 함
this.createTodoElement(id, this.todoInputEl.value);
}
1. input.value (할일을 작성하는 Input) 의 아무런 값이 없다면 alert 메소드를 사용해, 사용자에게 내용을 입력하라고 함.
2. 할일이 추가 될 때마다 새로운 id 가 생성됨.
3. createTodoElement() 라는 할일이 추가 되었을 때 할일 리스트에 추가 할 엘리먼트들을 생성하는 함수를 호출해준다.
4. 파라미터로 id 와, input에 작성한 값을 보내준다.
Date.now()
- 1970년 1월 1일 0시 0분 0초부터 현재까지 경과된 밀리 초를 Number 형으로 반환해줌
- now()는 Date의 정적 메소드이기 때문에, 항상 Date.now() 처럼 사용해야 함
Date.now() 1672924948319
createTodoElement () - 추가한 할일들의 Element를 동적으로 생성해주는 함수
createTodoElement(id, value, status = null) {
const todoDiv = document.createElement("div");
todoDiv.classList.add("todo");
if (status === "DONE") {
todoDiv.classList.add("done");
}
todoDiv.dataset.id = id;
const todoContent = document.createElement("input");
todoContent.value = value;
todoContent.readOnly = true;
todoContent.classList.add("todo-item");
const fragment = new DocumentFragment();
fragment.appendChild(todoContent);
fragment.appendChild(
this.createButton("complete-btn", "complete-btn", ["fas", "fa-check"])
);
fragment.appendChild(
this.createButton("edit-btn", "edit-btn", ["fas", "fa-edit"])
);
fragment.appendChild(
this.createButton("delete-btn", "delete-btn", ["fas", "fa-trash"])
);
fragment.appendChild(
this.createButton("save-btn", "save-btn", ["fas", "fa-save"])
);
todoDiv.appendChild(fragment);
this.todoListEl.appendChild(todoDiv);
this.todoInputEl.value = "";
}
1. 하나의 할 일 요소들을 묶는 element 를 "todoDiv" 라고 만들어준다.
2. "todoDiv" 에게 'todo' 라는 클래스를 추가해준다. (css 적용을 위해)
3. 할 일의 상태가 완료('DONE')되었다면 todoDiv에게 "done" 이라는 클래스를 추가해준다.
4. todoDiv의 data-id에 id 값을 넣어준다 (data-id 도 동적으로 추가해주는 것)
5. 작성한 할 일을 읽거나 수정할 수 있도록 input element 를 만들어준다.
6. 할 일 input 에 value 값은 onClickAddBtn() 에서 가져온 todoInputEl.value 값을 넣어준다.
7. 할 일 input 은 처음엔 수정하지 않을테니 readOnly 라는 속성을 추가해준다 (true 로 해야지 읽기만 가능)
8. 할 일 input 에 "todo-item" 이라는 클래스를 추가해준다.
9. fragment 를 만들어서, DOM에 생성하기 전에 먼저 fragmentDOM에 생성 될 엘리먼트들을 추가해준다.
10. fragment 에 먼저 할 일 Input (todoContent) 를 넣어준다.
11. 체크버튼, 수정버튼, 삭제버튼, 저장버튼 을 생성해주도록 한다.
12. todoDiv 에 만들어놓은 fragment 들을 모두 추가한다.
13. todoListEl (할 일들이 추가 될 공간)에 todoDiv 를 추가한다.
14. 추가가 완료된다면, todoInputEl 의 value 값을 빈 값으로 만들어서, 새로 작성하기 쉽게 만들어준다.
appendChild()
- 선택한 요소 안에 자식요소를 추가하는 메서드
- prepend() 메서드와 달리 맨 뒤에 위치하도록 추가됨
createButton() - 속성을 갖고 있는 버튼을 생성하는 함수
createButton(btnId, btnClassName, iconClassName) {
const btn = document.createElement("button");
const icon = document.createElement("i");
icon.classList.add(...iconClassName);
btn.appendChild(icon);
btn.id = btnId;
btn.classList.add(btnClassName);
return btn;
}
1. button 이라는 엘리먼트를 btn 이라는 변수에 저장해서 생성한다.
2. icon 을 추가하기 위해서 i 라는 태그도 icon 이라는 변수에 저장해서 생성한다.
3. icon에 createTodoElement() 에서 파라미터로 보내준 class name 을 추가해준다. (2가지이기때문에 스프레드 신택스를 사용)
4. btn 의 자식 요소에 icon 을 추가해준다.
5. 버튼의 아이디는 createTodoElement()에서 파라미터로 보내준 이름을 id로 주도록 함.
6. 버튼의 클래스 또한 위와 같이 추가함.
7. 만든 btn 을 반환해서 createTodoElement () 로 보내준다.
버튼들마다 이벤트 걸어주기
이벤트리스너 함수
addEvent() {
...
this.todoListEl.addEventListener("click", this.onClickTodoList.bind(this));
}
onClickTodoList () - 생성된 할 일에서 버튼을 클릭하면 발생하는 함수
onClickTodoList(event) {
const { target } = event;
const btn = target.closest("button");
if (!btn) return;
if (btn.matches("#delete-btn")) {
this.deleteTodo(target);
} else if (btn.matches("#edit-btn")) {
this.editTodo(target);
} else if (btn.matches("#save-btn")) {
this.saveTodo(target);
} else if (btn.matches("#complete-btn")) {
this.completeTodo(target);
}
}
1. 클릭한 target 을 먼저 event 에서 찾아 저장해준다.
2. button 안에는 svg 로 아이콘이 있기때문에, button을 클릭해도 아이콘을 클릭한 것처럼 이벤트 버블링이 생기기 때문에 svg가 target 으로 됨
=> closest() 메서드를 사용해 button 을 찾게 해준다.
3. 할 일 요소를 모두 묶고 있기 때문에 Input 을 클릭해도 이벤트가 발생한다
=> 조건문으로 클릭한 target 이 button 이 아니라면, 이벤트가 실행되지 않도록 한다.
4. 버튼들마다 실행 될 이벤트가 다르기 때문에 조건문을 사용해서 위와 같이 맞는 버튼을 찾아 함수를 호출해줍니다.
=> target 을 파라미터로 꼭 보내줍니다.
closest(selectors)
- 주어진 요소와 일치하는 요소를 찾을 때까지, 자기 자신을 포함해 위쪽 (부모 방향, 문서 루트까지)으로 문서 트리를 순회함
- 가장 가까운 element 또는 자기 자신, 일치하는 요소가 없으면 null 을 반환함.
matches(selectors)
- 기준 element가 css 선택자 ( ex. #id, .class, [href = https] 등의 선택자 ) 선택이 되는지 여부를 확인하는 메서드
- selectors : CSS 선택자와 동일한 표현식의 문자열
- 반환값은 boolean 값으로 선택되면 true, 선택되지않으면 false
삭제하는 이벤트
deleteTodo() - 할 일을 삭제해주는 이벤트
deleteTodo(target) {
const todoDiv = target.closest(".todo");
todoDiv.addEventListener("transitionend", () => {
todoDiv.remove();
});
todoDiv.classList.add("delete");
// this.storage.deleteTodo(todoDiv.dataset.id); 3탄에서 함
}
1. createTodoElement () 함수에서 만든 todoDiv 를 클릭한 target에서 closest 메서드를 사용해 찾아주도록 한다.
2. target 의 todoDiv에게 이벤트 리스너를 실행해준다
=> "transitionend" 라는 이벤트를 주어 , 애니메이션이 완료되면 target의 todoDiv (즉 할 일) 을 삭제해주도록 함
3. target의 todoDiv에게 delete 라는 클래스를 추가해, 이벤트를 주도록 하며 서서히 사라지게 해줌
addEventListener("transitionend")
- transition이 완료된 이후에 발생하는 이벤트, transition 완료를 감지한다.
- transition과 함께 사용하는 함수
- addEventListener를 사용하여 이벤트 모니터링 가능
remove()
- 선택한 요소를 DOM 트리에서 제거하는 메서드
- 삭제된 요소와 연관된 데이터나 이벤트도 같이 삭제됨
수정하는 이벤트
editTodo() - 수정 버튼을 클릭하면 발생하는 이벤트
editTodo(target) {
const todoDiv = target.closest(".todo");
const todoInputEl = todoDiv.querySelector("input");
todoInputEl.readOnly = false;
todoInputEl.focus();
todoDiv.classList.add("edit");
}
1. createTodoElement () 함수에서 만든 todoDiv 를 클릭한 target에서 closest 메서드를 사용해 찾아주도록 한다. (삭제와 같음)
2. 읽기 전용으로 되어있던 할 일이 적어져있는 input 을 가져와준다. (todoInputEl)
3. input의 읽기모드를 false 해주어, 수정 가능한 input 으로 바꾸게 해준다.
4. 읽기모드가 false 된다면, input 에 focus 될 수 있도록 focus() 메서드를 사용해준다.
5. todoDiv 에게 edit 라는 클래스를 추가해주어, edit 모드의 css 가 적용될 수 있도록 해준다.
saveTodo() - 수정 완료 버튼을 클릭하면 발생하는 이벤트
saveTodo(target) {
const todoDiv = target.closest(".todo");
todoDiv.classList.remove("edit");
const todoInputEl = todoDiv.querySelector("input");
todoInputEl.readOnly = true;
// const { id } = todoDiv.dataset; 3탄에서 함
// this.storage.editTodo(id, todoInputEl.value); 3탄에서 함
}
1. createTodoElement () 함수에서 만든 todoDiv 를 클릭한 target에서 closest 메서드를 사용해 찾아주도록 한다. (삭제와 같음)
2. edit 클래스를 삭제해주어, edit 모드의 css 를 적용하지 않게 해준다.
3. 수정이 가능한 input 을 가져와준다. (todoInputEl)
4. 수정모드에서 읽기모드로 다시 변경해주도록 함
할 일 체크 이벤트
completeTodo() - 체크 버튼을 클릭하면 발생하는 이벤트
completeTodo(target) {
const todoDiv = target.closest(".todo");
todoDiv.classList.toggle("done");
// const { id } = todoDiv.dataset;
// this.storage.editTodo(
// id,
// "",
// todoDiv.classList.contains("done") ? "DONE" : "TODO"
// ); 3탄
}
1. createTodoElement () 함수에서 만든 todoDiv 를 클릭한 target에서 closest 메서드를 사용해 찾아주도록 한다. (삭제와 같음)
2. todoDiv에게 done 이라는 클래스를 추가해, 다른 할 일과 차별화를 줌 (css적으로)
=> 체크를 풀었다가 체크를 했다가 할 수 있도록 toggle 을 사용함
할 일 상태 카테고리별로 필터링
이벤트리스너 함수
addEvent() {
...
this.addRadioBtnEvent();
}
addRadioBtnEvent() - 카테고리 버튼들에게 클릭 이벤트를 주는 함수
addRadioBtnEvent() {
for (const filterRadioBtnEl of this.filterRadioBtnEls) {
filterRadioBtnEl.addEventListener(
"click",
this.onClickRadioBtn.bind(this)
);
}
}
1. for 문을 통해, 3개의 카테고리 버튼을 가져와 하나씩 클릭 이벤트를 주도록한다
onClickRadioBtn() - 카테고리 클릭하면 필터링 되는 이벤트
onClickRadioBtn(event) {
const { value } = event.target;
this.filterTodo(value);
// window.location.href = `#/${value.toLowerCase()}`; 2탄
}
1. event.target.value 를 value 라는 이름으로 저장해준다.
=> 아래와 같이 클릭한 카테고리 버튼의 value 값이 나옴
ALL TODO DONE
2. filterTodo() 라는 함수를 만들어서 호출해줌
filterTodo() - 필터링 해주는 함수
filterTodo(status) {
const todoDivEls = this.todoListEl.querySelectorAll("div.todo");
for (const todoDivEl of todoDivEls) {
switch (status) {
case "ALL":
todoDivEl.style.display = "flex";
break;
case "DONE":
todoDivEl.style.display = todoDivEl.classList.contains("done")
? "flex"
: "none";
break;
case "TODO":
todoDivEl.style.display = todoDivEl.classList.contains("done")
? "none"
: "flex";
break;
}
}
}
1. 추가 된 할 일들을 todoDivEls 라는 변수로 담아준다. (배열로 담아짐)
2. for 문을 사용해서 할 일들 하나하나에게 switch 문을 통해 상태에 따라 카테고리에 보여지는 모습을 정해준다.
- case "ALL" 일 때
: display 가 원래 none 으로 되어있는데 모두 "flex" 로 해서, 보이도록 한다.
- case "DONE" 일 때
: 할 일에게 "done" 이라는 클래스가 포함되어있다면 "flex"를 주어 "DONE" 카테고리에서 보이도록 한다.
- case "TODO" 일 때
: 할 일에게 "done" 이라는 클래스가 포함되어있다면 "none" 으로 주어 현재 진행중인 할 일만 보여주도록 함.
for ...of 반복문
- ES6에 추가된 새로운 컬렉션 전용 반복 구문
- 반복이 가능한 객체의 모든 원소를 하나씩 추출하여 변수에 담아 반복문을 수행하는 문법
전체 기능 구현 코드
addEventListener("DOMContentLoaded")
- 브라우저가 HTML을 전부 읽고 DOM Tree 를 완성하는 즉시 발생
- DOM Tree 가 다 만들어진 후에 DOM에 접근이 가능하기때문에, DOM이 생성되기전 DOM을 조작하는 자바스크립트 코드가 실행되어 원하지 않는 결과를 내는것을 막을 수 있다.
- 로딩 측면에서 DOMContentLoaded가 우위에 있음
'Javascript > 응용프로젝트' 카테고리의 다른 글
[3] 자바스크립트로 투두리스트(todo-list) 만들기 + localStorage에 대해서 (0) | 2023.01.09 |
---|---|
[2] 자바스크립트로 투두리스트(todo-list) 만들기 (0) | 2023.01.09 |
자바스크립트로 그림판 만들기 (+ 다운로드 기능) (0) | 2023.01.05 |
자바스크립트로 BMI 계산기 구현하기 (0) | 2023.01.03 |
자바스크립트로 Date-Picker 구현하기 (0) | 2023.01.03 |