frontend-hamroun
Version: 
A lightweight frontend JavaScript framework with React-like syntax
166 lines (165 loc) • 5.08 kB
JavaScript
/**
 * 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
};