UNPKG

@devmore/vanilact

Version:
352 lines (351 loc) 11.5 kB
let currentComponent; let hookIndex = 0; let appInstance; let rootElement; let renderCount = 0; let setupEventList = []; const componentStore = /* @__PURE__ */ new Map(); let nextId = 0; const isHTML = (str) => { const doc = new DOMParser().parseFromString(str, "text/html"); return Array.from(doc.body.childNodes).some((node) => node.nodeType === 1); }; const isClassComponent = (component) => { return typeof component === "function" && component.prototype && component.prototype.render; }; function render(vnode, container, parentInstance) { let dom = null; let instance = null; if (typeof vnode === "string" || typeof vnode === "number") { if (typeof vnode === "string" && isHTML(vnode)) dom = document.createRange().createContextualFragment(vnode); else dom = document.createTextNode(String(vnode)); if (container) container.appendChild(dom); (parentInstance == null ? void 0 : parentInstance.setDom) && (parentInstance == null ? void 0 : parentInstance.setDom(container)); (parentInstance == null ? void 0 : parentInstance.onMount) && (parentInstance == null ? void 0 : parentInstance.onMount()); return dom; } if (Array.isArray(vnode)) { dom = document.createDocumentFragment(); vnode.forEach((child) => { const childNode = render(child, dom); if (childNode) dom.appendChild(childNode); }); if (container) container.appendChild(dom); (parentInstance == null ? void 0 : parentInstance.setDom) && (parentInstance == null ? void 0 : parentInstance.setDom(container)); (parentInstance == null ? void 0 : parentInstance.onMount) && (parentInstance == null ? void 0 : parentInstance.onMount()); return dom; } if (typeof vnode.type === "function") { if (isClassComponent(vnode.type)) { instance = new vnode.type(vnode.props || {}); vnode.instance = instance; (instance == null ? void 0 : instance.willMount) && (instance == null ? void 0 : instance.willMount()); const componentVNode = instance.render(); registerComponent(container, vnode, componentVNode); dom = render(componentVNode, container, instance); (parentInstance == null ? void 0 : parentInstance.setDom) && (parentInstance == null ? void 0 : parentInstance.setDom(container)); (parentInstance == null ? void 0 : parentInstance.onMount) && (parentInstance == null ? void 0 : parentInstance.onMount()); return dom; } else { const componentVNode = vnode.type(vnode.props || {}); dom = render(componentVNode, container, parentInstance); registerComponent(container, vnode, dom); return dom; } } dom = document.createElement(vnode.type); const props = vnode.props || {}; for (const [key, value] of Object.entries(props)) { if (key == "ref") { if (typeof value === "function") { value(dom); } else if (typeof value === "object" && value !== null && "current" in value) { value.current = dom; } } else if (key.startsWith("on") && typeof value === "function") { dom.addEventListener(key.slice(2).toLowerCase(), value); } else if (key !== "children") { dom.setAttribute(key, value); } } const children = [].concat(props.children || []); children.forEach((child) => { const childNode = render(child, dom); if (childNode) dom.appendChild(childNode); }); if (container) container.appendChild(dom); (instance == null ? void 0 : instance.setDom) && (instance == null ? void 0 : instance.setDom(container)); (instance == null ? void 0 : instance.onMount) && (instance == null ? void 0 : instance.onMount()); (parentInstance == null ? void 0 : parentInstance.setDom) && (parentInstance == null ? void 0 : parentInstance.setDom(container)); (parentInstance == null ? void 0 : parentInstance.onMount) && (parentInstance == null ? void 0 : parentInstance.onMount()); return dom; } function renderComponent(component, container) { currentComponent = component; componentStore.clear(); hookIndex = 0; nextId = 0; let output = null; let instance = null; if (isClassComponent(component.type)) { instance = new component.type(component.props); component.instance = instance; (instance == null ? void 0 : instance.willMount) && (instance == null ? void 0 : instance.willMount()); output = instance.render(); } else { output = component.type(component.props); } registerComponent(container, component, output); render(output, container, instance); runEffects(component); } function rerender() { if (renderCount++ > 100) throw new Error("Too many rerenders!"); rootElement.innerHTML = ""; renderComponent(appInstance, rootElement); if (setupEventList && setupEventList.length > 0) { for (const fn of setupEventList) { if (typeof fn === "function") { try { fn(); } catch (e) { console.error(e.stack); } } } setupEventList = []; } renderCount = 0; } function matchRoute(pathname, routePattern) { const pathParts = pathname.split("/").filter((e) => e); const routeParts = routePattern.split("/").filter((e) => e); if (pathParts.length !== routeParts.length) return null; const params = {}; for (let i = 0; i < pathParts.length; i++) { if (routeParts[i].startsWith(":")) { const key = routeParts[i].slice(1); params[key] = pathParts[i]; } else if (pathParts[i] !== routeParts[i]) { return null; } } return params; } function registerComponent(container, componentNode, dom) { const id = nextId++; componentStore.set(id, { container, componentNode }); componentNode.container = container; componentNode.__id = id; componentNode.__dom = dom; return id; } function useState(initialValue) { const hooks = currentComponent.hooks || (currentComponent.hooks = []); if (hooks[hookIndex] === void 0) hooks[hookIndex] = initialValue; const index = hookIndex; const setState = (newValue) => { if (hooks[index] !== newValue) { hooks[index] = newValue; rerender(); } }; return [hooks[hookIndex++], setState]; } function useEffect(callback, deps) { const hooks = currentComponent.hooks || (currentComponent.hooks = []); const prev = hooks[hookIndex]; const hasChanged = !prev || !deps || deps.some((d, i) => d !== prev.deps[i]); if (hasChanged) { hooks[hookIndex] = { callback, deps, cleanup: null }; currentComponent.effects || (currentComponent.effects = []); currentComponent.effects.push(hookIndex); } hookIndex++; } function runEffects(component) { const hooks = component.hooks; const indices = component.effects || []; for (const i of indices) { const effect = hooks[i]; if (effect.cleanup) effect.cleanup(); const cleanup = effect.callback(); if (typeof cleanup === "function") { effect.cleanup = cleanup; } } component.effects = []; } function createElement(type, props = {}, ...children) { return { type, props: { ...props, children } }; } function useLocation() { const [loc, setLoc] = useState(() => location.pathname); useEffect(() => { const onChange = () => setLoc(location.pathname); window.addEventListener("popstate", onChange); return () => window.addEventListener("popstate", onChange); }, []); return loc; } function navigate(path, params = {}) { let arrParams = []; if (params) { Object.entries(params).map(([k, v]) => { arrParams.push(`${encodeURIComponent(decodeURIComponent(k))}=${encodeURIComponent(decodeURIComponent(v))}`); }); } history.pushState({}, "", [path, arrParams.filter((e) => e).join("&")].filter((e) => e).join("?")); rerender(); } function lazy(importFn) { let LoadedComponent = null; let loading = false; let error = null; return function LazyWrapper(props) { const [_, forceUpdate] = useState(0); if (!LoadedComponent && !loading) { loading = true; importFn().then((module) => { LoadedComponent = module.default; forceUpdate((x) => x + 1); }).catch((err) => { error = err; forceUpdate((x) => x + 1); }); } if (error) { console.error(error.stack); return createElement("div"); } if (!LoadedComponent) { return createElement("div"); } return createElement(LoadedComponent, props, ...props.children || []); }; } function Router({ routes, errorViews = [] }) { const pathname = location.pathname || "/"; for (const { path, component, middlewares } of routes) { const params = matchRoute(pathname, path); if (params) { if (middlewares && middlewares.length > 0) { for (const fn of middlewares || []) { if (typeof fn === "function" && !fn()) { let fallback401 = errorViews == null ? void 0 : errorViews.find((s) => s.statusCode === 401); if (!fallback401) { fallback401 = { component: () => createElement( "center", { style: "position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%)" }, createElement("h1", null, "401 Access Denied") ) }; } return createElement(fallback401.component, fallback401.props); } } } return createElement(component, { params }); } } let fallback404 = errorViews == null ? void 0 : errorViews.find((s) => s.statusCode === 404); if (!fallback404) { fallback404 = { component: () => createElement( "center", { style: "position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%)" }, createElement("h1", null, "404 Not Found") ) }; } return createElement(fallback404.component, fallback404.props); } function createApp(root) { rootElement = root; return { render(component) { appInstance = { type: component, props: {}, hooks: [] }; rerender(); window.addEventListener("popstate", rerender); } }; } class IComponent { /** * Entry point */ constructor() { this.dom = null; } /** * Set the parent node element. * @param dom */ setDom(dom) { this.dom = dom; } /** * Get the dom or the specific element from the dom children * This method use querySelector in finding the element. * @param selector * @returns */ getDom(selector) { if (selector) return this.dom.querySelector(selector); return this.dom; } /** * Return an array of element that is matched to the selector parameter given. * This method use querySelectorAll in finding all the matched elements. * @param selector * @returns */ getDomAll(selector) { return this.dom.querySelectorAll(selector); } /** * Will be called before the render occur. */ willMount() { } /** * Mounting template to DOM container */ render() { } /** * Will be called after the render. */ onMount() { } } const createRef = (initial = null) => { let ref = { current: initial }; return ref; }; const Fragment = ({ children }) => children; const onSetup = (fn) => { if (typeof fn === "function") { setupEventList.push(fn); } }; export { Fragment, IComponent, Router, createApp, createElement, createRef, isClassComponent, isHTML, lazy, navigate, onSetup, useEffect, useLocation, useState }; //# sourceMappingURL=vanilact.js.map