UNPKG

@forest-js/core

Version:

A tiny, functional DOM engine with explicit update and real DOM.

213 lines 7.75 kB
import { enqueue, ensureMeta } from "../dom"; const toNode = (node) => { if (typeof node === "string" || typeof node === "number") { return document.createTextNode(String(node)); } if (node === null || node === undefined) { return document.createTextNode(""); } return node; }; const flatten = (nodes) => { if (Array.isArray(nodes)) { return Array.from(nodes).flatMap((node) => { if (Array.isArray(node)) return flatten(node); if (node instanceof Node) return node; return toNode(node); }); } return [toNode(nodes)]; }; const isSameNode = (a, b) => { if (a.length !== b.length) return false; for (let i = 0; i < a.length; i++) { if (a[i] !== b[i]) return false; } return true; }; const processPromiseResult = (result) => { if (result && typeof result === "object" && "default" in result) { return result.default; } return result; }; // head 요소 핸들링을 위한 함수 수정 const handleHeadElement = (child) => { if (!(child instanceof Element)) return false; // 개별 head 내부 요소 처리 (기존 로직과 동일) if (child.tagName === "TITLE") { const existing = document.head.querySelector("title"); if (existing) { document.head.replaceChild(child, existing); } else { document.head.appendChild(child); } return true; } else if (child.tagName === "META") { const name = child.getAttribute("name"); const property = child.getAttribute("property"); const httpEquiv = child.getAttribute("http-equiv"); let selector = ""; if (name) selector = `meta[name="${name}"]`; else if (property) selector = `meta[property="${property}"]`; else if (httpEquiv) selector = `meta[http-equiv="${httpEquiv}"]`; if (selector) { const existing = document.head.querySelector(selector); if (existing) { document.head.replaceChild(child, existing); return true; } } // 일치하는 메타 태그가 없으면 추가 document.head.appendChild(child); return true; } else if (child.tagName === "LINK") { const rel = child.getAttribute("rel"); const href = child.getAttribute("href"); if (rel && href) { const selector = `link[rel="${rel}"][href="${href}"]`; const existing = document.head.querySelector(selector); if (existing) { document.head.replaceChild(child, existing); return true; } } // 일치하는 링크 태그가 없으면 추가 document.head.appendChild(child); return true; } else if (child.tagName === "STYLE" || child.tagName === "SCRIPT") { const id = child.getAttribute("id"); if (id) { const selector = `${child.tagName.toLowerCase()}#${id}`; const existing = document.head.querySelector(selector); if (existing) { document.head.replaceChild(child, existing); return true; } } else if (child.tagName === "SCRIPT") { const src = child.getAttribute("src"); if (src) { const selector = `script[src="${src}"]`; const existing = document.head.querySelector(selector); if (existing) { document.head.replaceChild(child, existing); return true; } } } // 일치하는 태그가 없으면 추가 document.head.appendChild(child); return true; } // 다른 모든 head 요소는 그냥 추가 document.head.appendChild(child); return true; }; // 노드 처리 로직 개선 const appendNodes = (el, nodes, placeholder) => { // head 요소 특별 처리 if (el.tagName === "HEAD" || el === document.head) { nodes.forEach((child) => handleHeadElement(child)); if (placeholder && placeholder.parentNode === el) { el.removeChild(placeholder); } } else { if (placeholder && placeholder.parentNode === el) { // placeholder 대체 로직 유지 el.insertBefore(nodes[0], placeholder); el.removeChild(placeholder); nodes.slice(1).forEach((node) => el.appendChild(node)); } else { // 일반 요소 처리 로직 개선 const oldChildren = Array.from(el.childNodes); if (oldChildren.length > 0 && !isSameNode(oldChildren, nodes)) { el.replaceChildren(...nodes); } else if (oldChildren.length === 0) { nodes.filter((node) => !(node instanceof HTMLHeadElement)).forEach((node) => el.appendChild(node)); } } } }; /** * @function addChild * @description Utility for adding children to an element * @template E Element type * @template R Child content to add (can be promise or dynamic import) * @template S StoreMap type when used reactively * @param args - Child content or store and mapper function * @returns Utility function for adding children * @example * ```ts * addChild("Hello")(MyElement); * // if you want to add head in your app, you can do this: * addChild([...someHead's children])(tree("head")); * ``` */ export const addChild = (...args) => { return (el) => { if (args.length === 2 && typeof args[1] === "function") { const [stores, mapper] = args; const placeholder = document.createTextNode(""); // 로딩 중 placeholder el.appendChild(placeholder); const apply = async () => { const values = {}; for (const key in stores) values[key] = stores[key].get(); try { const result = mapper(values); const processedResult = result instanceof Promise ? processPromiseResult(await result) : result; const children = flatten(processedResult); appendNodes(el, children, placeholder); } catch (error) { console.error("Error loading dynamic component:", error); } }; apply(); // 스토어 구독 const unsubs = Object.values(stores).map((store) => store.subscribe(() => enqueue(apply))); const meta = ensureMeta(el); meta.storeBindings ??= new Set(); unsubs.forEach((unsub) => meta.storeBindings.add(unsub)); } else { const [children] = args; if (children instanceof Promise) { const placeholder = document.createTextNode(""); el.appendChild(placeholder); children .then(async (result) => { const processedResult = processPromiseResult(result); const nodes = flatten(processedResult); appendNodes(el, nodes, placeholder); }) .catch((error) => { console.error("Error loading dynamic children:", error); }); } else { // 일반 동기 처리 const nodes = flatten(children); appendNodes(el, nodes); } } return el; }; }; //# sourceMappingURL=child.js.map