Skip to content

Commit

Permalink
Add ability to use navigationAPI synchronously (v1.1.0)
Browse files Browse the repository at this point in the history
- Add `navigation.pause()` method; alternative to `return null`
- Add `clearCache()` method for better clarity
- Update all api methods to call `clearCache()`
- Update Readme with changes
- Update example to utilize new features
  • Loading branch information
allpro committed Apr 22, 2019
1 parent 938d242 commit 3114750
Show file tree
Hide file tree
Showing 8 changed files with 110 additions and 76 deletions.
16 changes: 8 additions & 8 deletions .size-snapshot.json
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
{
"esm/index.js": {
"bundled": 154890,
"minified": 50880,
"gzipped": 14792,
"bundled": 154702,
"minified": 50857,
"gzipped": 14786,
"treeshaked": {
"rollup": {
"code": 27936,
"code": 27990,
"import_statements": 93
},
"webpack": {
"code": 30149
"code": 30201
}
}
},
"umd/@allpro/react-router-pause.min.js": {
"bundled": 29426,
"minified": 27934,
"gzipped": 9722
"bundled": 29480,
"minified": 28004,
"gzipped": 9731
}
}
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,10 @@ The `navigation` API passed to the handler has these methods:
<br>Returns the `location` object representing the paused navigation,
or `null` if no event is paused.

- **`navigation.pause()`**
<br>Pause navigation event - equivalent to returning `null` from the handler.
<br>**Note**: This must be called _before_ the handler returns.

- **`navigation.resume()`**
<br>Triggers the 'paused' navigation event to occur.

Expand All @@ -197,7 +201,8 @@ The `navigation` API passed to the handler has these methods:

#### `handler` Function Return Values

When called, the handler must return one of these 5 responses:
If the handler does NOT call any navigationAPI method is before it returns,
then it must return one of these responses:

