UNPKG

nano-jsx

Version:

SSR first, lightweight 1kB JSX library.

203 lines 6.42 kB
// inspired by https://codesandbox.io/s/build-own-react-router-v4-mpslz import { Component } from '../component.js'; import { _render, h, isSSR } from '../core.js'; const instances = []; const register = (comp) => instances.push(comp); const unregister = (comp) => instances.splice(instances.indexOf(comp), 1); const historyPush = (path) => { window.history.pushState({}, '', path); instances.forEach(instance => instance.handleChanges()); window.dispatchEvent(new Event('pushstate')); }; const historyReplace = (path) => { window.history.replaceState({}, '', path); instances.forEach(instance => instance.handleChanges()); window.dispatchEvent(new Event('replacestate')); }; export const matchPath = (pathname, options) => { const { exact = false, regex } = options; let { path } = options; if (!path) { return { path: null, url: pathname, isExact: true, params: {} }; } let match; let params = {}; // path with params if (path.includes('/:')) { const pathArr = path.split('/'); const pathnameArr = pathname.split('/'); pathArr.forEach((p, i) => { if (/^:/.test(p)) { const key = p.slice(1); const value = pathnameArr[i]; // if a regex is provided, check it it matches if (regex && regex[key]) { const regexMatch = regex[key].test(value); if (!regexMatch) return null; } params = { ...params, [key]: value }; pathArr[i] = pathnameArr[i]; } }); path = pathArr.join('/'); } // catch all if (path === '*') match = [pathname]; // regular path if (!match) match = new RegExp(`^${path}`).exec(pathname); if (!match) return null; const url = match[0]; const isExact = pathname === url; if (exact && !isExact) return null; return { path, url, isExact, params }; }; export class Switch extends Component { constructor() { super(...arguments); this.index = 0; this.path = ''; this.match = { index: -1, path: '' }; } didMount() { window.addEventListener('popstate', this.handleChanges.bind(this)); register(this); } didUnmount() { window.removeEventListener('popstate', this.handleChanges.bind(this)); unregister(this); } handleChanges() { this.findChild(); if (this.shouldUpdate()) this.update(); } findChild() { this.match = { index: -1, path: '' }; // flatten children this.props.children = this.props.children.flat(); for (let i = 0; i < this.props.children.length; i++) { const child = this.props.children[i]; const { path, exact, regex } = child.props; const match = matchPath(isSSR() ? _nano.location.pathname : window.location.pathname, { path, exact, regex }); if (match) { this.match.index = i; this.match.path = path; return; } } } shouldUpdate() { return this.path !== this.match.path || this.index !== this.match.index; } render() { this.findChild(); const child = this.props.children[this.match.index]; if (this.match.index === -1) { this.path = ''; this.index = 0; } if (child) { const { path } = child.props; this.path = path; this.index = this.match.index; const el = _render(child); return h('div', {}, _render(el)); } else if (this.props.fallback) { return h('div', {}, _render(this.props.fallback)); } else { return h('div', {}, 'not found'); } } } // alias for <Switch /> export class Routes extends Switch { } export const Route = ({ path, regex, children }) => { // lookup pathname and parameters const pathname = isSSR() ? _nano.location.pathname : window.location.pathname; const params = parseParamsFromPath(path); // pass the route as props to the children children.forEach((child) => { if (child.props) child.props = { ...child.props, route: { path, regex, pathname, params } }; }); return children; }; export const to = (to, replace = false) => { replace ? historyReplace(to) : historyPush(to); }; export const Link = ({ to, replace, children, ...rest }) => { const handleClick = (event) => { event.preventDefault(); replace ? historyReplace(to) : historyPush(to); }; return h('a', { href: to, onClick: (e) => handleClick(e), ...rest }, children); }; class CListener { constructor() { this._listeners = new Map(); if (isSSR()) return; this._route = window.location.pathname; const event = (e) => { const newRoute = window.location.pathname; this._listeners.forEach(fnc => { fnc(newRoute, this._route, e); }); this._route = newRoute; }; window.addEventListener('pushstate', event); window.addEventListener('replacestate', event); window.addEventListener('popstate', event); } use() { const id = Math.random().toString(36).substring(2); return { subscribe: (fnc) => { this._listeners.set(id, fnc); }, cancel: () => { if (this._listeners.has(id)) this._listeners.delete(id); } }; } } let listener; export const Listener = () => { if (!listener) listener = new CListener(); return listener; }; /** Pass "this.props.route.path" to it. */ export const parseParamsFromPath = (path) => { let params = {}; const _pathname = isSSR() ? _nano.location.pathname.split('/') : window.location.pathname.split('/'); path.split('/').forEach((p, i) => { if (p.startsWith(':')) params = { ...params, [p.slice(1)]: _pathname[i] }; }); return params; }; //# sourceMappingURL=router.js.map