Tobias Speicher aca2f08b77 refactor: replace deprecated String.prototype.substr()
.substr() is deprecated so we replace it with .slice() which works similarily but isn't deprecated

Signed-off-by: Tobias Speicher <rootcommander@gmail.com>
2022-03-30 14:07:03 +08:00

289 lines
7.0 KiB
JavaScript

// Inspired by react-router and universal-router
import { useState, useEffect, useLayoutEffect, createElement } from 'rax';
import pathToRegexp from 'path-to-regexp';
const cache = {};
function decodeParam(val) {
try {
return decodeURIComponent(val);
} catch (err) {
return val;
}
}
function matchPath(route, pathname, parentParams) {
let { path, routes, exact: end = true, strict = false, sensitive = false } = route;
// If not has path or has routes that should do not exact match
if (path == null || routes) {
end = false;
}
// Default path is empty
path = path || '';
const regexpCacheKey = `${path}|${end}|${strict}|${sensitive}`;
const keysCacheKey = `${regexpCacheKey }|`;
let regexp = cache[regexpCacheKey];
const keys = cache[keysCacheKey] || [];
if (!regexp) {
regexp = pathToRegexp(path, keys, {
end,
strict,
sensitive,
});
cache[regexpCacheKey] = regexp;
cache[keysCacheKey] = keys;
}
const result = regexp.exec(pathname);
if (!result) {
return null;
}
const url = result[0];
const params = { ...parentParams, history: router.history, location: router.history.location };
for (let i = 1; i < result.length; i++) {
const key = keys[i - 1];
const prop = key.name;
const value = result[i];
if (value !== undefined || !Object.prototype.hasOwnProperty.call(params, prop)) {
if (key.repeat) {
params[prop] = value ? value.split(key.delimiter).map(decodeParam) : [];
} else {
params[prop] = value ? decodeParam(value) : value;
}
}
}
return {
path: !end && url.charAt(url.length - 1) === '/' ? url.slice(1) : url,
params,
};
}
function matchRoute(route, baseUrl, pathname, parentParams) {
let matched;
let childMatches;
let childIndex = 0;
return {
next() {
if (!matched) {
matched = matchPath(route, pathname, parentParams);
if (matched) {
return {
done: false,
$: {
route,
baseUrl,
path: matched.path,
params: matched.params,
},
};
}
}
if (matched && route.routes) {
while (childIndex < route.routes.length) {
if (!childMatches) {
const childRoute = route.routes[childIndex];
childRoute.parent = route;
childMatches = matchRoute(
childRoute,
baseUrl + matched.path,
pathname.slice(matched.path.length),
matched.params,
);
}
const childMatch = childMatches.next();
if (!childMatch.done) {
return {
done: false,
$: childMatch.$,
};
}
childMatches = null;
childIndex++;
}
}
return { done: true };
},
};
}
let _initialized = false;
let _routerConfig = null;
const router = {
history: null,
handles: [],
errorHandler() { },
addHandle(handle) {
return router.handles.push(handle);
},
removeHandle(handleId) {
router.handles[handleId - 1] = null;
},
triggerHandles(component) {
router.handles.forEach((handle) => {
handle && handle(component);
});
},
match(fullpath) {
if (fullpath == null) return;
router.fullpath = fullpath;
const parent = router.root;
const matched = matchRoute(
parent,
parent.path,
fullpath,
);
function next(parent) {
const current = matched.next();
if (current.done) {
const error = new Error(`No match for ${fullpath}`);
return router.errorHandler(error, router.history.location);
}
let { component } = current.$.route;
if (typeof component === 'function') {
component = component(current.$.params, router.history.location);
}
if (component instanceof Promise) {
// Lazy loading component by import('./Foo')
return component.then((component) => {
// Check current fullpath avoid router has changed before lazy loading complete
if (fullpath === router.fullpath) {
router.triggerHandles(component);
}
});
} else if (component != null) {
router.triggerHandles(component);
return component;
} else {
return next(parent);
}
}
return next(parent);
},
};
function matchLocation({ pathname }) {
router.match(pathname);
}
function getInitialComponent(routerConfig) {
let InitialComponent = [];
if (_routerConfig === null) {
if (process.env.NODE_ENV !== 'production') {
if (!routerConfig) {
throw new Error('Error: useRouter should have routerConfig, see: https://www.npmjs.com/package/rax-use-router.');
}
if (!routerConfig.history || !routerConfig.routes) {
throw new Error('Error: routerConfig should contain history and routes, see: https://www.npmjs.com/package/rax-use-router.');
}
}
_routerConfig = routerConfig;
}
if (_routerConfig.InitialComponent) {
InitialComponent = _routerConfig.InitialComponent;
}
router.history = _routerConfig.history;
return InitialComponent;
}
let unlisten = null;
let handleId = null;
let pathes = '';
export function useRouter(routerConfig) {
const [component, setComponent] = useState(getInitialComponent(routerConfig));
let newPathes = '';
if (routerConfig) {
_routerConfig = routerConfig;
const { routes } = _routerConfig;
router.root = Array.isArray(routes) ? { routes } : routes;
if (Array.isArray(routes)) {
newPathes = routes.map(it => it.path).join(',');
} else {
newPathes = routes.path;
}
}
if (_initialized && _routerConfig.history) {
if (newPathes !== pathes) {
matchLocation(_routerConfig.history.location);
pathes = newPathes;
}
}
useLayoutEffect(() => {
if (unlisten) {
unlisten();
unlisten = null;
}
if (handleId) {
router.removeHandle(handleId);
handleId = null;
}
const { history } = _routerConfig;
const { routes } = _routerConfig;
router.root = Array.isArray(routes) ? { routes } : routes;
handleId = router.addHandle((component) => {
setComponent(component);
});
// Init path match
if (_initialized || !_routerConfig.InitialComponent) {
matchLocation(history.location);
pathes = newPathes;
}
unlisten = history.listen(({ location }) => {
matchLocation(location);
pathes = newPathes;
});
_initialized = true;
return () => {
pathes = '';
router.removeHandle(handleId);
handleId = null;
unlisten();
unlisten = null;
};
}, []);
return { component };
}
export function withRouter(Component) {
function Wrapper(props) {
const { history } = router;
return createElement(Component, { ...props, history, location: history.location });
}
Wrapper.displayName = `withRouter(${ Component.displayName || Component.name })`;
Wrapper.WrappedComponent = Component;
return Wrapper;
}