Skip to content

Commit

Permalink
Merge pull request #27 from 4Catalyzer/transition-hook
Browse files Browse the repository at this point in the history
Add transition hook support
  • Loading branch information
taion authored Nov 3, 2016
2 parents c258269 + 2b690f3 commit d791cd4
Show file tree
Hide file tree
Showing 10 changed files with 250 additions and 20 deletions.
1 change: 1 addition & 0 deletions examples/transition-hook/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Found Transition Hook Example
16 changes: 16 additions & 0 deletions examples/transition-hook/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "transition-hook",
"version": "0.1.0",
"private": true,
"devDependencies": {
"react-scripts": "0.7.0"
},
"dependencies": {
"found": "../..",
"react": "^15.3.2",
"react-dom": "^15.3.2"
},
"scripts": {
"start": "react-scripts start"
}
}
14 changes: 14 additions & 0 deletions examples/transition-hook/public/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Found Transition Hook Example</title>
</head>

<body>
<div id="root"></div>
</body>

</html>
176 changes: 176 additions & 0 deletions examples/transition-hook/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import createBrowserRouter from 'found/lib/createBrowserRouter';
import { routerShape } from 'found/lib/PropTypes';
import Link from 'found/lib/Link';
import React from 'react';
import ReactDOM from 'react-dom';

function LinkItem(props) {
// TODO: Remove the pragma once evcohen/eslint-plugin-jsx-a11y#81 ships.
return (
<li>
<Link // eslint-disable-line jsx-a11y/anchor-has-content
{...props}
activeStyle={{ fontWeight: 'bold' }}
/>
</li>
);
}

const appPropTypes = {
children: React.PropTypes.node,
};

function App({ children }) {
return (
<div>
<ul>
<LinkItem to="/" exact>
Main
</LinkItem>
<LinkItem to="/other">
Other
</LinkItem>
</ul>

{children}
</div>
);
}

App.propTypes = appPropTypes;

const mainPropTypes = {
router: routerShape.isRequired,
};

class Main extends React.Component {
constructor(props, context) {
super(props, context);

this.state = {
transitionType: 'confirm',
showCustomConfirm: false,
};

this.removeTransitionHook = props.router.addTransitionHook(
this.onTransition,
);

this.resolveCustomConfirm = null;
}

componentWillUnmount() {
this.removeTransitionHook();
}

onTransition = (location) => {
switch (this.state.transitionType) {
case 'confirm':
return 'Confirm';
case 'customConfirm':
// Handle the before unload case.
return location ? this.showCustomConfirm() : 'Confirm';
case 'allow':
return true;
case 'block':
return false;
case 'delayedConfirm':
// This won't prompt on before unload.
return new Promise((resolve) => {
setTimeout(resolve, 1000, 'Confirm');
});
case 'delayedAllow':
// This won't prompt on before unload.
return new Promise((resolve) => {
setTimeout(resolve, 1000, true);
});
default:
return null;
}
};

onChangeSelect = (event) => {
this.setState({ transitionType: event.target.value });
};

onClickYes = () => {
this.resolveCustomConfirm(true);
};

onClickNo = () => {
this.resolveCustomConfirm(false);
};

showCustomConfirm() {
this.setState({ showCustomConfirm: true });

return new Promise((resolve) => {
this.resolveCustomConfirm = (result) => {
this.setState({ showCustomConfirm: false });
this.resolveCustomConfirm = null;
resolve(result);
};
});
}

render() {
const { transitionType, showCustomConfirm } = this.state;

return (
<div>
<label htmlFor="transition-type">
Transition type
</label>
{' '}
<select
id="transition-type"
value={transitionType}
onChange={this.onChangeSelect}
>
<option value="confirm">Confirm</option>
<option value="customConfirm">Custom confirm</option>
<option value="allow">Allow</option>
<option value="block">Block</option>
<option value="delayedConfirm">Delayed confirm</option>
<option value="delayedAllow">Delayed allow</option>
</select>

{showCustomConfirm && (
<div>
Confirm
{' '}
<button onClick={this.onClickYes}>Yes</button>
<button onClick={this.onClickNo}>No</button>
</div>
)}
</div>
);
}
}

Main.propTypes = mainPropTypes;

const BrowserRouter = createBrowserRouter({
historyOptions: { useBeforeUnload: true },

routeConfig: [
{
path: '/',
Component: App,
children: [
{
Component: Main,
},
{
path: 'other',
Component: () => <div>Other</div>,
},
],
},
],
});

