Skip to content

[10기 이지은] TodoList with CRUD #219

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,15 @@

## 🎯 요구사항

- [ ] todo list에 todoItem을 키보드로 입력하여 추가하기
- [ ] todo list의 체크박스를 클릭하여 complete 상태로 변경 (li tag 에 completed class 추가, input 태그에 checked 속성 추가)
- [ ] todo list의 x버튼을 이용해서 해당 엘리먼트를 삭제
- [ ] todo list를 더블클릭했을 때 input 모드로 변경 (li tag 에 editing class 추가) 단 이때 수정을 완료하지 않은 상태에서 esc키를 누르면 수정되지 않은 채로 다시 view 모드로 복귀
- [ ] todo list의 item갯수를 count한 갯수를 리스트의 하단에 보여주기
- [ ] todo list의 상태값을 확인하여, 해야할 일과, 완료한 일을 클릭하면 해당 상태의 아이템만 보여주기
- [x] todo list에 todoItem을 키보드로 입력하여 추가하기
- [x] todo list의 체크박스를 클릭하여 complete 상태로 변경 (li tag 에 completed class 추가, input 태그에 checked 속성 추가)
- [x] todo list의 x버튼을 이용해서 해당 엘리먼트를 삭제
- [x] todo list를 더블클릭했을 때 input 모드로 변경 (li tag 에 editing class 추가) 단 이때 수정을 완료하지 않은 상태에서 esc키를 누르면 수정되지 않은 채로 다시 view 모드로 복귀
- [x] todo list의 item갯수를 count한 갯수를 리스트의 하단에 보여주기
- [x] todo list의 상태값을 확인하여, 해야할 일과, 완료한 일을 클릭하면 해당 상태의 아이템만 보여주기

## 🎯🎯 심화 요구사항
- [ ] localStorage에 데이터를 저장하여, TodoItem의 CRUD를 반영하기. 따라서 새로고침하여도 저장된 데이터를 확인할 수 있어야 함
- [x] localStorage에 데이터를 저장하여, TodoItem의 CRUD를 반영하기. 따라서 새로고침하여도 저장된 데이터를 확인할 수 있어야 함

<br/>

Expand Down
1 change: 1 addition & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>이벤트 - TODOS</title>
<link rel="stylesheet" href="./src/css/style.css" />
<script type="module" src="./src/app.js" defer></script>
</head>
<body>
<div class="todoapp">
Expand Down
11 changes: 11 additions & 0 deletions src/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import TodoApp from './components/TodoApp.js';
import LocalStorage from './components/LocalStorage.js';

const LS_KEY = 'TODOS';
window.addEventListener('DOMContentLoaded', () => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DMContentloaded라는 이벤트를 활용하는 건 처음 봤네요! 혹시 그냥 html 스크립트 파일을 등록하는 것과는 무슨 차이가 있고 추가하신 이유가 있을까요?

const storage = new LocalStorage(LS_KEY);
const savedData = storage.getItems();

const todoApp = new TodoApp(storage);
todoApp.init(savedData);
});
13 changes: 13 additions & 0 deletions src/components/LocalStorage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export default class LocalStorage {
constructor(key) {
this.key = key;
}
saveItems = (todoItems) => {
localStorage.setItem(this.key, JSON.stringify(todoItems));
};

getItems = () => {
const parsedData = localStorage.getItem(this.key);
return parsedData && JSON.parse(parsedData);
};
}
111 changes: 111 additions & 0 deletions src/components/TodoApp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import TodoItem from './TodoItem.js';
import TodoInput from './TodoInput.js';
import TodoList from './TodoList.js';
import TodoCount from './TodoCount.js';
import { TodoFilter, FilterType } from './TodoFilter.js';

export default function TodoApp(storage) {
this.storage = storage;
this.todoItems = [];
let id = 0;

this.init = (savedData) => {
this.todoItems = savedData ?? [];
id = savedData.length ?? 0;

savedData && this.setState(savedData);
};

this.setState = (updatedItems) => {
todoList.setState(updatedItems);
todoCount.setState(updatedItems);
};

const onAdd = (contents) => {
const newTodoItem = new TodoItem(contents, ++id);
this.todoItems.push(newTodoItem);
this.setState(this.todoItems);
this.storage.saveItems(this.todoItems);
};

const onComplete = (id) => {
this.todoItems = this.todoItems.map((item) => {
if (item.id === id) {
item.completed = !item.completed;
}
return item;
});
this.setState(this.todoItems);
this.storage.saveItems(this.todoItems);
};

const onDelete = (id) => {
this.todoItems = this.todoItems.filter((item) => {
return item.id !== id;
});
this.setState(this.todoItems);
this.storage.saveItems(this.todoItems);
};

const onEdit = (id) => {
this.todoItems = this.todoItems.map((item) => {
if (item.id === id) {
item.editing = !item.editing;
}
return item;
});
this.setState(this.todoItems);
this.storage.saveItems(this.todoItems);
};

const onUpdate = (e, id) => {
if (e.key === 'Enter') {
this.todoItems = this.todoItems.map((item) => {
if (item.id === id) {
item.contents = e.target.value;
item.editing = false;
}
return item;
});
this.setState(this.todoItems);
this.storage.saveItems(this.todoItems);
}
if (e.key === 'Escape') {
this.todoItems = this.todoItems.map((item) => {
if (item.id === id) {
item.editing = false;
}
return item;
});
this.setState(this.todoItems);
this.storage.saveItems(this.todoItems);
}
};

const onFilter = (type) => {
if (type === FilterType.all) {
this.setState(this.todoItems);
} else if (type === FilterType.active) {
const activeItems = this.todoItems.filter(
(item) => item.completed === false
);
this.setState(activeItems);
} else if (type === FilterType.completed) {
const completedItems = this.todoItems.filter(
(item) => item.completed === true
);
this.setState(completedItems);
}
};

const todoInput = new TodoInput();
todoInput.setEventListener(onAdd);

const todoList = new TodoList();
todoList.setEventListener(onComplete, onDelete, onEdit, onUpdate);

const todoCount = new TodoCount();

const todoFilter = new TodoFilter();
todoFilter.setEventListener(onFilter);
}
7 changes: 7 additions & 0 deletions src/components/TodoCount.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function TodoCount() {
this.todoCount = document.querySelector('.todo-count strong');

this.setState = (todoItems) => {
this.todoCount.textContent = todoItems.length;
};
}
37 changes: 37 additions & 0 deletions src/components/TodoFilter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
export const FilterType = Object.freeze({
all: 'all',
active: 'active',
completed: 'completed',
});

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Object.freeze()로 일종의 상수처리를 하는 방법 배워갑니다~:thumbsup:

