@devmore/vanilact
Version:
Pure vanila javascript.
352 lines (351 loc) • 11.5 kB
JavaScript
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