Skip to content
This repository has been archived by the owner on Dec 15, 2018. It is now read-only.

Commit

Permalink
Add navigation confirmation/blocking API
Browse files Browse the repository at this point in the history
  • Loading branch information
tptee committed Jan 30, 2018
1 parent e9c60dd commit 515ca12
Show file tree
Hide file tree
Showing 13 changed files with 327 additions and 195 deletions.
101 changes: 62 additions & 39 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@

The router follows three basic principles:

- The URL is just another member of the state tree.
- URL changes are just plain actions.
- Route matching should be simple and extendable.
* The URL is just another member of the state tree.
* URL changes are just plain actions.
* Route matching should be simple and extendable.

While the core router does not depend on any view library, it provides flexible React bindings and components.

Expand Down Expand Up @@ -73,16 +73,12 @@ const routes = {
// Install the router into the store for a browser-only environment.
// routerForBrowser is a factory method that returns a store
// enhancer and a middleware.
const {
reducer,
middleware,
enhancer
} = routerForBrowser({
const { reducer, middleware, enhancer } = routerForBrowser({
// The configured routes. Required.
routes,
// The basename for all routes. Optional.
basename: '/example'
})
});

const clientOnlyStore = createStore(
combineReducers({ router: reducer, yourReducer }),
Expand Down Expand Up @@ -111,7 +107,7 @@ if (initialLocation) {
import { push, replace, go, goBack, goForward } from 'redux-little-router';

// `push` and `replace`
//
//
// Equivalent to pushState and replaceState in the History API.
// If you installed the router with a basename, `push` and `replace`
// know to automatically prepend paths with it. Both action creators
Expand All @@ -132,14 +128,17 @@ replace({
// Optional second argument accepts a `persistQuery` field. When true,
// reuse the query object from the previous location instead of replacing
// or emptying it.
push({
pathname: '/messages',
query: {
filter: 'business'
push(
{
pathname: '/messages',
query: {
filter: 'business'
}
},
{
persistQuery: true
}
}, {
persistQuery: true
});
);

// Navigates forward or backward a specified number of locations
go(3);
Expand All @@ -150,6 +149,19 @@ goBack();

// Equivalent to the browser forward button
goForward();

// Creates a function that blocks navigation with window.confirm when returning a string.
// You can customize how the prompt works by passing a `historyOptions` option with a
// `getUserConfirmation` function to `routerForBrowser`, `routerForExpress`, etc.
// See https://www.npmjs.com/package/history#blocking-transitions
block((location, action) => {
if (location.pathname === '/messages') {
return 'Are you sure you want to leave the messages view?';
}
});

// Removes the previous `block()`.
unblock();
```

Note: if you used the vanilla action types prior to `v13`, you'll need to migrate to using the public action creators.
Expand Down Expand Up @@ -227,8 +239,8 @@ Your custom reducers or selectors can derive a large portion of your app's state

`redux-little-router` provides the following to make React integration easier:

- A `<Fragment>` component that conditionally renders children based on current route and/or location conditions.
- A `<Link>` component that sends navigation actions to the middleware when tapped or clicked. `<Link>` respects default modifier key and right-click behavior. A sibling component, `<PersistentQueryLink>`, persists the existing query string on navigation.
* A `<Fragment>` component that conditionally renders children based on current route and/or location conditions.
* A `<Link>` component that sends navigation actions to the middleware when tapped or clicked. `<Link>` respects default modifier key and right-click behavior. A sibling component, `<PersistentQueryLink>`, persists the existing query string on navigation.

Instances of each component automatically `connect()` to the router state with `react-redux`.

Expand All @@ -246,7 +258,7 @@ Think of `<Fragment>` as the midpoint of a "flexibility continuum" that starts w
The simplest fragment is one that displays when a route is active:

```jsx
<Fragment forRoute='/home/messages/:team'>
<Fragment forRoute="/home/messages/:team">
<p>This is the team messages page!</p>
</Fragment>
```
Expand All @@ -266,13 +278,13 @@ To show a `Fragment` when no other `Fragment`s match a route, use `<Fragment for
`<Fragment>` lets you nest fragments to match your UI hierarchy to your route hierarchy, much like the `<Route>` component does in `react-router@v3`. Given a URL of `/about/bio/dat-boi`, and the following elements:

```jsx
<Fragment forRoute='/about'>
<Fragment forRoute="/about">
<div>
<h1>About</h1>
<Fragment forRoute='/bio'>
<Fragment forRoute="/bio">
<div>
<h2>Bios</h2>
<Fragment forRoute='/dat-boi'>
<Fragment forRoute="/dat-boi">
<div>
<h3>Dat Boi</h3>
<p>Something something whaddup</p>
Expand Down Expand Up @@ -302,12 +314,20 @@ To show a `Fragment` when no other `Fragment`s match a route, use `<Fragment for
`<Fragment>` makes basic component-per-page navigation easy:

```jsx
<Fragment forRoute='/'>
<Fragment forRoute="/">
<div>
<Fragment forRoute='/'><Home /></Fragment>
<Fragment forRoute='/about'><About /></Fragment>
<Fragment forRoute='/messages'><Messages /></Fragment>
<Fragment forRoute='/feed'><Feed /></Fragment>
<Fragment forRoute="/">
<Home />
</Fragment>
<Fragment forRoute="/about">
<About />
</Fragment>
<Fragment forRoute="/messages">
<Messages />
</Fragment>
<Fragment forRoute="/feed">
<Feed />
</Fragment>
</div>
</Fragment>
```
Expand All @@ -317,20 +337,23 @@ To show a `Fragment` when no other `Fragment`s match a route, use `<Fragment for
Using the `<Link>` component is simple:

```jsx
<Link className='anything' href='/yo'>
<Link className="anything" href="/yo">
Share Order
</Link>
```

Alternatively, you can pass in a location object to `href`. This is useful for passing query objects:

```jsx
<Link className='anything' href={{
pathname: '/home/messages/a-team?test=ing',
query: {
test: 'ing'
}
}}>
<Link
className="anything"
href={{
pathname: '/home/messages/a-team?test=ing',
query: {
test: 'ing'
}
}}
>
Share Order
</Link>
```
Expand All @@ -339,8 +362,8 @@ To change how `<Link>` renders when its `href` matches the current location (i.e

```jsx
<Link
href='/wat'
className='normal-link'
href="/wat"
className="normal-link"
activeProps={{ className: 'active-link' }}
>
Wat
Expand Down Expand Up @@ -379,5 +402,5 @@ We consider `redux-little-router` to be **stable**. Any API changes will be incr

## Community

- [react-redux-boiler](https://github.com/justrossthings/react-redux-boiler)
- [hoc-little-router](https://github.com/Trampss/hoc-little-router)
* [react-redux-boiler](https://github.com/justrossthings/react-redux-boiler)
* [hoc-little-router](https://github.com/Trampss/hoc-little-router)
20 changes: 19 additions & 1 deletion demo/client/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ import { render } from 'react-dom';

/* Invert comments for immutable */
import { createStore, combineReducers, compose, applyMiddleware } from 'redux';
import { routerForBrowser, initializeCurrentLocation } from '../../src';
import {
routerForBrowser,
initializeCurrentLocation,
block,
unblock
} from '../../src';
// import { createStore, compose, applyMiddleware } from 'redux';
// import { combineReducers } from 'redux-immutable';
// import { Map, fromJS } from 'immutable';
Expand All @@ -15,6 +20,8 @@ import routes from './routes';
import wrap from './wrap';
import Demo from './demo';

const UNBLOCK_DELAY = 10000;

/* Invert comments for immutable */
const { reducer, enhancer, middleware } = routerForBrowser({ routes });
const initialState = window.__INITIAL_STATE || {};
Expand All @@ -37,6 +44,17 @@ const store = createStore(
const initialLocation = store.getState().router;
// const initialLocation = store.getState().get('router').toJS();

store.dispatch(
// eslint-disable-next-line consistent-return
block(() => {
if (location.pathname.indexOf('cheese') !== -1) {
return `Are you sure you want to see other pages? It's really all downhill from here.`;
}
})
);

setTimeout(() => store.dispatch(unblock()), UNBLOCK_DELAY);

if (initialLocation) {
store.dispatch(initializeCurrentLocation(initialLocation));
}
Expand Down
16 changes: 10 additions & 6 deletions interfaces/history.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,19 @@ declare module 'history' {

declare type GetUserConfirmation = (
message: string,
callback: (continueTransition: bool) => void
callback: (continueTransition: boolean) => void
) => void;

declare type BrowserHistoryOptions = {|
basename?: string,
forceRefresh?: bool,
forceRefresh?: boolean,
keyLength?: number,
getUserConfirmation: GetUserConfirmation;
getUserConfirmation?: GetUserConfirmation
|};

declare function createBrowserHistory(options?: BrowserHistoryOptions): History;
declare function createBrowserHistory(
options?: BrowserHistoryOptions
): History;

declare type MemoryHistoryOptions = {|
initialEntries?: Array<string>,
Expand All @@ -62,7 +64,9 @@ declare module 'history' {
getUserConfirmation?: GetUserConfirmation
|};

declare function createMemoryHistory(options?: MemoryHistoryOptions): MemoryHistory;
declare function createMemoryHistory(
options?: MemoryHistoryOptions
): MemoryHistory;

declare type HashType = 'slash' | 'noslash' | 'hashbang';

Expand All @@ -81,7 +85,7 @@ declare module 'history' {
currentLocation?: Location
): Location;

declare function locationsAreEqual(a: Location, b: Location): bool;
declare function locationsAreEqual(a: Location, b: Location): boolean;

declare function parsePath(path: string): Location;
declare function createPath(location: Location): string;
Expand Down
10 changes: 10 additions & 0 deletions src/actions.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// @flow
import type { BlockCallback } from 'history';
import type { Location, LocationOptions, Href } from './types';

import {
Expand All @@ -7,6 +8,8 @@ import {
GO,
GO_BACK,
GO_FORWARD,
BLOCK,
UNBLOCK,
LOCATION_CHANGED,
REPLACE_ROUTES,
DID_REPLACE_ROUTES
Expand All @@ -33,6 +36,13 @@ export const go = (index: number) => ({
export const goBack = () => ({ type: GO_BACK });
export const goForward = () => ({ type: GO_FORWARD });

export const block = (historyShouldBlock: BlockCallback) => ({
type: BLOCK,
payload: historyShouldBlock
});

export const unblock = () => ({ type: UNBLOCK });

export const locationDidChange = (location: Location) => ({
type: LOCATION_CHANGED,
payload: location
Expand Down
46 changes: 24 additions & 22 deletions src/environment/browser-router.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// @flow
import type { History } from 'history';
import type { History, BrowserHistoryOptions } from 'history';

import createBrowserHistory from 'history/createBrowserHistory';

Expand All @@ -9,34 +9,36 @@ import install from '../install';
type BrowserRouterArgs = {
routes: Object,
basename: string,
historyOptions: BrowserHistoryOptions,
history: History
};

export const createBrowserRouter = (installer: Function) =>
({
routes,
basename,
history = createBrowserHistory({ basename })
}: BrowserRouterArgs) => {
const {
pathname: fullPathname,
search,
hash,
state: { key, state } = {}
} = history.location;

// Strip the basename from the initial pathname
const pathname = fullPathname.indexOf(basename) === 0
export const createBrowserRouter = (installer: Function) => ({
routes,
basename,
historyOptions = {},
history = createBrowserHistory({ basename, ...historyOptions })
}: BrowserRouterArgs) => {
const {
pathname: fullPathname,
search,
hash,
state: { key, state } = {}
} = history.location;

// Strip the basename from the initial pathname
const pathname =
fullPathname.indexOf(basename) === 0
? fullPathname.slice(basename.length)
: fullPathname;

const descriptor = basename
? { pathname, basename, search, hash, key, state }
: { pathname, search, hash, key, state };
const descriptor = basename
? { pathname, basename, search, hash, key, state }
: { pathname, search, hash, key, state };

const location = normalizeHref(descriptor);
const location = normalizeHref(descriptor);

return installer({ routes, history, location });
};
return installer({ routes, history, location });
};

export default createBrowserRouter(install);
Loading

0 comments on commit 515ca12

Please sign in to comment.