diff --git a/vanilla-todo/index.html b/vanilla-todo/index.html index 168f299..f9b3d5c 100644 --- a/vanilla-todo/index.html +++ b/vanilla-todo/index.html @@ -7,7 +7,6 @@
- - + diff --git a/vanilla-todo/src/App.js b/vanilla-todo/src/App.js deleted file mode 100644 index 908755e..0000000 --- a/vanilla-todo/src/App.js +++ /dev/null @@ -1,86 +0,0 @@ -import Component from "./Component.js"; - -import Title from "./components/Title.js"; -import AddForm from "./components/AddForm.js"; -import TodoList from "./components/TodoList.js"; - -class App extends Component { - initState() { - return { - todos: [], - editing: null, - }; - } - html() { - return ` -
-
- - `; - } - declare() { - const { todos, editing } = this.state; - new Title($app.querySelector("#Title")); - new AddForm($app.querySelector("#AddForm"), { - addTodo: this.addTodo.bind(this), - }); - new TodoList($app.querySelector("#TodoList"), { - todos, - editing, - deleteTodo: this.deleteTodo.bind(this), - toggleTodo: this.toggleTodo.bind(this), - editTodo: this.editTodo.bind(this), - startEditing: this.startEditing.bind(this), - }); - } - findTodo(todoId) { - const { todos } = this.state; - const todoIndex = todos.findIndex((value) => value.id === todoId); - const todo = todos.find((value) => value.id === todoId); - return { todo, index: todos[todoIndex] }; - } - addTodo(content) { - const newTodo = { - content, - id: Date.now(), - done: false, - }; - this.setState({ todos: [...this.state.todos, newTodo] }); - } - deleteTodo(todoId) { - this.setState({ - todos: this.state.todos.filter((value) => value.id !== todoId), - }); - } - toggleTodo(todoId) { - const found = this.findTodo(todoId); - const oldTodo = found.todo; - const newTodo = { - ...oldTodo, - done: !oldTodo.done, - }; - - const todos = [...this.state.todos]; - todos.splice(found.index, 1, newTodo); - this.setState({ todos: todos }); - } - editTodo(content) { - const { todos, editing } = this.state; - const newTodo = { - ...editing, - content, - }; - - const newTodos = [...todos]; - newTodos.splice(this.findTodo(editing.id).index, 1, newTodo); - - this.setState({ editing: null, todos: newTodos }); - } - startEditing(todoId) { - const editingTodo = this.findTodo(todoId).todo; - this.setState({ editing: editingTodo }); - } -} - -const $app = document.querySelector("#App"); -new App($app); diff --git a/vanilla-todo/src/Component.js b/vanilla-todo/src/Component.js deleted file mode 100644 index 10c5c5d..0000000 --- a/vanilla-todo/src/Component.js +++ /dev/null @@ -1,48 +0,0 @@ -import { observable, observe } from "./observer.js"; - -export default class Component { - $root; - #state; - #props; - constructor($root, props) { - this.$root = $root; - this.#props = props; - this.#state = observable(this.initState()); - observe(() => { - this.render(); - this.declare(); - this.setEvent(); - }); - } - initState() { - return {}; - } - get state() { - return this.#state; - } - get props() { - return this.#props; - } - setState(newState) { - for (const [key, value] of Object.entries(newState)) { - if (this.#state[key] === undefined) continue; - this.#state[key] = value; - } - } - html() { - return `
`; - } - render() { - this.$root.innerHTML = this.html(); - } - setEvent() {} - addEvent(eventType, targetSelector, callback) { - document.body.addEventListener(eventType, (event) => { - if (!event.target.closest(targetSelector)) { - return; - } - callback(event); - }); - } - declare() {} -} diff --git a/vanilla-todo/src/components/AddForm.js b/vanilla-todo/src/components/AddForm.js index ea3997d..a514e0a 100644 --- a/vanilla-todo/src/components/AddForm.js +++ b/vanilla-todo/src/components/AddForm.js @@ -1,24 +1,34 @@ -import Component from "../Component.js"; -import Selector from "../constants/Selector.js"; +import Component from "../core/Component.js"; +import { SELECTOR } from "../constants/_index.js"; +import { store, SET_TODO, todoService } from "../store.js"; + export default class AddForm extends Component { html() { return ` -
+
`; } - setEvent() { - const handleSubmitAdding = (event) => { - event.preventDefault(); - - const $addForm = document.getElementById(Selector.ADD_FORM_ID); - const $todoInput = $addForm.querySelector("input"); + event() { + return [ + { + type: "submit", + target: `#${SELECTOR.ADD_FORM_ID}`, + handler: this.handleSubmitAdding.bind(this), + }, + ]; + } + handleSubmitAdding(event) { + event.preventDefault(); - this.props.addTodo($todoInput.value); + const $addForm = document.getElementById(SELECTOR.ADD_FORM_ID); + const $todoInput = $addForm.querySelector("input"); - $todoInput.value = ""; - }; - this.addEvent("submit", `#${Selector.ADD_FORM_ID}`, handleSubmitAdding); + store.dispatch({ + type: SET_TODO, + payload: todoService.addTodo($todoInput.value), + }); + $todoInput.value = ""; } } diff --git a/vanilla-todo/src/components/EditForm.js b/vanilla-todo/src/components/EditForm.js new file mode 100644 index 0000000..3163f75 --- /dev/null +++ b/vanilla-todo/src/components/EditForm.js @@ -0,0 +1,44 @@ +import Component from "../core/Component.js"; +import { SELECTOR } from "../constants/_index.js"; +import { store, SET_TODO, SET_EDITING_NULL, todoService } from "../store.js"; + +export default class EditForm extends Component { + html() { + const { editingId, todos } = store.getState(); + if (!editingId) { + return; + } + + const editingTodo = todos.find((value) => value.id === editingId); + + return ` +
+ + +
+ `; + } + event() { + return [ + { + type: "submit", + target: `.${SELECTOR.EDIT_FORM_CLASSNAME}`, + handler: this.handleSubmitEditing.bind(this), + }, + ]; + } + + handleSubmitEditing(event) { + event.preventDefault(); + store.dispatch({ + type: SET_TODO, + payload: todoService.editTodo({ + content: event.target[0].value, + id: store.getState().editingId, + }), + }); + store.dispatch({ + type: SET_EDITING_NULL, + }); + } +} diff --git a/vanilla-todo/src/components/Filter.js b/vanilla-todo/src/components/Filter.js new file mode 100644 index 0000000..d089eae --- /dev/null +++ b/vanilla-todo/src/components/Filter.js @@ -0,0 +1,27 @@ +import Component from "../core/Component.js"; +import { CONTAINER, TODO_FILTER } from "../constants/_index.js"; +import { SET_FILTER, store } from "../store.js"; +export default class Filter extends Component { + html() { + return ` + + + + `; + } + event() { + return [ + { + type: "click", + target: `#${CONTAINER.FILTER}`, + handler: this.handleClickFilter.bind(this), + }, + ]; + } + handleClickFilter(event) { + store.dispatch({ + type: SET_FILTER, + payload: event.target.id, + }); + } +} diff --git a/vanilla-todo/src/components/Title.js b/vanilla-todo/src/components/Title.js index 42f410d..95d1b19 100644 --- a/vanilla-todo/src/components/Title.js +++ b/vanilla-todo/src/components/Title.js @@ -1,7 +1,3 @@ -import Component from "../Component.js"; -export default class Title extends Component { - html() { - return ` -

To do List

`; - } -} +const Title = () => `

To do List

`; + +export default Title; diff --git a/vanilla-todo/src/components/TodoList.js b/vanilla-todo/src/components/TodoList.js index 3a977f0..b2a286a 100644 --- a/vanilla-todo/src/components/TodoList.js +++ b/vanilla-todo/src/components/TodoList.js @@ -1,99 +1,102 @@ -import Component from "../Component.js"; -import Selector from "../constants/Selector.js"; +import Component from "../core/Component.js"; +import { SELECTOR, CONTAINER, TODO_FILTER } from "../constants/_index.js"; +import EditForm from "./EditForm.js"; +import { store, SET_EDITING, SET_TODO, todoService } from "../store.js"; export default class TodoList extends Component { html() { - return this.props.todos + const { todos, editingId } = store.getState(); + + return this.filteredTodos(todos) .map( ({ id, content, done }) => ` -
  • - +
    +
  • ` ) .join(""); } + filteredTodos(todos) { + const { filter } = store.getState(); + switch (filter) { + case TODO_FILTER.TODO: + return todos.filter((todo) => !todo.done); + case TODO_FILTER.DONE: + return todos.filter((todo) => todo.done); + case TODO_FILTER.ALL: + return todos; + } + } + mounted() { + const { editingId } = store.getState(); - setEvent() { - const { deleteTodo, toggleTodo, editTodo, startEditing } = this.props; - - const getTodoElementId = (todoElement) => { - return Number(todoElement.id); - }; - - const handleClickDelete = (event) => { - const { - target: { parentNode: $targetTodo }, - } = event; - - deleteTodo(getTodoElementId($targetTodo)); - }; - - const handleChangeToggle = (event) => { - const { - target: { parentNode: $labelElement }, - } = event; + if (!editingId) { + return; + } - const $targetTodo = $labelElement.parentNode; + const $editForm = document.querySelector( + `#${CONTAINER.EDIT_FORM}-${editingId}` + ); - toggleTodo(getTodoElementId($targetTodo)); - }; + new EditForm($editForm); + } - const handleSubmitEditing = (event) => { - event.preventDefault(); + event() { + return [ + { + type: "click", + target: `.${SELECTOR.EDIT_BUTTON_CLASSNAME}`, + handler: this.handleClickStartEditing.bind(this), + }, + { + type: "click", + target: `.${SELECTOR.DELETE_BUTTON_CLASSNAME}`, + handler: this.handleClickDelete.bind(this), + }, + { + type: "change", + target: `.${SELECTOR.TOGGLE_CLASSNAME}`, + handler: this.handleChangeToggle.bind(this), + }, + ]; + } - const content = event.target[0].value; - editTodo(content); - }; + getTodoIdFrom($targetElement) { + const $todoElement = $targetElement.closest(".todoItem"); + return Number($todoElement.id); + } - const handleClickStartEditing = (event) => { - const { - target: { parentNode: $targetTodo }, - } = event; + handleClickStartEditing(event) { + const { target } = event; + store.dispatch({ type: SET_EDITING, payload: this.getTodoIdFrom(target) }); + } - startEditing(getTodoElementId($targetTodo)); - }; + handleClickDelete(event) { + const { target } = event; + store.dispatch({ + type: SET_TODO, + payload: todoService.deleteTodo(this.getTodoIdFrom(target)), + }); + } - this.addEvent( - "submit", - `.${Selector.EDIT_FORM_CLASSNAME}`, - handleSubmitEditing - ); - this.addEvent( - "click", - `.${Selector.EDIT_BUTTON_CLASSNAME}`, - handleClickStartEditing - ); - this.addEvent( - "click", - `.${Selector.DELETE_BUTTON_CLASSNAME}`, - handleClickDelete - ); - this.addEvent( - "change", - `.${Selector.TOGGLE_CLASSNAME}`, - handleChangeToggle - ); + handleChangeToggle(event) { + const { target } = event; + store.dispatch({ + type: SET_TODO, + payload: todoService.toggleTodo(this.getTodoIdFrom(target)), + }); } } diff --git a/vanilla-todo/src/constants/Container.js b/vanilla-todo/src/constants/Container.js new file mode 100644 index 0000000..e8ab087 --- /dev/null +++ b/vanilla-todo/src/constants/Container.js @@ -0,0 +1,9 @@ +const CONTAINER = { + ADD_FORM: "AddForm", + EDIT_FORM: "EditForm", + TITLE: "Title", + TODO_LIST: "TodoList", + FILTER: "Filter", +}; + +export default CONTAINER; diff --git a/vanilla-todo/src/constants/CustomEvent.js b/vanilla-todo/src/constants/CustomEvent.js new file mode 100644 index 0000000..1c1bd9e --- /dev/null +++ b/vanilla-todo/src/constants/CustomEvent.js @@ -0,0 +1,5 @@ +const CUSTOM_EVENT = { + LOCATION_CHANGE: "locationChange", +}; + +export default CUSTOM_EVENT; diff --git a/vanilla-todo/src/constants/Selector.js b/vanilla-todo/src/constants/Selector.js index 2a3c2be..6902b87 100644 --- a/vanilla-todo/src/constants/Selector.js +++ b/vanilla-todo/src/constants/Selector.js @@ -1,7 +1,11 @@ -export default class Selector { - static ADD_FORM_ID = "addForm"; - static EDIT_FORM_CLASSNAME = "editForm"; - static EDIT_BUTTON_CLASSNAME = "editing"; - static TOGGLE_CLASSNAME = "toggle"; - static DELETE_BUTTON_CLASSNAME = "delete"; -} +const SELECTOR = { + ADD_FORM_ID: "addForm", + EDIT_FORM_CLASSNAME: "editForm", + EDIT_BUTTON_CLASSNAME: "editing", + TOGGLE_CLASSNAME: "toggle", + DELETE_BUTTON_CLASSNAME: "delete", + FILTER_TODO_ID: "filterTodo", + FILTER_DONE_ID: "filterDone", +}; + +export default SELECTOR; diff --git a/vanilla-todo/src/constants/TodoFilter.js b/vanilla-todo/src/constants/TodoFilter.js new file mode 100644 index 0000000..ed043ce --- /dev/null +++ b/vanilla-todo/src/constants/TodoFilter.js @@ -0,0 +1,7 @@ +const TODO_FILTER = { + TODO: "TODO", + DONE: "DONE", + ALL: "ALL", +}; + +export default TODO_FILTER; diff --git a/vanilla-todo/src/constants/_index.js b/vanilla-todo/src/constants/_index.js new file mode 100644 index 0000000..9573758 --- /dev/null +++ b/vanilla-todo/src/constants/_index.js @@ -0,0 +1,4 @@ +export { default as SELECTOR } from "./Selector.js"; +export { default as CONTAINER } from "./Container.js"; +export { default as CUSTOM_EVENT } from "./CustomEvent.js"; +export { default as TODO_FILTER } from "./TodoFilter.js"; diff --git a/vanilla-todo/src/core/Component.js b/vanilla-todo/src/core/Component.js new file mode 100644 index 0000000..5d61dd4 --- /dev/null +++ b/vanilla-todo/src/core/Component.js @@ -0,0 +1,59 @@ +import { deepCopy } from "../util/deepCopy.js"; +import { observable, observe } from "./observer.js"; + +class Component { + $root; + #state; + #props; + constructor($root, props) { + this.$root = $root; + this.#props = props; + this.#state = observable(this.initState()); + observe(() => { + this.#render(); + this.#setEvent(); + this.mounted(); + }); + } + get state() { + return deepCopy(this.#state); + } + get props() { + return deepCopy(this.#props); + } + setState(newState) { + for (const [key, value] of Object.entries(newState)) { + if (!this.#state.hasOwnProperty(key)) continue; + this.#state[key] = value; + } + } + #render() { + this.$root.innerHTML = this.html() || ""; + } + #addEvent(eventType, targetSelector, callback) { + const listener = (event) => { + if (!event.target.closest(targetSelector)) { + return; + } + callback(event); + }; + + if (this.$root) { + this.$root.addEventListener(eventType, listener); + } + } + #setEvent() { + const events = this.event() || []; + events.forEach(({ type, target, handler }) => { + this.#addEvent(type, target, handler); + }); + } + initState() { + return {}; + } + html() {} + event() {} + mounted() {} +} + +export default Component; diff --git a/vanilla-todo/src/core/ReduxStore.js b/vanilla-todo/src/core/ReduxStore.js new file mode 100644 index 0000000..1caa5e5 --- /dev/null +++ b/vanilla-todo/src/core/ReduxStore.js @@ -0,0 +1,20 @@ +import { deepCopy } from "../util/deepCopy.js"; +import { setState } from "../util/setState.js"; +import { observable } from "./observer.js"; + +const createStore = (reducer) => { + const state = observable(reducer()); + + const dispatch = (action) => { + const newState = reducer(state, action); + setState(state, newState); + }; + + const getState = () => { + return deepCopy(state); + }; + + return { dispatch, getState }; +}; + +export { createStore }; diff --git a/vanilla-todo/src/observer.js b/vanilla-todo/src/core/observer.js similarity index 84% rename from vanilla-todo/src/observer.js rename to vanilla-todo/src/core/observer.js index 670109c..3ba95d7 100644 --- a/vanilla-todo/src/observer.js +++ b/vanilla-todo/src/core/observer.js @@ -4,13 +4,7 @@ const MILLI_PER_FRAME = MILLISEC / FRAMES; let currentObserver = null; -const observe = (observer) => { - currentObserver = observer; - observer(); - currentObserver = null; -}; - -const batch = (callback, limit = MILLI_PER_FRAME) => { +const debounce = (callback, limit = MILLI_PER_FRAME) => { let timeout; return () => { clearTimeout(timeout); @@ -20,6 +14,12 @@ const batch = (callback, limit = MILLI_PER_FRAME) => { }; }; +const observe = (observer) => { + currentObserver = debounce(observer); + observer(); + currentObserver = null; +}; + const observable = (props) => { let observers = new Set(); const state = new Proxy( @@ -27,7 +27,7 @@ const observable = (props) => { { get(target, prop) { if (typeof currentObserver === "function") { - observers.add(batch(currentObserver)); + observers.add(currentObserver); } return target[prop]; }, diff --git a/vanilla-todo/src/core/router/HistoryRouter.js b/vanilla-todo/src/core/router/HistoryRouter.js new file mode 100644 index 0000000..45a84c0 --- /dev/null +++ b/vanilla-todo/src/core/router/HistoryRouter.js @@ -0,0 +1,47 @@ +import { CUSTOM_EVENT } from "../../constants/_index.js"; +import { deepCopy } from "../../util/deepCopy.js"; + +const locationChange = new CustomEvent(CUSTOM_EVENT.LOCATION_CHANGE); + +export default class HistoryRouter { + #url; + constructor() { + this.#url = new URL(window.location.href); + window.addEventListener("popstate", (e) => { + const { + target: { + location: { pathname, search }, + }, + } = e; + + this.#setUrl(pathname + search); + }); + } + get path() { + return this.#url.pathname; + } + get queryString() { + const queryString = {}; + for (const [key, value] of this.#url.searchParams.entries()) { + queryString[key] = value; + } + return deepCopy(queryString); + } + #setUrl(url) { + this.#url.href = this.#url.origin + url; + window.dispatchEvent(locationChange); + } + push(url) { + if (typeof url !== "string") { + throw Error("URL은 string 형식이어야 합니다."); + } + + this.#setUrl(url); + + if (url === location.href) { + history.replaceState({}, "", url); + } else { + history.pushState({}, "", url); + } + } +} diff --git a/vanilla-todo/src/pages/Main.js b/vanilla-todo/src/pages/Main.js new file mode 100644 index 0000000..9188fa0 --- /dev/null +++ b/vanilla-todo/src/pages/Main.js @@ -0,0 +1,37 @@ +import Component from "../core/Component.js"; +import Title from "../components/Title.js"; +import AddForm from "../components/AddForm.js"; +import TodoList from "../components/TodoList.js"; +import { CONTAINER } from "../constants/_index.js"; +import { store } from "../store.js"; +import { router } from "../route.js"; +import Filter from "../components/Filter.js"; + +export default class Main extends Component { + html() { + console.log(store.getState()); + return ` + +
    ${Title()}
    +
    +
    + + `; + } + mounted() { + new Filter(document.querySelector(`#${CONTAINER.FILTER}`)); + new AddForm(document.querySelector(`#${CONTAINER.ADD_FORM}`)); + new TodoList(document.querySelector(`#${CONTAINER.TODO_LIST}`)); + } + event() { + return [ + { + type: "click", + target: "#move", + handler: () => { + router.push("/move"); + }, + }, + ]; + } +} diff --git a/vanilla-todo/src/pages/Move.js b/vanilla-todo/src/pages/Move.js new file mode 100644 index 0000000..904c94d --- /dev/null +++ b/vanilla-todo/src/pages/Move.js @@ -0,0 +1,7 @@ +import Component from "../core/Component.js"; + +export default class Move extends Component { + html() { + return `

    Move!

    `; + } +} diff --git a/vanilla-todo/src/pages/_index.js b/vanilla-todo/src/pages/_index.js new file mode 100644 index 0000000..b021e16 --- /dev/null +++ b/vanilla-todo/src/pages/_index.js @@ -0,0 +1,2 @@ +export { default as Main } from "./Main.js"; +export { default as Move } from "./Move.js"; diff --git a/vanilla-todo/src/route.js b/vanilla-todo/src/route.js new file mode 100644 index 0000000..c798222 --- /dev/null +++ b/vanilla-todo/src/route.js @@ -0,0 +1,24 @@ +import { CUSTOM_EVENT } from "./constants/_index.js"; +import HistoryRouter from "./core/router/HistoryRouter.js"; +import { Main, Move } from "./pages/_index.js"; + +export const router = new HistoryRouter(); + +const $app = document.querySelector("#App"); + +const renderPath = () => { + switch (router.path) { + case "/": + new Main($app); + return; + case "/move": + new Move($app); + return; + default: + return; + } +}; + +renderPath(); + +window.addEventListener(CUSTOM_EVENT.LOCATION_CHANGE, renderPath); diff --git a/vanilla-todo/src/services/TodoService.js b/vanilla-todo/src/services/TodoService.js new file mode 100644 index 0000000..f4d6c79 --- /dev/null +++ b/vanilla-todo/src/services/TodoService.js @@ -0,0 +1,44 @@ +import { deepCopy } from "../util/deepCopy.js"; + +class TodoService { + #todos = []; + + get todos() { + return deepCopy([...this.#todos]); + } + findTodo(todoId) { + const todoIndex = this.#todos.findIndex((value) => value.id === todoId); + return { todo: this.#todos[todoIndex], index: todoIndex }; + } + addTodo(content) { + const newTodo = { + content, + id: Date.now(), + done: false, + }; + this.#todos = [...this.#todos, newTodo]; + + return this.todos; + } + deleteTodo(todoId) { + this.#todos = this.#todos.filter((value) => value.id !== todoId); + return this.todos; + } + toggleTodo(todoId) { + const { index, todo } = this.findTodo(todoId); + const newTodos = [...this.#todos]; + newTodos[index] = { ...todo, done: !todo.done }; + this.#todos = newTodos; + return this.todos; + } + editTodo(newTodo) { + const { id, content } = newTodo; + const { index, todo: oldTodo } = this.findTodo(id); + const newTodos = [...this.#todos]; + newTodos[index] = { ...oldTodo, content }; + this.#todos = newTodos; + return this.todos; + } +} + +export default TodoService; diff --git a/vanilla-todo/src/store.js b/vanilla-todo/src/store.js new file mode 100644 index 0000000..5a10d45 --- /dev/null +++ b/vanilla-todo/src/store.js @@ -0,0 +1,30 @@ +import { createStore } from "./core/ReduxStore.js"; +import TodoService from "./services/TodoService.js"; + +export const todoService = new TodoService(); + +const initState = { + todos: todoService.todos, + editingId: null, + filter: null, +}; + +export const SET_TODO = "SET_TODO"; +export const SET_EDITING = "SET_EDITING"; +export const SET_EDITING_NULL = "SET_EDITING_NULL"; +export const SET_FILTER = "SET_FILTER"; + +export const store = createStore((state = initState, action = {}) => { + switch (action.type) { + case SET_TODO: + return { ...state, todos: action.payload }; + case SET_EDITING: + return { ...state, editingId: action.payload }; + case SET_EDITING_NULL: + return { ...state, editingId: null }; + case SET_FILTER: + return { ...state, filter: action.payload }; + default: + return state; + } +}); diff --git a/vanilla-todo/src/util/deepCopy.js b/vanilla-todo/src/util/deepCopy.js new file mode 100644 index 0000000..41827b4 --- /dev/null +++ b/vanilla-todo/src/util/deepCopy.js @@ -0,0 +1,33 @@ +export const deepCopy = (value) => { + let deepCopyValue; + if (Array.isArray(value)) { + deepCopyValue = []; + deepCopyArray(deepCopyValue, value); + } else if (typeof value === "object" && value !== null) { + deepCopyValue = {}; + deepCopyObject(deepCopyValue, value); + } else { + deepCopyValue = value; + } + return deepCopyValue; +}; + +const deepCopyArray = (deepCopyValue, arr) => { + arr.forEach((v, i) => { + if (typeof v === "object" && v !== null) { + deepCopyValue[i] = deepCopy(v); + } else { + deepCopyValue[i] = v; + } + }); +}; + +const deepCopyObject = (deepCopyValue, obj) => { + for (const [k, v] of Object.entries(obj)) { + if (typeof v === "object" && v !== null) { + deepCopyValue[k] = deepCopy(v); + } else { + deepCopyValue[k] = v; + } + } +}; diff --git a/vanilla-todo/src/util/setState.js b/vanilla-todo/src/util/setState.js new file mode 100644 index 0000000..cb9ff83 --- /dev/null +++ b/vanilla-todo/src/util/setState.js @@ -0,0 +1,8 @@ +const setState = (state, newState) => { + for (const [key, value] of Object.entries(newState)) { + if (!state.hasOwnProperty(key)) continue; + state[key] = value; + } +}; + +export { setState }; diff --git a/vanilla-todo/todo.js b/vanilla-todo/todo.js deleted file mode 100644 index b38d3a1..0000000 --- a/vanilla-todo/todo.js +++ /dev/null @@ -1,170 +0,0 @@ -const ADD_FORM_ID = "addForm"; -const EDIT_FORM_CLASSNAME = "editForm"; -const EDIT_BUTTON_CLASSNAME = "editing"; -const TOGGLE_CLASSNAME = "toggle"; -const DELETE_BUTTON_CLASSNAME = "delete"; - -const $addForm = document.getElementById(ADD_FORM_ID); -const $todoInput = $addForm.querySelector("input"); -const $todoList = document.getElementById("todoList"); - -let todos = []; -let editing = null; - -const findTodo = (todoId) => { - const todo = todos.find((value) => value.id === todoId); - return todo; -}; - -const findTodoIndex = (todoId) => { - const todoIndex = todos.findIndex((value) => value.id === todoId); - return todoIndex; -}; - -const getTodoElementId = (todoElement) => { - return Number(todoElement.id); -}; - -const handleClickDelete = (event) => { - const { - target: { parentNode: $targetTodo }, - } = event; - - deleteTodo(getTodoElementId($targetTodo)); -}; - -const deleteTodo = (todoId) => { - todos.splice(findTodoIndex(todoId), 1); - renderTodo(); -}; - -const handleChangeToggle = (event) => { - const { - target: { parentNode: $labelElement }, - } = event; - - const $targetTodo = $labelElement.parentNode; - - toggleTodo(getTodoElementId($targetTodo)); -}; - -const toggleTodo = (todoId) => { - const todoIndex = findTodoIndex(todoId); - const oldTodo = findTodo(todoId); - console.log(todoIndex, oldTodo); - const newTodo = { - ...oldTodo, - done: !oldTodo.done, - }; - todos.splice(todoIndex, 1, newTodo); - renderTodo(); -}; - -const editTodo = (content) => { - const newTodo = { - ...editing, - content, - }; - - todos.splice(findTodoIndex(editing.id), 1, newTodo); - - editing = null; - - renderTodo(); -}; - -const handleSubmitEditing = (event) => { - event.preventDefault(); - - const content = event.target[0].value; - editTodo(content); -}; - -const handleClickStartEditing = (event) => { - const { - target: { parentNode: $targetTodo }, - } = event; - - startEditing(findTodo(getTodoElementId($targetTodo))); -}; - -const paintEditForm = (todoId) => { - const editingTodo = document.getElementById(todoId); - const editForm = editingTodo.querySelector("form"); - const editingButton = editingTodo.querySelector(EDIT_BUTTON_CLASSNAME); - - editForm.classList.remove("hidden"); - editingButton.classList.add("hidden"); -}; - -const startEditing = (todo) => { - editing = todo; - - paintEditForm(todo.id); -}; - -const addTodo = () => { - editing = null; - - const newTodo = { - content: $todoInput.value, - id: Date.now(), - done: false, - }; - $todoInput.value = ""; - - todos.push(newTodo); - renderTodo(); -}; - -const handleSubmitAdding = (event) => { - event.preventDefault(); - - addTodo(); -}; - -const addEvent = (eventType, targetSelector, callback) => { - document.body.addEventListener(eventType, (event) => { - if (!event.target.closest(targetSelector)) { - return; - } - callback(event); - }); -}; - -addEvent("submit", `#${ADD_FORM_ID}`, handleSubmitAdding); -addEvent("submit", `.${EDIT_FORM_CLASSNAME}`, handleSubmitEditing); -addEvent("click", `.${EDIT_BUTTON_CLASSNAME}`, handleClickStartEditing); -addEvent("click", `.${DELETE_BUTTON_CLASSNAME}`, handleClickDelete); -addEvent("change", `.${TOGGLE_CLASSNAME}`, handleChangeToggle); - -const renderTodo = () => { - $todoList.innerHTML = todos - .map( - ({ id, content, done }) => ` -
  • - -
    - - -
    - - -
  • - ` - ) - .join(""); -};