Skip to content

Instantly share code, notes, and snippets.

@koistya
Last active August 29, 2015 14:20
Show Gist options
  • Save koistya/8b1a7cc7fa311d30acba to your computer and use it in GitHub Desktop.
Save koistya/8b1a7cc7fa311d30acba to your computer and use it in GitHub Desktop.
import React from 'react';
import R from 'ramda';
import {Actions} from 'flummox';
import matchRoute from '../util/matchRoute';
import {log, levels as logLevels} from '../util/log';
import jsonApiRequest from '../di/jsonApiRequest';
const logger = log('RouterActions');
const debugFn = logger(logLevels.debug);
const logFn = logger(logLevels.log);
const routes = [
['/shop/items/:id', 'ShopItemPage']
];
function createRouteHandler(Handler, props) {
let routerHandler;
if (Handler.routerHandler) {
routerHandler = () => {
return Handler.routerHandler(props);
};
}
return class RouteHandler extends React.Component {
static routerHandler = routerHandler;
render() {
return <Handler {...this.props} {...props} />;
}
};
}
const notFoundFallback = R.curry(function notFoundFallback(getter, type, isArray, jsonApiResponse) {
if (type === 'Shop::Item' && !isArray) {
return createRouteHandler(getter('ShopItemPage'), {
id: jsonApiResponse.data.id
});
}
return getter('NotFoundPage');
});
const matchHandlerByController = R.curry(function matchHandlerByController(getter, controller, action, props) {
if (controller === 'items' && action === 'show') {
return createRouteHandler(getter('ShopItemPage'), {
id: props.id
});
}
return null;
});
const matchHandlerByUrl = R.curry(function matchHandlerByUrl(getter, url) {
const route = routes
.map(([pattern, handlerName]) => {
const props = matchRoute(pattern, url);
return props ? {handlerName, props} : null;
})
.filter(result => !!result)
.shift();
if (!route) {
return null;
}
return createRouteHandler(getter(route.handlerName), route.props);
});
export default class RouterActions extends Actions {
constructor(flux) {
super();
window.addEventListener('popstate', ({state}) => this.handlePopState({state}));
this.flux = flux;
this.getter = (key) => {
logFn(`Matched handler "${key}"`);
return flux.getStore('router').getPage(key);
};
this.notFoundFallback = notFoundFallback(this.getter);
this.matchHandlerByController = matchHandlerByController(this.getter);
this.matchHandlerByUrl = matchHandlerByUrl(this.getter);
}
fill(pages) {
return pages;
}
async initialize({controller, action, props}) {
const url = [document.location.pathname, document.location.search].join('');
let handler;
window.history.replaceState({url}, document.title);
if (controller && action) {
handler = this.matchHandlerByController(controller, action, props || {});
} else {
handler = this.matchHandlerByUrl(url);
}
if (!handler) {
handler = (await this.handleNotFound({url})).handler;
}
return {url, handler};
}
async navigateTo({url}) {
if (this.flux.getStore('router').getUrl() === url) {
return {url, handler: this.flux.getStore('router').getHandler()};
}
let loading = false;
let cached = false;
let handler;
if ((handler = this.flux.getStore('router').getHandlerForUrl(url))) {
cached = true;
} else {
handler = this.matchHandlerByUrl(url);
}
if (!handler) {
loading = true;
this.setLoading({loading: true});
handler = (await this.handleNotFound({url})).handler;
}
if (handler.routerHandler && !cached) {
if (!loading) {
loading = true;
this.setLoading({loading: true});
}
await handler.routerHandler();
}
window.history.pushState({url}, handler.windowTitle || document.title, url);
if (loading) {
this.setLoading({loading: false});
}
return {url, handler};
}
async handleNotFound({url}) {
try {
url += url.indexOf('?') >= 0 ? '&' : '?';
url += '_random=' + Math.random();
const jsonApiResponse = await jsonApiRequest(url, {
method: 'GET'
});
let handler;
if (jsonApiResponse && jsonApiResponse.data) {
const isArray = Array.isArray(jsonApiResponse.data);
const item = isArray ? jsonApiResponse.data[0] : jsonApiResponse.data;
if (item) {
handler = this.notFoundFallback(item.type, isArray, jsonApiResponse);
}
}
if (!handler) {
handler = this.getter('NotFoundPage');
}
return {jsonApiResponse, url, handler};
} catch (error) {
this.flux.getActions('error').handleServerError({error});
return {error, url};
}
}
async handlePopState({state}) {
if (!state || !state.url) {
window.location.reload();
return {};
}
const {url} = state;
let handler = this.flux.getStore('router').getHandlerForUrl(url) || this.matchHandlerByUrl(url);
if (!handler) {
this.setLoading({loading: true});
handler = (await this.handleNotFound({url})).handler;
this.setLoading({loading: false});
}
return {url, handler};
}
setLoading({loading}) {
return {loading};
}
}
import {Store} from 'flummox';
import Immutable from 'immutable';
import {log, levels as logLevels} from '../util/log';
const logger = log('RouterStore');
const logFn = logger(logLevels.log);
export default class RouterStore extends Store {
constructor(flux) {
super();
this.register(flux.getActionIds('router').fill, this.handleFill);
this.registerAsync(flux.getActionIds('router').initialize, null, this.handleHandlerChange);
this.registerAsync(flux.getActionIds('router').navigateTo, null, this.handleHandlerChange);
this.registerAsync(flux.getActionIds('router').handlePopState, null, this.handleHandlerChange);
this.register(flux.getActionIds('router').setLoading, this.handleSetLoading);
this.flux = flux;
this.state = {
pages: Immutable.Map(),
handlers: Immutable.Map(),
loading: false,
url: null,
handler: null
};
}
handleFill(pages) {
this.setState({pages: this.state.pages.merge(pages)});
}
handleSetLoading({loading}) {
this.setState({loading});
}
handleHandlerChange({url, handler}) {
this.waitFor(this.flux.getStore('jsonApi'));
logFn(`Cached handler for url "${url}"`);
this.setState({
url,
handler,
handlers: this.state.handlers.set(url, handler)
});
}
getPage(key) {
return this.state.pages.get(key);
}
getHandlerForUrl(url) {
return this.state.handlers.get(url);
}
isLoading() {
return this.state.loading;
}
getUrl() {
return this.state.url;
}
getHandler() {
return this.state.handler;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment