Skip to content
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

Open
wants to merge 10 commits into
base: hyoungnam
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.DS_Store
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 @@ -34,5 +34,6 @@ <h1>TODOS</h1>
</div>
</main>
</div>
<script src="./src/index.js" type="module"></script>
</body>
</html>
13 changes: 13 additions & 0 deletions src/App.js
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")));
}
22 changes: 22 additions & 0 deletions src/components/TodoFilters.js
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() {}
}
26 changes: 26 additions & 0 deletions src/components/TodoInput.js
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 = "";
}
}
}
8 changes: 8 additions & 0 deletions src/components/TodoList/constant.js
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";
88 changes: 88 additions & 0 deletions src/components/TodoList/helper.js
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;
}
Comment on lines +1 to +88
Copy link
Author

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 계층으로

34 changes: 34 additions & 0 deletions src/components/TodoList/index.js
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("");
}
}
15 changes: 15 additions & 0 deletions src/components/TodoTotal.js
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
Copy link
Author

Choose a reason for hiding this comment

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

Suggested change
const curViewTodos = view === "all" ? todos
const currentViewTodos = view === "all" ? todos

reduce 메소드를 사용할때만 cur 사용하기

: filterTodos(todos, view);
this.$app.innerHTML = `총 <strong>${curViewTodos.length}</strong> 개</span>`;
}
}
3 changes: 3 additions & 0 deletions src/index.js
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());
4 changes: 4 additions & 0 deletions src/storage/index.js
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));
25 changes: 25 additions & 0 deletions src/store/index.js
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 {

Choose a reason for hiding this comment

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

형남님 코드를 보면서 많이 배울 수 있었습니다 .!! 감사합니다 👍

constructor() {
this.state = get(USER, { todos: [], view: "all" });
Copy link
Author

Choose a reason for hiding this comment

The 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) {
Copy link
Author

Choose a reason for hiding this comment

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

비즈니스 로직은 상태를 가진 계층에서 처리하기

this.state = { ...this.state, ...newState };
set(USER, this.state);
this.observing();
}
}
10 changes: 10 additions & 0 deletions src/utils/constants.js
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'
16 changes: 16 additions & 0 deletions src/utils/helpers.js
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;
});
}
3 changes: 3 additions & 0 deletions src/utils/selectors.js
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)
Copy link
Author

@hyoungnam hyoungnam Jul 26, 2021

Choose a reason for hiding this comment

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

Suggested change
export const isInClassList = (tagName, eventTarget) => eventTarget.classList.contains(tagName)
export const isClassListContains = (tagName, eventTarget) => eventTarget.classList.contains(tagName)

좀 더 명확하게 적기