Skip to content
Open
Show file tree
Hide file tree
Changes from 9 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
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)

좀 더 명확하게 적기