- **`true`** or **`undefined`** - Allow navigation to continue.
- **`false`** - Cancel the navigation event, permanently.
Expand Down
8 changes: 4 additions & 4 deletions example/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions example/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@allpro/react-router-pause-example",
"version": "1.0.0",
"version": "1.1.0",
"description": "Helper for React Router to provide customizable asynchronous navigation blocking",
"repository": "allpro/react-router-pause/example",
"homepage": "https://allpro.github.io/react-router-pause",
Expand All @@ -20,7 +20,7 @@
},
"dependencies": {
"@allpro/form-manager": "^0.4.2",
"@allpro/react-router-pause": "^0.9.4",
"@allpro/react-router-pause": ">=1.0.0",
"@material-ui/core": "^3.9.0",
"@material-ui/icons": "^3.0.0",
"classnames": "^2.2.6",
Expand Down
34 changes: 18 additions & 16 deletions example/src/FormPage/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import Divider from '@material-ui/core/Divider'
import { useFormManager } from '@allpro/form-manager'

import ReactRouterPause from '@allpro/react-router-pause'
// import ReactRouterPause from '../ReactRouterPause' // Development

import FormDescription from './FormDescription'
import Bookmarks from './Bookmarks'
Expand Down Expand Up @@ -109,11 +110,11 @@ function FormPage(props) {

// Check if this is the Submit Link-button
if (location.state === 'submit') {
submitForm()
// If data OK, continue; else form is ALREADY displaying errors
// Return a promise to pause navigation until it settles
return submitForm()
.then(isValid => {
if (isValid) {
navigation.resume()
return true // Same as navigation.resume()
}
else {
setNotificationProps({
Expand All @@ -122,25 +123,26 @@ function FormPage(props) {
setNotificationProps({})
}
})
return false // Same as navigation.cancel()
}
})
}
else if (!form.isDirty()) {
// If form is not dirty, then ALLOW navigation

// If form is not dirty, then ALLOW navigation
if (!form.isDirty()) {
return true
}
else {
// If not the submit button, then prompt user with options
setDialogProps({
open: true,
cancel: closeThen(navigation.cancel),
resume: closeThen(navigation.resume),
redirect: closeThen(navigation.push, '/page4'),
onClose: closeThen(noop)
})
}

return null // PAUSE navigation while we wait for prompt response
// If not the submit button, then prompt user with options
setDialogProps({
open: true,
cancel: closeThen(navigation.cancel),
resume: closeThen(navigation.resume),
redirect: closeThen(navigation.push, '/page4'),
onClose: closeThen(noop)
})
return null // null = PAUSE navigation
// navigation.pause() - Can also use method to signal pause
}

// Handle bookmark scrolling
Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@allpro/react-router-pause",
"version": "1.0.0",
"version": "1.1.0",
"description": "Helper for React Router to provide customizable asynchronous navigation blocking",
"repository": {
"type": "git",
Expand All @@ -19,6 +19,9 @@
"esm",
"umd"
],
"publishConfig": {
"access": "public"
},
"engines": {
"node": ">=8",
"npm": ">=5"
Expand Down
98 changes: 61 additions & 37 deletions src/ReactRouterPause.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import PropTypes from 'prop-types'

import bindAll from 'lodash/bindAll'
import cloneDeep from 'lodash/cloneDeep'
import isBoolean from 'lodash/isBoolean'
import isFunction from 'lodash/isFunction'
import isNull from 'lodash/isNull'
import isUndefined from 'lodash/isUndefined'
Expand All @@ -13,8 +12,6 @@ import pick from 'lodash/pick'
import { isPromise, isSameFunction, fingerprint } from './utils'
import defaultConfig from './defaultConfig'

const Empty = () => null


/**
* @public
Expand All @@ -32,7 +29,7 @@ class ReactRouterPause extends React.Component {
this.ignoreNextNavigationEvent = false

// Cache the location data for navigation event that was delayed.
this.pausedNavigation = null
this.cachedNavigation = null

// Cache for unblock function returned by history.block
this.historyUnblock = null
Expand Down Expand Up @@ -147,28 +144,36 @@ class ReactRouterPause extends React.Component {
* @returns {(Object|null)}
*/
pausedLocation() {
const route = this.pausedNavigation
const route = this.cachedNavigation
/** @namespace route.location **/
return route ? cloneDeep(route.location) : null
}

/**
* Clear the cached location
*/
clearCache() {
this.cachedNavigation = null
}

/**
* Is there currently a location cached that we can 'resume'?
* @returns {boolean}
*/
isPaused() {
return !!this.pausedNavigation
return !!this.cachedNavigation
}

/**
* Resume previously pausedNavigation blocked by handler callback.
* Resume previously cachedNavigation blocked by handler callback.
*/
resume() {
if (!this.isPaused()) return

const { history } = this.props
let { location, action } = this.pausedNavigation
let { location, action } = this.cachedNavigation
action = action.toLowerCase()
this.clearCache()

// Avoid blocking the next event
this.allowNextEvent(true)
Expand All @@ -190,14 +195,15 @@ class ReactRouterPause extends React.Component {
* Clear cached navigation/location data so cannot be used
*/
cancel() {
this.pausedNavigation = null
this.clearCache()
}

/**
* @param {(string|Object)} pathOrLocation
* @param {Object} [state]
*/
push(pathOrLocation, state) {
this.clearCache()
this.allowNextEvent(true) // Avoid blocking this event
this.props.history.push(pathOrLocation, state)
}
Expand All @@ -207,6 +213,7 @@ class ReactRouterPause extends React.Component {
* @param {Object} [state]
*/
replace(pathOrLocation, state) {
this.clearCache()
this.allowNextEvent(true) // Avoid blocking this event
this.props.history.replace(pathOrLocation, state)
}
Expand All @@ -218,6 +225,12 @@ class ReactRouterPause extends React.Component {
* @returns {boolean}
*/
askHandler(location, action) {
let resp = true
let pauseCalled = false

// Cache route info so can resume route later
this.cachedNavigation = { location, action }

const navigationAPI = pick(this, [
'isPaused', // Returns true or false
'pausedLocation', // Returns location-object or null
Expand All @@ -226,45 +239,58 @@ class ReactRouterPause extends React.Component {
'push',
'replace'
])
let resp = true
// Add SYNCHRONOUS pause method to API
// Allows 'pause' to be set via an API call instead of returning null
navigationAPI.pause = () => {
pauseCalled = true
}

// Prevent a component-level error from breaking router navigation
try {
resp = this.handler(navigationAPI, location, action)
}
catch (err) {} // eslint-disable-line

// If nothing is returned, let navigation proceed as normal
if (isUndefined(resp)) {
return true
// If pausedLocation is empty, an api method must have been called
if (!this.pausedLocation) {
return false
}

// A boolean response means allow or cancel - NO paused navigation
if (isBoolean(resp)) {
return resp
// If navigation.pause() was called, THIS TAKES PRECEDENT
if (pauseCalled) {
resp = null
}

const isPromiseResp = isPromise(resp)

// A Promise OR Null response means pause/delay navigation
if (isPromiseResp || isNull(resp)) {
// Cache route info so can resume route later
this.pausedNavigation = { location, action }

// Promise will resume navigation if resolved; cancel if rejected
if (isPromiseResp) {
// noinspection JSUnresolvedFunction
resp
.then(val => {
if (val === false) this.cancel()
else this.resume()
})
.catch(this.cancel)
}
// A Null response means pause/delay navigation
if (isNull(resp)) {
return false
}

// A Promise response means pause/delay navigation
// Promise will resume navigation if resolved; cancel if rejected
if (isPromise(resp)) {
// noinspection JSUnresolvedFunction,JSObjectNullOrUndefined
resp
.then(val => {
if (val === false) this.cancel()
else this.resume()
})
.catch(this.cancel)

return false
}


// NOT PAUSED, so clear the cached location
this.clearCache()

if (resp === false) {
return false
}
if (resp === true || isUndefined(resp)) {
return true
}

// Log warning if an invalid response received, including undefined
console.error(
`Invalid response from ReactRouterPause.handler: \`${resp}\`. ` +
Expand Down Expand Up @@ -309,7 +335,7 @@ class ReactRouterPause extends React.Component {
const resp = !!this.askHandler(location, action)

// There are only 3 responses that block navigation
if (resp === false || resp === null || isPromise(resp)) {
if (resp === false || isNull(resp) || isPromise(resp)) {
return false
}
}
Expand All @@ -320,9 +346,7 @@ class ReactRouterPause extends React.Component {


render() {
// This component does not render anything, but...
// Some JSX is required here for React to see this is a 'component'
return <Empty />
return null
}
}

Expand Down
Loading

0 comments on commit 3114750

Please sign in to comment.