Skip to content

Instantly share code, notes, and snippets.

@steve-taylor
Last active August 29, 2017 02:36
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save steve-taylor/7adf8b1e5d9d8130f4aa81aff075d615 to your computer and use it in GitHub Desktop.
Save steve-taylor/7adf8b1e5d9d8130f4aa81aff075d615 to your computer and use it in GitHub Desktop.
Failed attempt at using Bacon.js and Bacon.js Router
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>bacon.js and baconjs-router POC</title>
<script src="https://unpkg.com/babel-standalone@6.15.0/babel.min.js"></script>
<script src="https://unpkg.com/react@latest/dist/react.js"></script>
<script src="https://unpkg.com/react-dom@latest/dist/react-dom.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bacon.js/0.7.95/Bacon.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.min.js"></script>
<script>
// Setup some globals for Bacon.js Router.
const process = {browser: true};
const bacon = Bacon;
const {tail, noop, isEqual} = _;
</script>
<!-- Bacon.js Router inside -->
<script>
let pauseUpdating = false;
let historyBus;
/**
* baconRouter from intial baseUrl and initialPath, updating browser URL location and states automagically.
*
* Routes:
* - PathMatching String or Regex,
* - Called function (return stream),
* - PathMatching String or Regex,
* - Called function (return stream),
*
* should look like
* [
* '',
* () => bacon.later(0, {pageType: '404'}),
*
* /(.+)\/supercoach/,
* (matchId) => bacon.later(0, {matchId, pageType: 'supercoach'}),
* ]
*
* @param {String} baseUrl Base Path (to be ignored from URL.location)
* @param {String} initialPath Starting Path (should match one of your routes)
* @param {...Mixed} routesAndReturns (String|Regex, Function, n+) Route + Function to call on match
* @return {Observable} EventStream that returns your matched route stream per route.
*/
function baconRouter(baseUrl, initialPath, ...routesAndReturns) {
// @TODO BaseUrl, Initial Path, And RoutesAndReturns should be objects.
// Required next is '404' or missed routes perhaps. Currently the returned stream
// is just 'nothing', means we won't hit anything.
let hasReplacedState = false;
const historyBus = getBaconRouterHistoryBus();
const history = bacon.update(
{
location: baseUrl + '/' + initialPath,
state: null,
title: null
},
[historyBus], ((previous, newHistory) => newHistory)
).doAction(({state, title, location}) => {
if (pauseUpdating || !process || !process.browser) {
return;
}
const thisHistory = { // For first render, history will have no values so take from the window
state,
title: title || window.document.title,
location: location || window.location.href
};
window.document.title = thisHistory.title;
if (hasReplacedState) {
window.history.pushState(thisHistory, title, location);
} else {
window.history.replaceState(thisHistory, title);
hasReplacedState = true;
}
}).skipDuplicates(isEqual);
listenToPopState(historyBus);
return history.flatMapLatest((history) => {
let {location/*, state*/} = history; // eslint-disable-line spaced-comment
const currentRoute = location.replace(baseUrl, ''); // @TODO Less hacky.
let route, routeReturns;
let matches;
// Because the routes and functions are 'paired', loop in increments of 2, first section is a route
// where the second section is the function to call and return.
for (let i = 0; i < routesAndReturns.length; i = i + 2) {
route = routesAndReturns[i];
routeReturns = routesAndReturns[i + 1];
if (typeof routeReturns != 'function') {
throw `baconRouter: Unexpected input ${typeof routeReturns} at argument ${i}.
Format is <base>, <initialPath>, <route-match>, <route-response-function>, <route-match>...`;
}
if (typeof route == 'string') {
if (route === currentRoute) {
return routeReturns();
}
} else if (route instanceof RegExp) {
matches = route.exec(currentRoute);
if (matches) {
return routeReturns(...tail(matches)); // First item is the string that matched, not the capture groups.
}
} else {
throw 'baconRouter: Unknown route test method';
}
}
return bacon.never();
});
}
/**
* The bacon router history bus can be used to push locations into browser history
*
* @return {Observable} A bus which expects objects like {location, state, title}
*/
function getBaconRouterHistoryBus() {
if (process && process.browser) {
if (!historyBus) {
historyBus = new bacon.Bus();
}
return historyBus;
} else {
// Always recreate the history bus for node.
return new bacon.Bus();
}
}
function listenToPopState(historyBus) {
if (!process || !process.browser) {
return;
}
let originalOnPopState = window.onpopstate || noop;
let originalUnload = window.onbeforeunload || noop;
window.onpopstate = ((event) => {
const stateData = event.state || {};
pauseUpdating = true;
historyBus.push({
state: stateData.state,
title: stateData.title,
location: stateData.location
});
setTimeout(() => {
pauseUpdating = false;
});
originalOnPopState(event);
});
window.onbeforeunload = (() => {
pauseUpdating = true;
originalUnload(arguments);
});
}
</script>
</head>
<body>
<div id="react-app"></div>
<script type="text/babel" data-presets="es2015,stage-2,react">
const startRoutingBus = new Bacon.Bus();
const loginBus = new Bacon.Bus();
const logoutBus = new Bacon.Bus();
const LandingPage = () => (
<div>
Loading...
</div>
);
const HomePage = () => (
<div>
Home
</div>
);
const UserPage = ({username, givenName, familyName}) => (
<table>
<thead>
<tr>
<th>Name</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>Username</td>
<td>{username}</td>
</tr>
<tr>
<td>Given name</td>
<td>{givenName}</td>
</tr>
<tr>
<td>Family name</td>
<td>{familyName}</td>
</tr>
</tbody>
</table>
);
const SomePage = () => (
<div>
Some page
</div>
);
const NotFoundPage = () => (
<div>
Not found
</div>
);
const userProp = Bacon.update(
null,
[loginBus], (state, {username, givenName, familyName}) => {
console.log('userProp received user object from loginBus:', {username, givenName, familyName});
return ({
username,
givenName,
familyName
});
},
[logoutBus], () => null
).doLog('userProp');
logoutBus.onValue(() => {
getBaconRouterHistoryBus().push({
location: '/page',
title: 'Some page'
});
});
const appState = Bacon.combineTemplate({
user: userProp
// and so on...
}).doLog('appState');
const pageStream = Bacon
.later(0, <LandingPage/>)
.concat(startRoutingBus.flatMapLatest(() => baconRouter(
window.location.origin,
window.location.pathname,
'/', () => {
console.log('Route: /');
return Bacon.later(0, <HomePage/>);
},
'/user', () => {
console.log('Route: /user');
return appState.map('.user').map(user => <UserPage {...user}/>);
},
'/page', () => {
console.log('Route: /page');
return Bacon.later(0, <SomePage/>);
},
/\/(.+)/, (path) => {
console.log('Route (not found): ', path);
return Bacon.later(0, <NotFoundPage/>);
}
)))
.doAction(value => {
console.log('pageStream:', value.type.name);
});
const mountPoint = document.getElementById('react-app');
pageStream.onValue(page => {
console.log('Rendering page...');
ReactDOM.render(page, mountPoint);
});
setTimeout(() => {
// Start mapping routes to the page stream. (Needs to start before pushing any app state.)
console.log('Start mapping routes to pages in pageStream ...');
startRoutingBus.push();
setTimeout(() => {
// Push user info to the appState.user property.
console.log('Pushing user to loginBus => userProp => appState.user ...');
loginBus.push({
username: 'steve.taylor',
givenName: 'Steve',
familyName: 'Taylor'
});
setTimeout(() => {
// Go to the user page. (appState.user should resolve now.)
console.log('Go to the user page ...');
getBaconRouterHistoryBus().push({
location: '/user',
title: 'User'
});
});
});
}, 1000); // 1s delay to simulate loading current user from an endpoint
</script>
</body>
</html>
@steve-taylor
Copy link
Author

steve-taylor commented Aug 29, 2017

Expected sequence of events:

  • <LandingPage/> is immediately rendered.
  • After a simulated endpoint call
    • The router starts, so something is indirectly subscribed to user info being pushed to loginBus.
    • a {username, givenName, familyName} object is pushed to appState.user via loginBus.
  • The router is directed to navigate to /user.
  • The /user route handler maps the user object to the <User/> page.
  • The <User/> page is rendered.

Problem:

appState.user repeatedly resolves to null instead of the user object pushed to it via loginBus. The message "userProp received user object from loginBus:" is never logged, so userProp never receives the user info from loginBus.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment