diff --git a/week4/index.html b/week4/index.html new file mode 100644 index 0000000..fb91e1e --- /dev/null +++ b/week4/index.html @@ -0,0 +1,12 @@ + + + + + + 4주차 JavaScript 실습 + + +
+ + + \ No newline at end of file diff --git a/week4/src/App.js b/week4/src/App.js new file mode 100644 index 0000000..5be2e10 --- /dev/null +++ b/week4/src/App.js @@ -0,0 +1,135 @@ +import Component from "./Component.js"; + +import ItemAppender from "./components/ItemAppender.js"; +import ItemsView from "./components/ItemsView.js"; + +import { observable } from "./Observer.js"; + +class App extends Component { + init() { + this.state = observable({ + todoItems: [ + { name: "코딩하기", done: false, updateState: false }, + { name: "밥먹기", done: true, updateState: false }, + { name: "양치하기", done: false, updateState: false }, + ], + }); + } + + template() { + return ` +

4주차 미션 - 옵저버

+ +
+
+ `; + } + + mount() { + //투두리스트 state를 불러옴 + const { todoItems } = this.state; + const $itemAppender = this.$component.querySelector("#item-appender"); + const $itemsView = this.$component.querySelector("#items-view"); + + // state를 props로 전달 + new ItemAppender($itemAppender); + new ItemsView($itemsView, { todoItems }); + } + + setEvents() { + this.appendTodoItem(); + this.deleteTodoItem(); + this.toggleTodoItem(); + this.updateTodoItem(); + } + + appendTodoItem() { + const { todoItems } = this.state; + const appendBtn = this.$component.querySelector("#append-btn"); + + if (appendBtn) { + appendBtn.addEventListener("click", () => { + const newTodo = + this.$component.querySelector("#append-input").value; + + this.setState({ + todoItems: [ + ...todoItems, + observable({ + name: newTodo, + done: false, + updateState: false, + }), + ], + }); + }); + } + } + + deleteTodoItem() { + const { todoItems } = this.state; + const $itemsView = this.$component.querySelector("#items-view"); + + $itemsView.addEventListener("click", (event) => { + if (event.target.id === "delete-btn") { + const todoIndex = parseInt( + event.target.closest("li").getAttribute("data-id") + ); + console.log(todoIndex); + + const deletedTodoItems = todoItems.filter( + (item, index) => index !== todoIndex + ); + this.setState({ todoItems: deletedTodoItems }); + } + }); + } + + toggleTodoItem() { + const { todoItems } = this.state; + const $itemsView = this.$component.querySelector("#items-view"); + + $itemsView.addEventListener("change", (event) => { + if (event.target.id === "toggle-btn") { + const todoIndex = parseInt( + event.target.closest("li").getAttribute("data-id") + ); + const toggledTodoItems = [...todoItems]; + toggledTodoItems[todoIndex] = { + ...toggledTodoItems[todoIndex], + done: !toggledTodoItems[todoIndex].done, + }; + this.setState({ todoItems: toggledTodoItems }); + } + }); + } + + updateTodoItem() { + const { todoItems } = this.state; + const $itemsView = this.$component.querySelector("#items-view"); + + $itemsView.addEventListener("click", (event) => { + if (event.target.id === "update-btn") { + const todoIndex = parseInt( + event.target.closest("li").getAttribute("data-id") + ); + + const updatedTodoItems = [...todoItems]; + updatedTodoItems[todoIndex].updateState = + !updatedTodoItems[todoIndex].updateState; + + if (!updatedTodoItems[todoIndex].updateState) { + const $itemsTitle = this.$component.querySelector( + `#title-${todoIndex}` + ); + + updatedTodoItems[todoIndex].name = $itemsTitle.value; + } + + this.setState({ todoItems: updatedTodoItems }); + } + }); + } +} + +new App(document.querySelector("#app")); diff --git a/week4/src/Component.js b/week4/src/Component.js new file mode 100644 index 0000000..de77efe --- /dev/null +++ b/week4/src/Component.js @@ -0,0 +1,54 @@ +import { observable, observe } from "./Observer.js"; + +export default class Component { + $component; + $props; + state; + + constructor($component, $props) { + this.$component = $component; + this.$props = $props; + this.init(); + this.updateState(); + this.render(); + } + + template() { + return "
"; + } + + mount() { + /* 하위 컴포넌트 마운트 */ + } + + render() { + this.$component.innerHTML = this.template(); + this.mount(); + this.setEvents(); + } + + setState(newState) { + this.state = { ...this.state, ...newState }; + this.render(); + } + + init() { + //state 초기화 + // this.state = observable({ + // todoItems: [ + // { name: "코딩하기", done: false, updateState: false }, + // { name: "밥먹기", done: true, updateState: false }, + // { name: "양치하기", done: false, updateState: false }, + // ], + // }); + } + + setEvents() {} + + updateState() { + observe(() => { + this.render(); + console.log("렌더링"); + }); + } +} diff --git a/week4/src/Observer.js b/week4/src/Observer.js new file mode 100644 index 0000000..a916a5e --- /dev/null +++ b/week4/src/Observer.js @@ -0,0 +1,66 @@ +function debounce(func) { + let frameId; + return function () { + if (frameId) { + cancelAnimationFrame(frameId); + } + frameId = requestAnimationFrame(func); + }; +} + +let currentObserver = null; + +export const observe = (func) => { + currentObserver = debounce(func); + func(); + currentObserver = null; +}; + +export const observable = (obj) => { + const observers = new Map(); + + return new Proxy(obj, { + get(target, key) { + const value = target[key]; + + if (value !== null && typeof value === "object") + return observable(value); + + if (currentObserver && typeof observers !== "undefined") { + if (!observers.has(key)) observers.set(key, new Set()); + + observers.get(key).add(currentObserver); + } + + return value; + }, + set(target, key, value) { + target[key] = value; + + if (observers.has(key)) + observers.get(key).forEach((observer) => observer()); + + return true; + }, + }); +}; + +// const state = observable({ +// todoItems: [ +// { name: "코딩하기", done: false, updateState: false }, +// { name: "밥먹기", done: true, updateState: false }, +// { name: "양치하기", done: false, updateState: false }, +// ], +// }); + +// observe(() => +// console.log(state.todoItems[0].name + " 로그가 실행이 됐습니다.") +// ); + +// state.todoItems[0].name = "todo"; +// state.todoItems[0].name = "todo1"; +// state.todoItems[0].name = "todo2"; + +// requestAnimationFrame(() => { +// state.todoItems[0].name = "todo3"; +// }); diff --git a/week4/src/components/ItemAppender.js b/week4/src/components/ItemAppender.js new file mode 100644 index 0000000..14e5e00 --- /dev/null +++ b/week4/src/components/ItemAppender.js @@ -0,0 +1,12 @@ +import Component from "../Component.js"; + +export default class ItemAppender extends Component { + template() { + return ` +
+ + +
+ `; + } +} diff --git a/week4/src/components/ItemsView.js b/week4/src/components/ItemsView.js new file mode 100644 index 0000000..e53319f --- /dev/null +++ b/week4/src/components/ItemsView.js @@ -0,0 +1,41 @@ +import Component from "../Component.js"; + +import { observe } from "../Observer.js"; + +export default class ItemsView extends Component { + updateState() { + observe(() => { + console.log("ItemsView 컴포넌트에서 옵저버..."); + console.log(this.$props); + }); + } + + template() { + const { todoItems } = this.$props; + + return ` + + + `; + } +} diff --git a/week4/style.css b/week4/style.css new file mode 100644 index 0000000..88125e8 --- /dev/null +++ b/week4/style.css @@ -0,0 +1,73 @@ +body{ + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + background-color: #c9c9c9; + + padding: 0; + margin: 0; +} + +ul{ + list-style:none; + padding-left: 0; +} + +li{ + margin: 10px 0; +} + +#app{ + width: 60vw; + height: 80vh; + padding-top: 5vh; + margin-top: 5vh; + + display: flex; + flex-direction: column; + align-items: center; + background-color: white; + color: #292929; + + overflow-x: hidden; + overflow-y: scroll; +} + +.append-input{ + width: 200px; + height: 24px; + padding: 3px 10px; + + border-radius: 3px; + border: 1px solid #c3c3c3; +} + +.btn{ + width: 50px; + height: 32px; + border: none; + background-color: #c3c3c366; + cursor: pointer; +} + +.todo{ + width: 150px; + height: 24px; + margin: 3px; + + cursor: auto; + + border: none; + text-align: center; +} + +input:read-write{ + border: 1px solid #c3c3c3; + text-align: start; +} + +.checked{ + color: #a6a6a6; + text-decoration: line-through; +} \ No newline at end of file