Comment on lines +1 to +5

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저는 이 방법은 생각해보지 못했었는데, 좋은 아이디어 하나 배워갑니다~!


export function TodoFilter() {
this.filters = document.querySelector('.filters');

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

document.querySelector를 컴포넌트로 선언하는 방식도 좋을 것 같아요! (https://edu.nextstep.camp/s/RZqgJhQO/ls/LWPmQkbP)

저도 이번에 처음으로 써봤는데, 유용한 것 같습니다!!

this.filtersBtn = this.filters.querySelectorAll('a');

this.filters.addEventListener('click', (event) => this.handleClick(event));

this.setEventListener = (onFilter) => {
this.onFilter = onFilter;
};

this.handleClick = (event) => {
const type = event.target.className;

this.removeSelectedClass();
this.addSelectedClass(event.target);

this.onFilter && this.onFilter(type);
};

this.removeSelectedClass = () => {
this.filtersBtn.forEach((item) => {
if (item.classList.contains('selected')) {
item.classList.remove('selected');
Copy link

@nerdyinu nerdyinu Jul 12, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이부분은 toggle('selected')를 써보시는게 어떨까요?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

selected를 제거하는 부분이라 없는 경우에는 추가가 되서 toggle을 쓸 수가 없었어요 ㅎㅎㅎ

}
});
};

this.addSelectedClass = (target) => {
target.classList.add('selected');
};
}
18 changes: 18 additions & 0 deletions src/components/TodoInput.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// 입력받는 컴포넌트
export default function TodoInput() {
const todoInput = document.querySelector('#new-todo-title');

todoInput.addEventListener('keydown', (event) => this.handleOnAdd(event));

this.setEventListener = (onAdd) => {
this.onAdd = onAdd;
};

this.handleOnAdd = (event) => {
if (event.key === 'Enter') {
const newTodoTarget = event.target;
this.onAdd && this.onAdd(newTodoTarget.value);
newTodoTarget.value = '';
}
};
}
8 changes: 8 additions & 0 deletions src/components/TodoItem.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export default class TodoItem {
constructor(contents, id) {
this.id = id;
this.contents = contents;
this.completed = false;
this.editing = false;
}
}
60 changes: 60 additions & 0 deletions src/components/TodoList.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// todoList 보여주는 컴포넌트
export default function TodoList() {
const todoList = document.querySelector('#todo-list');

todoList.addEventListener('click', (event) => this.handleClick(event));
todoList.addEventListener('dblclick', (event) => this.handleDblClick(event));
todoList.addEventListener('keydown', (event) => this.handleKeydown(event));

this.setEventListener = (onComplete, onDelete, onEdit, onUpdate) => {
this.onComplete = onComplete;
this.onDelete = onDelete;
this.onEdit = onEdit;
this.onUpdate = onUpdate;
};

this.setState = (updatedTodoItems) => {
this.todoItems = updatedTodoItems;
this.render(this.todoItems);
};

this.render = (items) => {
const template = items.map((item) => {
return `
<li class="${item.completed ? 'completed' : ''} ${
item.editing ? 'editing' : ''
}" data-id="${item.id}">
<div class="view">
<input class="toggle" type="checkbox" ${
item.completed ? 'checked' : ''
}/>
<label class="label">${item.contents}</label>
<button class="destroy"></button>
</div>
<input class="edit" value="${item.contents}" />
</li>
`;
});
todoList.innerHTML = template.join('');
};

this.handleKeydown = (event) => {
const id = parseInt(event.target.parentNode.dataset.id);
this.onUpdate && this.onUpdate(event, id);
};

this.handleDblClick = (event) => {
const id = parseInt(event.target.parentNode.parentNode.dataset.id);
this.onEdit && this.onEdit(id);
};

this.handleClick = (event) => {
const id = parseInt(event.target.parentNode.parentNode.dataset.id);
if (event.target.className === 'toggle') {
this.onComplete && this.onComplete(id);
}
if (event.target.className === 'destroy') {
this.onDelete && this.onDelete(id);
}
};
}