ReactDOM.render(
<BrowserRouter />,
document.getElementById('root'),
);
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"homepage": "https://github.com/4Catalyzer/found#readme",
"dependencies": {
"babel-runtime": "^6.18.0",
"farce": "^0.0.3",
"farce": "^0.0.4",
"invariant": "^2.2.1",
"is-promise": "^2.1.0",
"lodash": "^4.16.4",
Expand Down
2 changes: 2 additions & 0 deletions src/PropTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ export const routerShape = React.PropTypes.shape({
createHref: React.PropTypes.func.isRequired,
createLocation: React.PropTypes.func.isRequired,
isActive: React.PropTypes.func.isRequired,

addTransitionHook: React.PropTypes.func.isRequired,
});
39 changes: 23 additions & 16 deletions src/createBaseRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export default function createBaseRouter({ routeConfig, matcher, render }) {
onResolveMatch: React.PropTypes.func.isRequired,
createHref: React.PropTypes.func.isRequired,
createLocation: React.PropTypes.func.isRequired,
addTransitionHook: React.PropTypes.func.isRequired,
isActive: React.PropTypes.func.isRequired,
initialRenderArgs: React.PropTypes.object,
};
Expand All @@ -31,7 +32,16 @@ export default function createBaseRouter({ routeConfig, matcher, render }) {
constructor(props, context) {
super(props, context);

const { initialRenderArgs } = props;
const {
push,
replace,
go,
createHref,
createLocation,
isActive,
addTransitionHook,
initialRenderArgs,
} = props;

this.state = {
element: initialRenderArgs ? render(initialRenderArgs) : null,
Expand All @@ -41,30 +51,27 @@ export default function createBaseRouter({ routeConfig, matcher, render }) {

this.shouldResolveMatch = false;
this.pendingResolvedMatch = false;
}

getChildContext() {
const {
// By assumption, the methods on the router context should never change.
this.router = {
push,
replace,
go,
createHref,
createLocation,
isActive,
} = this.props;

return {
router: {
push,
replace,
go,
createHref,
createLocation,
isActive,
},
addTransitionHook,
};

this.childContext = {
router: this.router,
};
}

getChildContext() {
return this.childContext;
}

// We use componentDidMount and componentDidUpdate to resolve the match if
// needed because element resolution is asynchronous anyway, and this lets
// us not worry about setState not being available in the constructor, or
Expand Down Expand Up @@ -100,12 +107,12 @@ export default function createBaseRouter({ routeConfig, matcher, render }) {
async resolveMatch() {
const { match, matchContext, resolveElements } = this.props;

// TODO: Use Reselect for this?
const routes = getRoutes(routeConfig, match);
const augmentedMatch = {
...match,
routes,
matcher, // For e.g. Redirect to format pattern.
router: this.router, // Convenience access for route components.
context: matchContext,
};

Expand Down
6 changes: 5 additions & 1 deletion src/createBrowserRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ import createRender from './createRender';
import resolveElements from './resolveElements';

export default function createBrowserRouter({
basename, renderPending, renderReady, renderError, ...options
basename,
renderPending,
renderReady,
renderError,
...options
}) {
const FarceRouter = createFarceRouter({
...options,
Expand Down
4 changes: 4 additions & 0 deletions src/createConnectedRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ export default function createConnectedRouter({
createHref: farce.createHref,
createLocation: farce.createLocation,
isActive: found.isActive,

// There's not really a better way to model this. Functions can't live in
// the store, as they're not serializable.
addTransitionHook: farce.addTransitionHook,
};
}

Expand Down
10 changes: 8 additions & 2 deletions src/createFarceRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ import foundReducer from './foundReducer';
import Matcher from './Matcher';

export default function createFarceRouter({
historyProtocol, historyMiddlewares = [], routeConfig, ...options
historyProtocol,
historyMiddlewares,
historyOptions,
routeConfig,
...options
}) {
const matcher = new Matcher(routeConfig);

Expand All @@ -27,7 +31,9 @@ export default function createFarceRouter({
found: foundReducer,
}),
compose(
createHistoryEnhancer(historyProtocol, historyMiddlewares),
createHistoryEnhancer(
historyProtocol, historyMiddlewares, historyOptions,
),
createMatchEnhancer(matcher),
),
);
Expand Down

0 comments on commit d791cd4

Please sign in to comment.