diff --git a/404.html b/404.html new file mode 100644 index 00000000..d43ffde2 --- /dev/null +++ b/404.html @@ -0,0 +1,26 @@ + + + + + + 상품 쇼핑몰 + + + + + +
+ + + diff --git a/index.html b/index.html index d43ffde2..94dead1b 100644 --- a/index.html +++ b/index.html @@ -1,26 +1,26 @@ - - - - 상품 쇼핑몰 - - - - - -
- - + + + + 상품 쇼핑몰 + + + + + +
+ + diff --git a/src/lib/createElement.js b/src/lib/createElement.js index 5d39ae7d..9088e365 100644 --- a/src/lib/createElement.js +++ b/src/lib/createElement.js @@ -1,5 +1,40 @@ import { addEvent } from "./eventManager"; -export function createElement(vNode) {} +export function createElement(vNode) { + if (vNode === null || vNode === undefined || typeof vNode === "boolean") { + return document.createTextNode(""); + } -function updateAttributes($el, props) {} + if (typeof vNode === "string" || typeof vNode === "number") { + return document.createTextNode(String(vNode)); + } + + if (Array.isArray(vNode)) { + const fragment = document.createDocumentFragment(); + + vNode.forEach((node) => fragment.appendChild(createElement(node))); + + return fragment; + } + + const el = document.createElement(vNode.type); + updateAttributes(el, vNode.props ?? {}); + + el.append(...vNode.children.map(createElement)); + + return el; +} + +function updateAttributes($el, props) { + Object.entries(props).forEach(([attribute, value]) => { + if (/^on[A-Z]/.test(attribute) && typeof value === "function") { + addEvent($el, attribute.toLowerCase().substring(2), value); + } else if (attribute === "className") { + $el.setAttribute("class", value); + } else if (typeof value === "boolean") { + $el[attribute] = value; + } else { + $el.setAttribute(attribute, value); + } + }); +} diff --git a/src/lib/createVNode.js b/src/lib/createVNode.js index 9991337f..05e7a6ff 100644 --- a/src/lib/createVNode.js +++ b/src/lib/createVNode.js @@ -1,3 +1,9 @@ export function createVNode(type, props, ...children) { - return {}; + const flatChildren = children.flat(Infinity).filter((child) => child === 0 || Boolean(child)); + + return { + type, + props, + children: flatChildren, + }; } diff --git a/src/lib/eventManager.js b/src/lib/eventManager.js index 24e4240f..c4c4f820 100644 --- a/src/lib/eventManager.js +++ b/src/lib/eventManager.js @@ -1,5 +1,41 @@ -export function setupEventListeners(root) {} +const eventMap = new WeakMap(); +const delegatedEvents = new Set(); -export function addEvent(element, eventType, handler) {} +export function setupEventListeners(root) { + delegatedEvents.forEach((eventType) => { + root.removeEventListener(eventType, handleDelegatedEvent); + root.addEventListener(eventType, handleDelegatedEvent); + }); +} -export function removeEvent(element, eventType, handler) {} +export function addEvent(element, eventType, handler) { + if (!eventMap.has(element)) { + eventMap.set(element, new Map()); + } + + const elementEvents = eventMap.get(element); + elementEvents.set(eventType, handler); + + delegatedEvents.add(eventType); +} + +export function removeEvent(element, eventType) { + const elementEvents = eventMap.get(element); + elementEvents.delete(eventType); +} + +function handleDelegatedEvent(event) { + let target = event.target; + + // NOTE: cancelBubble 속성은 deprecated이므로 다른 방법으로 구현해야 함 + while (target && !event.cancelBubble) { + const elementEvents = eventMap.get(target); + + if (elementEvents?.has(event.type)) { + const handler = elementEvents.get(event.type); + handler(event); + } + + target = target.parentNode; + } +} diff --git a/src/lib/normalizeVNode.js b/src/lib/normalizeVNode.js index 7dc6f175..add04d36 100644 --- a/src/lib/normalizeVNode.js +++ b/src/lib/normalizeVNode.js @@ -1,3 +1,18 @@ export function normalizeVNode(vNode) { - return vNode; + if (vNode === null || vNode === undefined || typeof vNode === "boolean") { + return ""; + } + + if (typeof vNode === "string" || typeof vNode === "number") { + return String(vNode); + } + + if (typeof vNode.type === "function") { + return normalizeVNode(vNode.type({ ...vNode.props, children: vNode.children })); + } + + return { + ...vNode, + children: vNode.children.map(normalizeVNode).filter(Boolean), + }; } diff --git a/src/lib/renderElement.js b/src/lib/renderElement.js index 04295728..2980e26e 100644 --- a/src/lib/renderElement.js +++ b/src/lib/renderElement.js @@ -3,8 +3,24 @@ import { createElement } from "./createElement"; import { normalizeVNode } from "./normalizeVNode"; import { updateElement } from "./updateElement"; +const vNodeMap = new WeakMap(); + export function renderElement(vNode, container) { // 최초 렌더링시에는 createElement로 DOM을 생성하고 // 이후에는 updateElement로 기존 DOM을 업데이트한다. // 렌더링이 완료되면 container에 이벤트를 등록한다. + + const oldVNode = vNodeMap.get(container); + const newVnode = normalizeVNode(vNode); + + if (!oldVNode) { + const element = createElement(newVnode); + container.appendChild(element); + } else { + updateElement(container, newVnode, oldVNode); + } + + vNodeMap.set(container, newVnode); + + setupEventListeners(container); } diff --git a/src/lib/updateElement.js b/src/lib/updateElement.js index ac321861..4acd503a 100644 --- a/src/lib/updateElement.js +++ b/src/lib/updateElement.js @@ -1,6 +1,113 @@ import { addEvent, removeEvent } from "./eventManager"; import { createElement } from "./createElement.js"; -function updateAttributes(target, originNewProps, originOldProps) {} +export function updateAttributes(target, originNewProps, originOldProps) { + if (!originNewProps && !originOldProps) return; -export function updateElement(parentElement, newNode, oldNode, index = 0) {} + if (originOldProps) { + Object.entries(originOldProps).forEach(([key, value]) => { + if (key === "children") return; + + if (key.startsWith("on")) { + const eventType = key.substring(2).toLowerCase(); + removeEvent(target, eventType, originOldProps[key]); + } else if (!originNewProps || !(key in originNewProps)) { + if (key === "className") { + target.removeAttribute("class"); + } else if (typeof value === "boolean") { + target[key] = false; + target.removeAttribute(key); + } else { + target.removeAttribute(key); + } + } + }); + } + + if (originNewProps) { + Object.entries(originNewProps).forEach(([key, value]) => { + if (key === "children") return; + + if (key === "className") { + if (value) { + target.setAttribute("class", value); + } else { + target.removeAttribute("class"); + } + return; + } + + if (key.startsWith("on")) { + const eventType = key.substring(2).toLowerCase(); + addEvent(target, eventType, value); + return; + } + + if (typeof value === "boolean") { + target[key] = value; + return; + } + + if (value != null && (!originOldProps || originOldProps[key] !== value)) { + target.setAttribute(key, String(value)); + } + }); + } +} + +export function updateElement(parentElement, newNode, oldNode, index = 0) { + if (!newNode && oldNode) { + if (parentElement.childNodes[index]) { + parentElement.removeChild(parentElement.childNodes[index]); + } + return; + } + + if (newNode && !oldNode) { + parentElement.appendChild(createElement(newNode)); + return; + } + + if (typeof newNode === "string" || typeof newNode === "number") { + if (newNode !== oldNode) { + const newTextNode = document.createTextNode(String(newNode)); + if (parentElement.childNodes[index]) { + parentElement.replaceChild(newTextNode, parentElement.childNodes[index]); + } else { + parentElement.appendChild(newTextNode); + } + } + return; + } + + if (newNode.type !== oldNode.type) { + if (parentElement.childNodes[index]) { + parentElement.replaceChild(createElement(newNode), parentElement.childNodes[index]); + } else { + parentElement.appendChild(createElement(newNode)); + } + return; + } + + const childNode = parentElement.childNodes[index]; + if (childNode) { + updateAttributes(childNode, newNode.props || {}, oldNode.props || {}); + + const newChildren = newNode.children || []; + const oldChildren = oldNode.children || []; + const maxLength = Math.max(newChildren.length, oldChildren.length); + + for (let i = 0; i < maxLength; i++) { + updateElement(childNode, newChildren[i], oldChildren[i], i); + } + + if (oldChildren.length > newChildren.length) { + for (let i = oldChildren.length - 1; i >= newChildren.length; i--) { + const extraChild = childNode.childNodes[i]; + if (extraChild) { + childNode.removeChild(extraChild); + } + } + } + } +}