@app-elements/router
Version:
The best router.
561 lines (465 loc) • 13.9 kB
JavaScript
import React, { createContext, useContext, useRef, useEffect, useState } from 'react';
function _extends() {
_extends = Object.assign || function (target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i];
for (var key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
target[key] = source[key];
}
}
}
return target;
};
return _extends.apply(this, arguments);
}
function _objectWithoutPropertiesLoose(source, excluded) {
if (source == null) return {};
var target = {};
var sourceKeys = Object.keys(source);
var key, i;
for (i = 0; i < sourceKeys.length; i++) {
key = sourceKeys[i];
if (excluded.indexOf(key) >= 0) continue;
target[key] = source[key];
}
return target;
}
var has = Object.prototype.hasOwnProperty;
/**
* Decode a URI encoded string.
*
* @param {String} input The URI encoded string.
* @returns {String} The decoded string.
* @api private
*/
var decode = function decode(input) {
return decodeURIComponent(input.replace(/\+/g, ' '));
};
/**
* Simple query string parser.
*
* @param {String} query The query string that needs to be parsed.
* @returns {Object}
* @api public
*/
function parse(query) {
var parser = /([^=?&]+)=?([^&]*)/g;
var result = {};
var part;
while ((part = parser.exec(query)) != null) {
var key = decode(part[1]); //
// Prevent overriding of existing properties. This ensures that build-in
// methods like `toString` or __proto__ are not overriden by malicious
// querystrings.
//
if (key in result) continue;
result[key] = decode(part[2]);
}
return result;
}
/**
* Transform an object into a query string.
*
* @param {Object} obj Object that should be transformed.
* @param {String} prefix Optional prefix.
* @returns {String}
* @api public
*/
function stringify(obj, prefix) {
if (prefix === void 0) {
prefix = '';
}
var pairs = []; //
// Optionally prefix with a '?' if needed
//
if (typeof prefix !== 'string') prefix = '?';
for (var key in obj) {
if (has.call(obj, key)) {
pairs.push(encodeURIComponent(key) + '=' + encodeURIComponent(obj[key]));
}
}
return pairs.length ? prefix + pairs.join('&') : '';
}
var hasProp = Object.prototype.hasOwnProperty;
var segmentize = function segmentize(url) {
return url.replace(/(^\/+|\/+$)/g, '').split('/');
};
function updateQuery(queries) {
var existingParams = parse(window.location.search);
return window.location.pathname + ("?" + stringify(_extends({}, existingParams, queries)));
} // route matching logic, taken from preact-router
var exec = function exec(url, route) {
var reg = /(?:\?([^#]*))?(#.*)?$/;
var c = url.match(reg);
var matches = {};
var ret;
if (c && c[1]) {
var p = c[1].split('&');
for (var i = 0; i < p.length; i++) {
var r = p[i].split('=');
matches[decodeURIComponent(r[0])] = decodeURIComponent(r.slice(1).join('='));
}
}
url = segmentize(url.replace(reg, ''));
route = segmentize(route || '');
var max = Math.max(url.length, route.length);
for (var _i = 0; _i < max; _i++) {
if (route[_i] && route[_i].charAt(0) === ':') {
var param = route[_i].replace(/(^:|[+*?]+$)/g, '');
var flags = (route[_i].match(/[+*?]+$/) || {})[0] || '';
var plus = ~flags.indexOf('+');
var star = ~flags.indexOf('*');
var val = url[_i] || '';
if (!val && !star && (flags.indexOf('?') < 0 || plus)) {
ret = false;
break;
}
matches[param] = decodeURIComponent(val);
if (plus || star) {
matches[param] = url.slice(_i).map(decodeURIComponent).join('/');
break;
}
} else if (route[_i] !== url[_i]) {
ret = false;
break;
}
}
if (ret === false) return false;
return matches;
};
function getRouteComponent(routes, currentPath) {
for (var route in routes) {
if (hasProp.call(routes[route], 'routes')) {
var shouldRender = Object.values(routes[route].routes).some(function (_ref) {
var path = _ref.path;
return path && exec(currentPath, path);
});
if (shouldRender) {
var App = routes[route].component;
return [App, null];
}
} else {
var routeArgs = exec(currentPath, routes[route].path);
if (routeArgs) {
var newRoute = {
name: route,
path: routes[route].path,
args: _extends({}, routes[route].args || {}, routeArgs)
};
var Component = routes[route].component;
return [Component, newRoute];
}
}
}
}
var getAllRoutes = function getAllRoutes(routes) {
return Object.keys(routes || {}).reduce(function (acc, r) {
var _extends2;
return hasProp.call(routes[r], 'routes') ? _extends({}, acc, getAllRoutes(routes[r].routes)) : _extends({}, acc, (_extends2 = {}, _extends2[r] = routes[r], _extends2));
}, {});
};
var getHref = function getHref(_ref2) {
var rule = _ref2.rule,
args = _ref2.args,
queries = _ref2.queries,
hash = _ref2.hash;
var replaced = Object.keys(args).reduce(function (acc, k) {
return acc.replace(":" + k, args[k]);
}, rule.path);
var hasQueries = Object.keys(queries).length > 0;
var hashStr = hash != null ? "#" + hash : '';
return "" + replaced + (!hasQueries ? '' : '?' + stringify(queries)) + hashStr;
};
var Context = createContext('Router');
var allRoutes;
var skipScroll;
function useRouterState() {
var _useState = useState(null),
path = _useState[0],
setPath = _useState[1];
var _useState2 = useState(null),
route = _useState2[0],
setRoute = _useState2[1];
var routeTo = function routeTo(newPath) {
if (newPath !== path) {
window.history.pushState(null, null, newPath);
setPath(newPath);
}
};
var setPathMaybe = function setPathMaybe(newPath) {
if (path == null || path !== newPath) {
setPath(newPath);
}
};
var setRouteMaybe = function setRouteMaybe(newRoute) {
if (route == null || route.name !== newRoute.name) {
setRoute(newRoute);
}
};
return {
path: path,
setPath: setPathMaybe,
routeTo: routeTo,
route: route,
setRoute: setRouteMaybe
};
}
function useRouter() {
var value = useContext(Context);
if (value == null) {
throw new Error('Component must be wrapped with <RouteProvider>');
}
return value;
}
function useScrollToTop() {
var backRef = useRef(false);
var nodeRef = useRef();
var pathRef = useRef();
var _useRouter = useRouter(),
path = _useRouter.path;
useEffect(function () {
var onPop = function onPop() {
return backRef.current = true;
};
window.addEventListener('popstate', onPop);
return function () {
return window.removeEventListener('popstate', onPop);
};
}, []);
var isBack = backRef && backRef.current;
if (!isBack && path !== pathRef.current) {
if (path !== skipScroll) {
if (nodeRef && nodeRef.current) {
nodeRef.current.scrollIntoView();
} else {
window.scrollTo(0, 0);
}
} else {
skipScroll = null;
}
pathRef.current = path;
} else if (isBack) {
backRef.current = false;
}
return nodeRef;
}
function Link(_ref) {
var to = _ref.to,
name = _ref.name,
className = _ref.className,
activeClass = _ref.activeClass,
_ref$args = _ref.args,
args = _ref$args === void 0 ? {} : _ref$args,
_ref$queries = _ref.queries,
queries = _ref$queries === void 0 ? {} : _ref$queries,
hash = _ref.hash,
noScroll = _ref.noScroll,
children = _ref.children,
props = _objectWithoutPropertiesLoose(_ref, ["to", "name", "className", "activeClass", "args", "queries", "hash", "noScroll", "children"]);
if (allRoutes == null) {
throw new Error('<Link /> must be child of <RouteProvider />');
}
to = to || name;
var rule = allRoutes[to];
if (!rule) {
console.error('No route found for name: ' + to);
return null;
}
var href = getHref({
rule: rule,
args: args,
queries: queries,
hash: hash
});
return /*#__PURE__*/React.createElement(Context.Consumer, null, function (_ref2) {
var path = _ref2.path,
routeTo = _ref2.routeTo;
var isActive = path === href;
var onClick = function onClick(ev) {
ev.preventDefault();
if (noScroll) {
skipScroll = href;
}
routeTo(href);
};
return /*#__PURE__*/React.createElement("a", _extends({
href: href,
onClick: onClick,
className: ((className || '') + " " + (isActive ? activeClass : '')).trim()
}, props), children);
});
}
function RouteTo(_ref3) {
var to = _ref3.to,
name = _ref3.name,
url = _ref3.url,
_ref3$args = _ref3.args,
args = _ref3$args === void 0 ? {} : _ref3$args,
_ref3$queries = _ref3.queries,
queries = _ref3$queries === void 0 ? {} : _ref3$queries,
hash = _ref3.hash;
var href;
to = to || name;
if (url != null) {
href = url;
} else {
if (allRoutes == null) {
throw new Error('<RouteTo /> must be child of <RouteProvider />');
}
var rule = allRoutes[to];
if (!rule) {
throw new Error('No route found for name: ' + to);
}
href = getHref({
rule: rule,
args: args,
queries: queries,
hash: hash
});
}
return /*#__PURE__*/React.createElement(Context.Consumer, null, function (context) {
if (href) {
context.routeTo(href);
}
return null;
});
}
function SyncRouterState(_ref4) {
var children = _ref4.children;
var routeNameRef = useRef(null);
if (!children || typeof children !== 'function') {
throw new Error('<SyncRouterState /> requires a function as a child.');
}
return /*#__PURE__*/React.createElement(Context.Consumer, null, function (context) {
if (!context || context.route == null) return null;
if (routeNameRef.current == null || routeNameRef.current !== context.route.name) {
children({
route: context.route,
path: context.path
});
routeNameRef.current = context.route.name;
}
});
}
function RouteProvider(_ref5) {
var routes = _ref5.routes,
initialPath = _ref5.initialPath,
children = _ref5.children;
var value = useRouterState();
if (allRoutes == null) {
allRoutes = getAllRoutes(routes);
}
useEffect(function () {
if (value.path == null) {
value.setPath(initialPath || window.location.pathname + window.location.search);
}
}, [value.path, value.setPath]);
useEffect(function () {
var onPop = function onPop(ev) {
var url = window.location.pathname;
if (value.path !== url) {
window.history.replaceState(null, null, url);
value.setPath(url);
}
};
window.addEventListener('popstate', onPop);
return function () {
return window.removeEventListener('popstate', onPop);
};
}, [value.path, value.setPath]);
return /*#__PURE__*/React.createElement(Context.Provider, {
value: value
}, children);
}
function Router(props) {
if (!props.routes) {
throw new Error('<Router /> must be given a routes object.');
}
var localRoutes = props.routes;
var context = useRouter();
if (context == null) {
throw new Error('<Router /> must be wrapped with <RouteProvider />.');
}
var path = context.path,
setRoute = context.setRoute;
if (path == null) {
return null;
}
var pair = getRouteComponent(localRoutes, path);
if (pair == null) {
setRoute({
name: 'Not Found',
args: {},
notFound: true,
path: path
});
return null;
}
var Component = pair[0],
newRoute = pair[1];
if (newRoute) {
setRoute(newRoute);
}
var childProps = newRoute != null ? newRoute.args : {};
return typeof Component === 'function' ? /*#__PURE__*/React.createElement(Component, childProps) : Component;
}
function StackRouter(_ref6) {
var localRoutes = _ref6.routes,
_ref6$limit = _ref6.limit,
limit = _ref6$limit === void 0 ? 2 : _ref6$limit,
children = _ref6.children;
var stackRef = useRef([]);
if (!localRoutes) {
throw new Error('<StackRouter /> must be given a routes object.');
}
var context = useRouter();
if (context == null) {
throw new Error('<StackRouter /> must be wrapped with <RouteProvider />.');
}
useEffect(function () {
var onPop = function onPop() {
stackRef.current = stackRef.current.slice(0, -1);
};
window.addEventListener('popstate', onPop);
return function () {
return window.removeEventListener('popstate', onPop);
};
}, [stackRef]);
var path = context.path,
setRoute = context.setRoute;
if (path == null) {
return null;
}
var pair = getRouteComponent(localRoutes, path);
if (pair == null) {
setRoute({
name: 'Not Found',
args: {},
notFound: true,
path: path
});
return null;
}
var Component = pair[0],
newRoute = pair[1];
if (newRoute) {
setRoute(newRoute);
var last = stackRef.current[stackRef.current.length - 1];
if (last == null || last.path !== path) {
if (last != null) {
stackRef.current[stackRef.current.length - 1].isBack = true;
}
stackRef.current = [].concat(stackRef.current, Object.assign({}, newRoute, {
Component: Component,
path: path
}));
}
}
var stack = stackRef.current.length > limit ? stackRef.current.slice(-limit) : [].concat(stackRef.current);
return children({
stack: stack
});
}
export { Link, RouteProvider, RouteTo, Router, StackRouter, SyncRouterState, exec, getHref, updateQuery, useRouter, useScrollToTop };
//# sourceMappingURL=router.m.js.map