radi-router
Version:
`radi-router` is the official router for [Radi.js](https://radi.js.org). It deeply integrates with Radi for seamless application building.
435 lines (370 loc) • 10.6 kB
JavaScript
export const version = '0.4.1';
// Pass routes to initiate things
export default ({
r,
l,
mount,
headless,
Component,
}, routes) => {
let current = {};
const COLON = ':'.charCodeAt(0);
const SLASH = '/'.charCodeAt(0);
var cr, crg, lr, ld;
const parseRoute = route => {
var parts = route.split('/'),
end = [],
p = [];
for (var i = 0; i < parts.length; i++) {
if (COLON === parts[i].charCodeAt(0)) {
end.push('([^/]+?)');
p.push(parts[i].substr(1));
} else if (parts[i] !== '') {
end.push(parts[i]);
}
}
return [new RegExp('^/' + end.join('/') + '(?:[/])?(?:[?&].*)?$', 'i'), p];
};
const parseAllRoutes = arr => {
var len = arr.length,
ret = new Array(len);
for (var i = len - 1; i >= 0; i--) {
ret[i] = parseRoute(arr[i]);
}
return ret;
};
const renderError = number => {
return current.config.errors[number]();
};
const writeUrl = url => {
window.location.hash = url;
return true;
};
const extractChildren = routes => {
let children = routes;
for (let child in routes) {
if (routes.hasOwnProperty(child) && routes[child].children) {
let extracted = extractChildren(routes[child].children);
for (var nn in extracted) {
if (extracted.hasOwnProperty(nn)) {
children[child + nn] = extracted[nn];
}
}
}
}
return children;
};
const getRoute = curr => {
if (lr === curr) return ld;
if (!cr) cr = Object.keys(current.routes);
if (!crg) crg = parseAllRoutes(cr);
var cahnged = false;
for (var i = 0; i < crg.length; i++) {
if (crg[i][0].test(curr)) {
ld = new Route(curr, crg[i], current.routes, cr[i]);
cahnged = true;
break;
}
}
lr = curr;
return !cahnged ? { key: null } : ld;
};
class Route {
constructor(curr, match, routes, key) {
const query = curr
.split(/[\?\&]/)
.slice(1)
.map(query => query.split('='))
.reduce(
(acc, key) =>
Object.assign(acc, {
[key[0]]: key[1]
}),
{}
);
var m = curr.match(match[0]);
this.path = curr;
this.key = key;
this.query = query;
this.cmp = routes[key] || {};
this.params = this.cmp.data || {};
for (var i = 0; i < match[1].length; i++) {
this.params[match[1][i]] = m[i + 1];
}
}
}
const combineTitle = (...args) => (
args.reduce((a, v) => (v ? a.concat(v) : a), [])
);
class Title extends Component {
constructor(...args) {
super(...args);
this.state = {
prefix: null,
text: null,
suffix: null,
seperator: ' | ',
};
}
setPrefix(prefix) {
return this.setState({prefix}, 'setPrefix');
}
setSuffix(suffix) {
return this.setState({suffix}, 'setSuffix');
}
set(text) {
return this.setState({text}, 'set');
}
setSeperator(seperator) {
return this.setState({seperator}, 'setSeperator');
}
onUpdate() {
let titleConfig = this.$router.getTitle() || {};
let pefix = (routes.title && routes.title.prefix) || this.state.prefix;
let text = titleConfig.text || (routes.title && routes.title.text) || this.state.text;
let suffix = (routes.title && routes.title.suffix) || this.state.suffix;
let title = combineTitle(
pefix,
text,
suffix
).join(this.state.seperator);
if (title && document.title !== title) {
document.title = title;
}
if (this.state.text && text !== this.state.text) {
this.set(null);
}
}
}
class RouterHead extends Component {
constructor(...args) {
super(...args);
this.state = {
location: window.location.hash.substr(1) || '/',
params: {},
query: {},
last: null,
active: null,
activeComponent: null,
current: {
title: null,
tags: [],
meta: {},
},
};
}
onMount() {
window.onhashchange = () => (this.setState(this.hashChange()), this.chain());
this.setState(this.hashChange());
this.chain();
}
chain() {
this.trigger('changed', this.state.active || '', this.state.last);
}
hashChange() {
var loc = window.location.hash.substr(1) || '/';
var a = getRoute(loc);
// console.log('[radi-router] Route change', a, this.state.location);
window.scrollTo(0, 0);
// this.resolve(this.inject(a.key || '', this.state.active))
let title = a.cmp && a.cmp.title;
return {
last: this.state.active,
location: loc,
params: a.params || {},
query: a.query || {},
active: a.key || '',
current: {
title: (typeof title === 'object') ? title : {
text: title,
} || null,
tags: (a.cmp && a.cmp.tags) || [],
meta: (a.cmp && a.cmp.meta) || {},
},
}
}
hasTag(...tag) {
return tag.reduce((acc, t) => acc || (this.state.current.tags.indexOf(t) >= 0), false);
}
getMeta() {
return this.state.current.meta || {};
}
getTitle() {
let title = this.state.current.title;
return title;
}
}
class Link extends Component {
constructor(...args) {
super(...args);
this.state = {
to: '/',
active: 'active',
core: false,
class: '',
id: null,
title: null,
};
}
view() {
return r(
'a',
{
href: l(this, 'to').process(url => '#'.concat(url)),
class: l(this, 'to').process(to =>
l(this.$router, 'active').process(active =>
l(this, 'class').process(cls => ([
(active === to || (this.state.core && new RegExp('^' + to).test(active)))
&& this.state.active,
cls
]))
)
),
id: l(this, 'id'),
title: l(this, 'title'),
},
...this.children
);
}
}
class Router extends Component {
constructor(...args) {
super(...args);
this.state = {
active: null,
};
}
onMount() {
this.setState({active: this.$router.state.active});
this.$router.on('changed', (active, last) => {
this.setState({active});
});
}
view() {
// return [
// l(this.$router, 'active').process(() => r('div', {}, this.inject(this.$router.state))),
// ...this.children,
// ];
return [
l(this, 'active').process(comp => this.extractComponent(comp)),
this.children,
]
// return r(
// 'template',
// {},
// l(this, 'active').process(comp => this.extractComponent(comp)),
// this.children,
// );
// return [
// l(this.$router, 'activeComponent').process(comp => comp),
// this.children,
// ]
// return r(
// 'template',
// {},
// l(this.$router, 'activeComponent').process(comp => comp),
// this.children,
// );
}
// Triggers when route is changed
extractComponent(active, last) {
// Route is not yet ready
// For the future, maybe show cached page or default screen
// or loading placeholder if time between this and next request
// is too long
if (active === null && typeof last === 'undefined') return;
// const { active, last } = this.state
const RouteComponent = current.routes[active];
const WillRender =
typeof RouteComponent === 'object'
? RouteComponent.component
: RouteComponent;
// Route not found or predefined error
if (
(typeof WillRender === 'undefined' ||
typeof WillRender === 'number' ||
!WillRender) &&
typeof RouteComponent === 'undefined'
)
return renderError(WillRender || 404);
// Plain redirect
if (typeof RouteComponent.redirect === 'string')
return writeUrl(RouteComponent.redirect);
// Check if has any guards to check
const guards = [
current.before || null,
RouteComponent.before || null,
].filter(guard => typeof guard === 'function');
if (guards.length > 0) {
const checkGuard = (resolve, reject) => {
const popped = guards.pop();
if (typeof popped !== 'function') {
return resolve(WillRender);
}
return popped.call(this, active, last, act => {
// Render
if (typeof act === 'undefined' || act === true) {
if (guards.length > 0) {
return checkGuard(resolve, reject);
} else {
return resolve(WillRender);
}
}
if (act) {
if (guards.length > 0) {
return checkGuard(resolve, reject);
} else {
return resolve(act);
}
}
// Redirect
// if (typeof act === 'string' && act.charCodeAt(0) === SLASH) {
// reject();
// return writeUrl(act);
// }
// Restricted
return resolve(renderError(403));
});
};
return () => new Promise(checkGuard);
}
if (typeof WillRender === 'function') {
// Route is component
if (WillRender.isComponent && WillRender.isComponent())
return r(WillRender);
// Route is plain function
return WillRender;
}
// Route is plain text/object
return WillRender;
}
}
const before = routes.beforeEach;
const after = routes.afterEach;
const getError = (code, fallback) => {
let error = routes.errors && routes.errors[code];
if (error) {
return typeof error === 'function' ? error : () => error;
}
return fallback;
};
current = {
config: {
errors: {
404: getError(404, () => r('div', {}, 'Error 404: Not Found')),
403: getError(403, () => r('div', {}, 'Error 403: Forbidden')),
},
},
before,
after,
routes: extractChildren(routes.routes),
write: writeUrl,
Link,
Router,
};
// Initiates router component
headless('router', RouterHead);
// Initiates title component
headless('title', Title);
return current;
};