create-bablojstest
Version:
A lightweight, fast, and scalable Single Page Application framework built with vanilla JavaScript. BABLOJS provides React-like features including Virtual DOM, hooks, routing, and component-based architecture without any build step or external dependencies
388 lines (334 loc) • 12.5 kB
JavaScript
import Home from "../views/Home.js";
import { render } from "./bablo.js";
import { babloApp } from "./BabloApp.js";
import { A, Button, Div, H1, P, Strong } from "./html.js";
import { requests } from "./requests.js";
const notFound = () => {
return Div(
{
style: "width: 100%; height: 100vh; display: flex; justify-content: center; align-items: center; background-color: #f8f9fa;"
},
Div(
{ style: "text-align: center; color: #343a40;" },
H1(
{
style: "font-size: 3rem; font-weight: 700; margin-bottom: 1rem;",
},
"404"
),
P({ style: "font-size: 1.1rem;" }, "Page not found"),
A(
{
href: requests.url("/"),
style: " margin-top: 1rem; color: #007bff; text-decoration: none;",
},
"Go Home"
)
)
);
}
const errorPage = (error) => {
return Div(
{
style: "width: 100%; height: 100vh; display: flex; justify-content: center; align-items: center; background-color: #f8f9fa;"
},
Div(
{ style: "text-align: center; color: #343a40;" },
H1(
{
style: "font-size: 3rem; font-weight: 700; margin-bottom: 1rem;",
},
"Error"
),
P({ style: "font-size: 1.1rem;" }, error),
A(
{
href: requests.url("/"),
style: " margin-top: 1rem; color: #007bff; text-decoration: none;",
},
"Go Home"
)
)
);
}
const unauthorized = () =>
Div(
{
style: {
width: "100%",
height: "100vh",
display: "flex",
justifyContent: "center",
alignItems: "center",
backgroundColor: "#f8f9fa",
},
},
Div(
{ style: { textAlign: "center", color: "#343a40" } },
H1(
{
style: { fontSize: "3rem", fontWeight: "700", marginBottom: "1rem" },
},
"Unauthorized"
),
P(
{ style: { fontSize: "1.1rem" } },
"You don’t have permission to access this page."
),
A(
{
href: requests.url("/login"),
style: {
marginTop: "1rem",
color: "#007bff",
textDecoration: "none",
},
},
"Login"
)
)
);
function setPageProps(routeObj) {
if (!routeObj || typeof routeObj !== "object") return;
// fallback checks meta, direct, app config, then globally defined (if any), then fallbackVal, empty
const fallback = (k, fallbackVal) =>
(routeObj?.meta && routeObj.meta[k] != null ? routeObj.meta[k]
: (routeObj[k] != null ? routeObj[k]
: (babloApp?.app && babloApp.app[k] != null ? babloApp.app[k]
: (babloApp && babloApp[k] != null ? babloApp[k]
: fallbackVal ?? ""))));
const metaTags = {
title: fallback("title", babloApp?.app?.name),
description: fallback("description", babloApp?.app?.description),
keywords: fallback("keywords", babloApp?.app?.keywords),
robots: fallback("robots", babloApp?.app?.robots),
author: fallback("author", babloApp?.app?.author),
license: fallback("license", babloApp?.app?.license),
ogImage: fallback("ogImage", babloApp?.app?.ogImage),
twitterImage: fallback("twitterImage", babloApp?.app?.twitterImage),
twitterTitle: fallback("twitterTitle", babloApp?.app?.twitterTitle),
twitterDescription: fallback("twitterDescription", babloApp?.app?.twitterDescription),
twitterCard: fallback("twitterCard", babloApp?.app?.twitterCard),
twitterUrl: fallback("twitterUrl", babloApp?.app?.twitterUrl),
twitterSite: fallback("twitterSite", babloApp?.app?.twitterSite),
twitterCreator: fallback("twitterCreator", babloApp?.app?.twitterCreator),
twitterDomain: fallback("twitterDomain", babloApp?.app?.twitterDomain),
};
// Set page title
document.title =
metaTags.title && metaTags.title !== babloApp?.app?.name
? `${metaTags.title} - ${babloApp?.app?.name || ""}`
: babloApp?.app?.name || "";
// Utility to get-or-create a meta tag
const getOrCreateMeta = (selector, attrs = {}) => {
let el = document.querySelector(selector);
if (!el) {
el = document.createElement("meta");
Object.entries(attrs).forEach(([k, v]) => el.setAttribute(k, v));
document.head.appendChild(el);
}
return el;
};
// Standard Meta Tags
getOrCreateMeta('meta[name="description"]', { name: "description" }).content =
metaTags.description || "";
getOrCreateMeta('meta[name="keywords"]', { name: "keywords" }).content =
metaTags.keywords || "";
getOrCreateMeta('meta[name="robots"]', { name: "robots" }).content =
metaTags.robots || "";
getOrCreateMeta('meta[name="author"]', { name: "author" }).content =
metaTags.author || "";
getOrCreateMeta('meta[name="license"]', { name: "license" }).content =
metaTags.license || "";
// Open Graph (og:) Tags for better social sharing
getOrCreateMeta('meta[property="og:title"]', { property: "og:title" }).content =
metaTags.title && metaTags.title !== babloApp?.app?.name
? `${metaTags.title} - ${babloApp?.app?.name || ""}`
: babloApp?.app?.name || "";
getOrCreateMeta('meta[property="og:description"]', { property: "og:description" }).content =
metaTags.description || "";
getOrCreateMeta('meta[property="og:type"]', { property: "og:type" }).content =
"website";
getOrCreateMeta('meta[property="og:url"]', { property: "og:url" }).content =
window.location.href || "";
if (metaTags.ogImage) {
getOrCreateMeta('meta[property="og:image"]', { property: "og:image" }).content = metaTags.ogImage;
}
// Twitter Card meta (basic support)
getOrCreateMeta('meta[name="twitter:card"]', { name: "twitter:card" }).content =
metaTags.twitterCard || "summary_large_image";
getOrCreateMeta('meta[name="twitter:title"]', { name: "twitter:title" }).content =
metaTags.twitterTitle ||
(metaTags.title && metaTags.title !== babloApp?.app?.name
? `${metaTags.title} - ${babloApp?.app?.name || ""}`
: babloApp?.app?.name || "");
getOrCreateMeta('meta[name="twitter:description"]', { name: "twitter:description" }).content =
metaTags.twitterDescription || metaTags.description || "";
if (metaTags.twitterImage) {
getOrCreateMeta('meta[name="twitter:image"]', { name: "twitter:image" }).content = metaTags.twitterImage;
}
if (metaTags.twitterUrl) {
getOrCreateMeta('meta[name="twitter:url"]', { name: "twitter:url" }).content = metaTags.twitterUrl;
}
if (metaTags.twitterSite) {
getOrCreateMeta('meta[name="twitter:site"]', { name: "twitter:site" }).content = metaTags.twitterSite;
}
if (metaTags.twitterCreator) {
getOrCreateMeta('meta[name="twitter:creator"]', { name: "twitter:creator" }).content = metaTags.twitterCreator;
}
if (metaTags.twitterDomain) {
getOrCreateMeta('meta[name="twitter:domain"]', { name: "twitter:domain" }).content = metaTags.twitterDomain;
}
}
export class Router {
constructor() {
this.routes = {};
}
async go(route) {
const newRoute = route.includes(requests.url(""))
? route
: requests.url(route);
window.history.pushState({}, "", newRoute);
//console.log("Pushed to history:", newRoute);
// Update babloApp location properties
babloApp.href = window.location.href;
babloApp.pathname = window.location.pathname;
babloApp.search = window.location.search;
babloApp.hash = window.location.hash;
// Dispatch custom event for route change
window.dispatchEvent(new CustomEvent('routechange', { detail: { route: newRoute } }));
await this.route(newRoute);
return newRoute;
}
async redirectHard(route) {
const newRoute = route.includes(requests.url(""))
? route
: requests.url(route);
window.location.href = newRoute;
return;
}
async redirect(route) {
return await this.go(route);
}
async navigate(route) {
return await this.go(route);
}
init() {
this.routeNavigator();
}
async route(route = null, component = null) {
try {
babloApp.appState.clear();
babloApp.appState.clear();
let cleanRoute = route;
if (!route) {
// get befor first ? mark
route = babloApp.href;
}
cleanRoute = route.replace(babloApp.baseUrl, "").toLowerCase() || "/";
if (cleanRoute.endsWith("/") && cleanRoute.length > 1) {
cleanRoute = "/" + cleanRoute.slice(0, -1);
}
if (!cleanRoute.startsWith("/")) {
cleanRoute = "/" + cleanRoute;
}
if (route.startsWith("/") && !route.startsWith(babloApp.baseUrl)) {
cleanRoute = route;
}
cleanRoute = cleanRoute.includes("?") ? cleanRoute.split("?")[0] : cleanRoute;
if (babloApp.routes) {
this.routes = babloApp.routes;
}
let routeObj = this.routes[cleanRoute];
if (!routeObj && !component) {
setPageProps({ title: "404 - Page Not Found", description: "" });
return render(notFound, babloApp.root);
}
if (component && typeof component === "function") {
setPageProps({ title: "Component", description: "" });
render(component, babloApp.root);
return;
}
//console.log("Component:", component);
if (component && typeof component === "object") {
routeObj = component;
}
//console.log("ROOT:", babloApp.root);
if (routeObj) {
setPageProps(routeObj);
let component;
// Handle both lazy loading and direct imports
if (typeof routeObj.component === "function") {
// Store the original function in case we need it
const componentFn = routeObj.component;
// Try calling it to check if it returns a Promise (lazy loading)
const result = componentFn();
if (result && typeof result.then === "function") {
// Lazy loading: result is a Promise
const module = await result;
component = module.default || module;
} else {
// Direct import: the function itself is the component
// (we called it and got a VNode, but render needs the function)
component = componentFn;
}
} else {
// Component is already the function itself
component = routeObj.component;
}
if (babloApp.componentState.has("component-state")) {
babloApp.componentState.delete("component-state");
}
babloApp.componentState.set("component-state", component);
render(component, babloApp.root);
return;
}
//console.log("No matching route found.");
} catch (error) {
console.error("Error loading route:", error);
setPageProps({ title: "Error", description: "" });
babloApp.root.innerHTML = "";
render(() => errorPage(error.toString()), babloApp.root);
}
}
routeNavigator() {
window.addEventListener("popstate", () => {
// Update babloApp location properties on popstate
babloApp.href = window.location.href;
babloApp.pathname = window.location.pathname;
babloApp.search = window.location.search;
babloApp.hash = window.location.hash;
// Dispatch custom event for route change
window.dispatchEvent(new CustomEvent('routechange', { detail: { route: window.location.pathname } }));
this.navigate(requests.windowGetHref());
});
document.addEventListener("click", (event) => {
const target = event.target.closest("a") || event.target;
const route = target.getAttribute("route");
const component = target.getAttribute("component");
if (target.tagName === "A") {
const href = target.href;
if (
!target.hasAttribute("refresh") &&
!target.hasAttribute("download") &&
!target.hasAttribute("target") &&
!href.includes("mailto:") &&
!href.includes("tel:") &&
!href.startsWith("#") &&
!href.includes("javascript:")
) {
event.preventDefault();
//console.log("HREF", href);
this.go(href);
}
} else if (route) {
this.go(requests.url(route));
} else if (component) {
render(component(), babloApp.root);
}
});
}
}
// Export a default router instance for convenience
// This will be configured in main.js with routes
export const router = new Router();