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

Add navigation confirmation/blocking API #258

Merged
merged 1 commit into from
Jan 30, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the only real update to the readme–the rest is just Prettier

// 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