-
Notifications
You must be signed in to change notification settings - Fork 217
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
[10기 김형남] TodoList with CRUD #215
base: hyoungnam
Are you sure you want to change the base?
Changes from all commits
08c6542
5e6d51e
7b548bb
45e3e62
48331a5
43ff3d1
a7b0419
07280d5
e5887c5
e6c0e5c
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 @@ | ||
.DS_Store |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -34,5 +34,6 @@ <h1>TODOS</h1> | |
</div> | ||
</main> | ||
</div> | ||
<script src="./src/index.js" type="module"></script> | ||
</body> | ||
</html> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import TodoInput from "./components/TodoInput.js"; | ||
import TodoList from "./components/TodoList/index.js"; | ||
import TodoTotal from "./components/TodoTotal.js"; | ||
import TodoFilters from "./components/TodoFilters.js"; | ||
|
||
import { $ } from "./utils/selectors.js"; | ||
|
||
export default function App(store) { | ||
store.addObserver(new TodoInput(store, $(".new-todo"))); | ||
store.addObserver(new TodoList(store, $(".todo-list"))); | ||
store.addObserver(new TodoTotal(store, $(".todo-count"))); | ||
store.addObserver(new TodoFilters(store, $(".filters"))); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { buildViewState } from "../utils/helpers.js"; | ||
import { $, isInClassList } from "../utils/selectors.js"; | ||
|
||
export default class TodoList { | ||
constructor(store, $app) { | ||
this.store = store; | ||
this.$app = $app; | ||
this.mount(); | ||
} | ||
mount() { | ||
this.$app.addEventListener("click", (e) => { | ||
const isAll = isInClassList("all", e.target); | ||
const isActive = isInClassList("active", e.target); | ||
const isCompleted = isInClassList("completed", e.target); | ||
const hash = e.target.hash ? e.target.hash.substring(1) : "all"; | ||
if (isAll || isActive || isCompleted) { | ||
buildViewState(hash, this.store, e); | ||
} | ||
}); | ||
} | ||
render() {} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
const CIPHER = 1000; | ||
|
||
export default class TodoInput { | ||
constructor(store, $app) { | ||
this.store = store; | ||
this.$app = $app; | ||
this.mount(); | ||
} | ||
mount() { | ||
this.$app.addEventListener("keypress", this.handleInputValue.bind(this)); | ||
} | ||
render() {} | ||
handleInputValue(e) { | ||
if (e.key === "Enter") { | ||
const prevState = this.store.getState(); | ||
const newTodo = { | ||
id: Math.floor(Math.random() * CIPHER), | ||
content: e.target.value, | ||
status: "active", | ||
}; | ||
const newState = { ...prevState, todos: [...prevState.todos, newTodo] }; | ||
this.store.setState(newState); | ||
e.target.value = ""; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
export const TOGGLE = "toggle"; | ||
export const DELETE = "delete"; | ||
export const EDIT = "edit"; | ||
export const EDITING = "editing"; | ||
export const DESTROY = "destroy"; | ||
export const COMPLETED = "completed"; | ||
export const CHECKED = "checked"; | ||
export const FALSE = "false"; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
//prettier-ignore | ||
import { TOGGLE, DESTROY, DELETE, EDITING, EDIT } from "./constant.js"; | ||
import { filterTodos } from "../../utils/helpers.js"; | ||
import { $, isInClassList } from "../../utils/selectors.js"; | ||
|
||
//MOUNT HELPER | ||
export function toggleTodoItem(e, store) { | ||
const isToggle = isInClassList(TOGGLE, e.target); | ||
if (isToggle) { | ||
buildNewState(TOGGLE, store, e); | ||
} | ||
} | ||
export function deleteTodoItem(e, store) { | ||
const isDestroy = isInClassList(DESTROY, e.target); | ||
if (isDestroy) { | ||
buildNewState(DELETE, store, e); | ||
} | ||
} | ||
export function setEditingMode(e) { | ||
const isList = e.target.closest("li"); | ||
if (isList) { | ||
isList.classList.add(EDITING); | ||
} | ||
} | ||
export function editSelectedTodo(e, store) { | ||
const isEditing = isInClassList(EDIT, e.target); | ||
if (isEditing && e.key === "Enter") { | ||
buildNewState(EDIT, store, e); | ||
e.target.closest("li").classList.remove(EDITING); | ||
} | ||
if (isEditing && e.key === "Escape") { | ||
const currentValue = $(".label").textContent; | ||
e.target.value = currentValue; | ||
e.target.closest("li").classList.remove(EDITING); | ||
} | ||
} | ||
|
||
//VIEW HELPER | ||
export function buildListTodos(store) { | ||
const { todos, view } = store.getState(); | ||
return view === "all" ? todos : filterTodos(todos, view); | ||
} | ||
|
||
//STATE HELPER | ||
function buildNewState(op, store, e) { | ||
const OPERATIONS = { | ||
toggle: toggleTodoStatus, | ||
delete: deleteTodo, | ||
edit: editTodo, | ||
}; | ||
const prevState = store.getState(); | ||
const targetId = Number(e.target.closest("li").getAttribute("dataset-id")); | ||
|
||
const newTodos = OPERATIONS[op](prevState, targetId, e); | ||
|
||
const newState = { ...prevState, todos: newTodos }; | ||
store.setState(newState); | ||
} | ||
|
||
//TODO - STATUS | ||
function toggleTodoStatus(prevState, targetId, e) { | ||
const newStatus = e.target.checked ? "completed" : "active"; | ||
const newTodos = prevState.todos.map((todo) => { | ||
if (todo.id === targetId) { | ||
return { ...todo, status: newStatus }; | ||
} | ||
return todo; | ||
}); | ||
return newTodos; | ||
} | ||
//TODO - DELETE | ||
function deleteTodo(prevState, targetId) { | ||
const newTodos = prevState.todos.filter((todo) => { | ||
return todo.id !== targetId; | ||
}); | ||
return newTodos; | ||
} | ||
|
||
//TODO - UPDATE | ||
function editTodo(prevState, targetId, e) { | ||
const newTodos = prevState.todos.map((todo) => { | ||
if (todo.id === targetId) { | ||
return { ...todo, content: e.target.value }; | ||
} | ||
return todo; | ||
}); | ||
return newTodos; | ||
} | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
//prettier-ignore | ||
import { buildListTodos, editSelectedTodo, toggleTodoItem, deleteTodoItem, setEditingMode } from "./helper.js"; | ||
import { COMPLETED, CHECKED, FALSE } from "./constant.js"; | ||
|
||
export default class TodoList { | ||
constructor(store, $app) { | ||
this.store = store; | ||
this.$app = $app; | ||
this.mount(); | ||
this.render(); | ||
} | ||
mount() { | ||
//prettier-ignore | ||
this.$app.addEventListener("keydown", (e) => editSelectedTodo(e, this.store)); | ||
this.$app.addEventListener("dblclick", (e) => setEditingMode(e)); | ||
this.$app.addEventListener("click", (e) => toggleTodoItem(e, this.store)); | ||
this.$app.addEventListener("click", (e) => deleteTodoItem(e, this.store)); | ||
} | ||
render() { | ||
this.$app.innerHTML = buildListTodos(this.store) | ||
.map(({ id, content, status, edit }) => { | ||
const isChecked = status === COMPLETED ? CHECKED : FALSE; | ||
return `<li dataset-id=${id} class="${status} ${edit}"> | ||
<div class="view"> | ||
<input class="toggle" type="checkbox" ${isChecked}> | ||
<label class="label">${content}</label> | ||
<button class="destroy" ></button> | ||
</div> | ||
<input class="edit" value="${content}"> | ||
</li>`; | ||
}) | ||
.join(""); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,15 @@ | ||||||
import { filterTodos } from "../utils/helpers.js"; | ||||||
|
||||||
export default class TodoTotal { | ||||||
constructor(store, $app) { | ||||||
this.store = store; | ||||||
this.$app = $app; | ||||||
} | ||||||
render() { | ||||||
const { view, todos } = this.store.getState(); | ||||||
//prettier-ignore | ||||||
const curViewTodos = view === "all" ? todos | ||||||
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.
Suggested change
reduce 메소드를 사용할때만 cur 사용하기 |
||||||
: filterTodos(todos, view); | ||||||
this.$app.innerHTML = `총 <strong>${curViewTodos.length}</strong> 개</span>`; | ||||||
} | ||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import App from "./App.js"; | ||
import Store from "./store/index.js"; | ||
new App(new Store()); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
export const get = (key, defaultState) => | ||
JSON.parse(localStorage.getItem(key)) || defaultState; | ||
export const set = (key, newState) => | ||
localStorage.setItem(key, JSON.stringify(newState)); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import { get, set } from "../storage/index.js"; | ||
const USER = "user"; | ||
|
||
export default class Store { | ||
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. 형남님 코드를 보면서 많이 배울 수 있었습니다 .!! 감사합니다 👍 |
||
constructor() { | ||
this.state = get(USER, { todos: [], view: "all" }); | ||
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. store에 state를 저장하는 것은 결합이 강하고 역할이 많으니 좀 더 분리 |
||
this.observers = []; | ||
} | ||
addObserver(observer) { | ||
this.observers.push(observer); | ||
} | ||
observing() { | ||
this.observers.forEach((observer) => observer.render()); | ||
} | ||
//GET | ||
getState() { | ||
return this.state; | ||
} | ||
//SET | ||
setState(newState) { | ||
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. 비즈니스 로직은 상태를 가진 계층에서 처리하기 |
||
this.state = { ...this.state, ...newState }; | ||
set(USER, this.state); | ||
this.observing(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
export const TOGGLE = "toggle"; | ||
export const DELETE = "delete"; | ||
export const EDIT = "edit" | ||
export const EDITING = "editing" | ||
export const DESTORY = "destory" | ||
export const ENTER = "Enter" | ||
export const ESCAPE = "Escape" | ||
export const COMPLETED = "completed" | ||
export const CHECKED = "checked" | ||
export const FALSE = 'false' |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import { $ } from "./selectors.js"; | ||
|
||
export function buildViewState(op, store, e) { | ||
$(".selected").classList.remove("selected"); | ||
e.target.className = `${op} selected`; | ||
|
||
const state = store.getState(); | ||
const newState = { ...state, view: op }; | ||
store.setState(newState); | ||
} | ||
|
||
export function filterTodos(todos, view) { | ||
return todos.filter((todo) => { | ||
if (todo.status === view) return todo; | ||
}); | ||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,3 @@ | ||||||
export const $ = (node) => document.querySelector(node); | ||||||
export const $all = (node) => document.querySelectorAll(node) | ||||||
export const isInClassList = (tagName, eventTarget) => eventTarget.classList.contains(tagName) | ||||||
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.
Suggested change
좀 더 명확하게 적기 |
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.
helper라는 함수 이름 아래 MOUNT, VIEW, STATE 로직이 다 모여있음. 역할 분리 필요하며 특히 state로직은 state 계층으로