UNPKG

frontend-hamroun

Version:

A lightweight frontend JavaScript framework with React-like syntax

166 lines (165 loc) 5.08 kB
/** * Router implementation for client and server navigation */ import { useState, useEffect } from './hooks.js'; import { createContext, useContext } from './context.js'; import { jsx } from './jsx-runtime.js'; export const RouterContext = createContext({ currentPath: '/', navigate: () => { }, params: {}, query: {} }); export function useRouter() { return useContext(RouterContext); } export function useLocation() { const router = useContext(RouterContext); return { pathname: router.currentPath, search: '', // TODO: Implement search parsing hash: '', // TODO: Implement hash parsing state: {} // TODO: Implement history state }; } export function useParams() { const router = useContext(RouterContext); return router.params; } export function useNavigate() { const router = useContext(RouterContext); return router.navigate; } export function RouterProvider({ children }) { // Initial path should be window.location.pathname on client, or passed from server context const [currentPath, setCurrentPath] = useState(typeof window !== 'undefined' ? window.location.pathname : '/'); const [params, setParams] = useState({}); const [query, setQuery] = useState({}); // Update path when URL changes useEffect(() => { if (typeof window === 'undefined') return; const handlePopState = () => { setCurrentPath(window.location.pathname); parseQuery(window.location.search); }; window.addEventListener('popstate', handlePopState); // Parse initial query parseQuery(window.location.search); return () => { window.removeEventListener('popstate', handlePopState); }; }, []); // Parse query string into object const parseQuery = (queryString) => { const queryObj = {}; if (queryString.startsWith('?')) { queryString = queryString.substring(1); } queryString.split('&').forEach(pair => { if (!pair) return; const [key, value] = pair.split('='); queryObj[decodeURIComponent(key)] = decodeURIComponent(value || ''); }); setQuery(queryObj); }; // Navigate programmatically const navigate = (path) => { if (typeof window === 'undefined') return; window.history.pushState(null, '', path); setCurrentPath(path); parseQuery(window.location.search); }; return jsx(RouterContext.Provider, { value: { currentPath, navigate, params, query }, children }); } export function Router({ routes }) { const router = useRouter(); const currentPath = router.currentPath; // Find matching route const matchedRoute = findMatchingRoute(routes, currentPath); if (!matchedRoute) { return jsx('div', { children: '404 - Page Not Found' }); } return matchedRoute.component({ children: matchedRoute.children }); } function findMatchingRoute(routes, path) { for (const route of routes) { const match = matchPath(path, route); if (match) { return { ...route, params: match.params }; } } return null; } function matchPath(pathname, route) { const { path, exact = true } = route; // Convert route pattern to regex const pattern = path.replace(/:[a-zA-Z0-9_]+/g, '([^/]+)'); const regex = new RegExp(`^${pattern}${exact ? '$' : ''}`); const match = pathname.match(regex); if (!match) { return null; } // Extract params const params = {}; const paramNames = path.match(/:[a-zA-Z0-9_]+/g) || []; paramNames.forEach((paramName, index) => { params[paramName.substring(1)] = match[index + 1]; }); return { params }; } export function Link({ to, children, ...rest }) { const router = useRouter(); const navigate = router.navigate; const handleClick = (e) => { e.preventDefault(); navigate(to); }; return jsx('a', { href: to, onClick: handleClick, ...rest, children }); } export function Route({ path, component }) { // This is a configuration component, not actually rendered return null; } export function Switch({ children }) { const router = useRouter(); const currentPath = router.currentPath; // Find the first matching route const child = Array.isArray(children) ? children.find(child => { if (!child || !child.props) return false; const routeObj = { path: child.props.path, component: child.props.component, exact: child.props.exact }; return matchPath(currentPath, routeObj); }) : children; return child || null; } export default { RouterProvider, Router, Link, Route, Switch, useRouter, useParams, useNavigate, useLocation };