-
Notifications
You must be signed in to change notification settings - Fork 216
[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
base: main
Are you sure you want to change the base?
Changes from all commits
9d0e1be
b0b3d0f
1527999
78984e1
fe59829
ec76522
a6a8d2e
0e26126
1f5766e
000c601
baf0d2e
f4b3183
578712f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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', () => { | ||
const storage = new LocalStorage(LS_KEY); | ||
const savedData = storage.getItems(); | ||
|
||
const todoApp = new TodoApp(storage); | ||
todoApp.init(savedData); | ||
}); |
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); | ||
}; | ||
} |
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); | ||
} |
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; | ||
}; | ||
} |
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', | ||
}); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Object.freeze()로 일종의 상수처리를 하는 방법 배워갑니다~:thumbsup:
Comment on lines
+1
to
+5
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 저는 이 방법은 생각해보지 못했었는데, 좋은 아이디어 하나 배워갑니다~! |
||
|
||
export function TodoFilter() { | ||
this.filters = document.querySelector('.filters'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이부분은 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. selected를 제거하는 부분이라 없는 경우에는 추가가 되서 toggle을 쓸 수가 없었어요 ㅎㅎㅎ |
||
} | ||
}); | ||
}; | ||
|
||
this.addSelectedClass = (target) => { | ||
target.classList.add('selected'); | ||
}; | ||
} |
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 = ''; | ||
} | ||
}; | ||
} |
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; | ||
} | ||
} |
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); | ||
} | ||
}; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DMContentloaded라는 이벤트를 활용하는 건 처음 봤네요! 혹시 그냥 html 스크립트 파일을 등록하는 것과는 무슨 차이가 있고 추가하신 이유가 있을까요?