nano-jsx
Version:
SSR first, lightweight 1kB JSX library.
203 lines • 6.42 kB
JavaScript
// 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