Last active
August 29, 2017 02:36
-
-
Save steve-taylor/7adf8b1e5d9d8130f4aa81aff075d615 to your computer and use it in GitHub Desktop.
Failed attempt at using Bacon.js and Bacon.js Router
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!doctype html> | |
<html> | |
<head> | |
<meta charset="utf-8"> | |
<title>bacon.js and baconjs-router POC</title> | |
<script src="https://unpkg.com/[email protected]/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> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Expected sequence of events:
<LandingPage/>
is immediately rendered.loginBus
.{username, givenName, familyName}
object is pushed toappState.user
vialoginBus
./user
./user
route handler maps the user object to the<User/>
page.<User/>
page is rendered.Problem:
appState.user
repeatedly resolves tonull
instead of the user object pushed to it vialoginBus
. The message "userProp received user object from loginBus:" is never logged, souserProp
never receives the user info fromloginBus
.