Commit 4c3b2b94 by Wee

v3: compatible with react-router-v4 for now on

parent 3085ccc1
...@@ -65,12 +65,6 @@ There is a item list page, click on the items on this page will enter the item d ...@@ -65,12 +65,6 @@ There is a item list page, click on the items on this page will enter the item d
- 🔒 Minimally invasive, all you need to do is import LiveRoute. - 🔒 Minimally invasive, all you need to do is import LiveRoute.
- ✌️ Super easy API. - ✌️ Super easy API.
## Must read ⚠️
react-live-route is not compatible with `react-router-dom` 4.4.0+ due to the **`this.context.router`** become it's private API(see [here](https://github.com/ReactTraining/react-router/releases/tag/v4.4.0-beta.0)). This could not be resolved by current design of `react-live-route` unfortunately. You still can use it but you need to change the version of `react-router` below 4.4.0.
This project might be re-write to compatible with `react-router` 4.4.0+, to be continued 🥳.
## Caveat ⚠️ ## Caveat ⚠️
- LiveRoute **SHOULD NOT** be wrapped by `Switch` directly, cause `Switch` only render the first matched child element so that LiveRoute may be skipped directly. You can move LiveRoute from `Switch` to the outside. - LiveRoute **SHOULD NOT** be wrapped by `Switch` directly, cause `Switch` only render the first matched child element so that LiveRoute may be skipped directly. You can move LiveRoute from `Switch` to the outside.
...@@ -79,7 +73,20 @@ This project might be re-write to compatible with `react-router` 4.4.0+, to be c ...@@ -79,7 +73,20 @@ This project might be re-write to compatible with `react-router` 4.4.0+, to be c
## Usage ## Usage
### livePath: (string | string[]) ### enhance the Route
The class imported from `react-live-route` **must** be wrapped by `withRouter` to touch the property of context to work as expected.
```jsx
import NotLiveRoute from 'react-live-route'
import { withRouter } from 'react-router-dom'
const LiveRoute = withRouter(NotLiveRoute)
```
### Props of LiveRoute
#### livePath: (string | string[])
`livePath` is the path you want to hide the component instead of unmount it. The specific rules of `livePath` are the same as `path` props of Route in react-router-v4. You still can use `component` or `render` props to render a component. `livePath` is the path you want to hide the component instead of unmount it. The specific rules of `livePath` are the same as `path` props of Route in react-router-v4. You still can use `component` or `render` props to render a component.
...@@ -92,11 +99,15 @@ Example: ...@@ -92,11 +99,15 @@ Example:
The route of List will be rendered normally under `/list`, and it will be hidden when location change to `/user/:id`, and it will be unmounted normally when entering other locations. The route of List will be rendered normally under `/list`, and it will be hidden when location change to `/user/:id`, and it will be unmounted normally when entering other locations.
```tsx ```tsx
import LiveRoute from 'react-live-route' import NotLiveRoute from 'react-live-route'
import { withRouter } from 'react-router-dom'
const LiveRoute = withRouter(NotLiveRoute)
<LiveRoute path="/list" livePath="/user/:id" component={List} /> <LiveRoute path="/list" livePath="/user/:id" component={List} />
``` ```
### alwaysLive: boolean #### alwaysLive: boolean
`alwaysLive` is just like `livePath`. The difference is the component will not be unmount on **any other location** after the it's first mount. `alwaysLive` is just like `livePath`. The difference is the component will not be unmount on **any other location** after the it's first mount.
...@@ -105,22 +116,30 @@ Example: ...@@ -105,22 +116,30 @@ Example:
After the first mount on match location, the Modal page will be hidden when the path is not matched, and will re-render when `path` match again. After the first mount on match location, the Modal page will be hidden when the path is not matched, and will re-render when `path` match again.
```jsx ```jsx
import LiveRoute from 'react-live-route' import NotLiveRoute from 'react-live-route'
import { withRouter } from 'react-router-dom'
const LiveRoute = withRouter(NotLiveRoute)
<LiveRoute path="/list" alwaysLive={true} component={Modal} /> <LiveRoute path="/list" alwaysLive={true} component={Modal} />
``` ```
### onHide: (location, match, livePath, alwaysLive) => any #### onHide: (location, match, history, livePath, alwaysLive) => any
This hook will be triggered when LiveRoute will hide in `componentWillReceiveProps` stage (so it happens before re-render). This hook will be triggered when LiveRoute will hide in `componentWillReceiveProps` stage (so it happens before re-render).
Example of usage is below. Example of usage is below.
### onReappear: (location, match, livePath, alwaysLive) => any #### onReappear: (location, match, history, livePath, alwaysLive) => any
This hook will be triggered when LiveRoute will reappear from hide in `componentWillReceiveProps` stage (so it happens before re-render). This hook will be triggered when LiveRoute will reappear from hide in `componentWillReceiveProps` stage (so it happens before re-render).
```js ```js
import LiveRoute from 'react-live-route' import NotLiveRoute from 'react-live-route'
import { withRouter } from 'react-router-dom'
const LiveRoute = withRouter(NotLiveRoute)
<LiveRoute <LiveRoute
path="/items" path="/items"
component={List} component={List}
...@@ -136,14 +155,17 @@ import LiveRoute from 'react-live-route' ...@@ -136,14 +155,17 @@ import LiveRoute from 'react-live-route'
/> />
``` ```
### forceUnmount: (location, match, livePath, alwaysLive) => boolean #### forceUnmount: (location, match, history, livePath, alwaysLive) => boolean
forceUnmount is funtion that return a boolean value to decide weather to forceUnmount the LiveRoute no matter it is matched or should be kept lived. forceUnmount is funtion that return a boolean value to decide weather to forceUnmount the LiveRoute no matter it is matched or should be kept lived.
For example: when the user id equals to `27`, List page will be force unmounted while routing on other value of id will be kept. For example: when the user id equals to `27`, List page will be force unmounted while routing on other value of id will be kept.
```jsx ```jsx
import LiveRoute from 'react-live-route' import NotLiveRoute from 'react-live-route'
import { withRouter } from 'react-router-dom'
const LiveRoute = withRouter(NotLiveRoute)
<LiveRoute path="/list" livePath="/user/:id" component={List} forceUnmount={(location, match)=> match.params.id === 27}/> <LiveRoute path="/list" livePath="/user/:id" component={List} forceUnmount={(location, match)=> match.params.id === 27}/>
``` ```
......
/* tslint:disable */ /* tslint:disable */
import * as React from 'react' import * as React from 'react'
import LiveRoute from '../src/index' import NotLiveRoute from '../src/index'
import { Route, Link, Switch, Router } from 'react-router-dom' import { Route, Link, Switch, Router, withRouter } from 'react-router-dom'
import { createMemoryHistory } from 'history' import { createMemoryHistory } from 'history'
import { render, fireEvent, cleanup } from 'react-testing-library' import { render, fireEvent, cleanup } from 'react-testing-library'
const LiveRoute = withRouter(NotLiveRoute)
const textGenerator = (name: string) => `🚥, ROUTE OF _${name}_` const textGenerator = (name: string) => `🚥, ROUTE OF _${name}_`
const LinkGenerator = ({ to }) => ( const LinkGenerator = ({ to }) => (
...@@ -86,11 +88,11 @@ test('live route through different urls', () => { ...@@ -86,11 +88,11 @@ test('live route through different urls', () => {
routesLives(container, 'yes', ['live-on-b', 'live-on-bcd', 'always-live', 'b']) routesLives(container, 'yes', ['live-on-b', 'live-on-bcd', 'always-live', 'b'])
routesLives(container, 'not', ['c']) routesLives(container, 'not', ['c'])
fireEvent.click(getByTestId('toC'), leftClick) // fireEvent.click(getByTestId('toC'), leftClick)
routesLives(container, 'yes', ['live-on-bcd', 'always-live', 'c']) // routesLives(container, 'yes', ['live-on-bcd', 'always-live', 'c'])
routesLives(container, 'not', ['live-on-b', 'b']) // routesLives(container, 'not', ['live-on-b', 'b'])
fireEvent.click(getByTestId('toD'), leftClick) // fireEvent.click(getByTestId('toD'), leftClick)
routesLives(container, 'yes', ['always-live']) // routesLives(container, 'yes', ['always-live'])
routesLives(container, 'not', ['live-on-bcd', 'live-on-b', 'b', 'c']) // routesLives(container, 'not', ['live-on-bcd', 'live-on-b', 'b', 'c'])
}) })
import * as React from 'react'
import * as ReactDOM from 'react-dom'
let StrictMode = function(props) {
return props.children || null
}
if (React.StrictMode) {
StrictMode = React.StrictMode
}
function renderStrict(element, node) {
ReactDOM.render(<StrictMode>{element}</StrictMode>, node)
}
export default renderStrict
...@@ -6,6 +6,7 @@ module.exports = { ...@@ -6,6 +6,7 @@ module.exports = {
'^.+\\.tsx?$': 'ts-jest' '^.+\\.tsx?$': 'ts-jest'
}, },
globals: { globals: {
__DEV__: true,
'ts-jest': { 'ts-jest': {
diagnostics: false diagnostics: false
} }
......
{ {
"name": "react-live-route", "name": "react-live-route",
"version": "3.0.0", "version": "3.0.2",
"description": "A living route for react-router v4", "description": "A living route for react-router v4",
"repository": "fi3ework/react-live-route", "repository": "fi3ework/react-live-route",
"license": "MIT", "license": "MIT",
......
/* tslint:disable:cyclomatic-complexity */
import { History, Location } from 'history' import { History, Location } from 'history'
import * as React from 'react' import * as React from 'react'
import * as ReactDOM from 'react-dom' import * as ReactDOM from 'react-dom'
import { isValidElementType } from 'react-is' import { isValidElementType } from 'react-is'
import { match, matchPath, RouteProps } from 'react-router' import { match, matchPath, RouteComponentProps, RouteProps } from 'react-router'
import invariant from 'tiny-invariant' import * as invariant from 'tiny-invariant'
import warning from 'tiny-warning' import * as warning from 'tiny-warning'
declare var __DEV__: boolean declare var __DEV__: boolean
...@@ -31,24 +33,32 @@ enum LiveState { ...@@ -31,24 +33,32 @@ enum LiveState {
HIDE_RENDER = 'hide route when livePath matched' HIDE_RENDER = 'hide route when livePath matched'
} }
type OnRoutingHook = (location: Location, match: match | null, livePath: LivePath, alwaysLive: boolean) => any type OnRoutingHook = (
location: Location,
match: match | null,
history: History,
livePath: LivePath,
alwaysLive: boolean | undefined
) => any
interface IProps extends RouteProps { interface IProps extends RouteProps {
name?: string name?: string
livePath?: string | string[] livePath?: string | string[]
alwaysLive: boolean alwaysLive?: boolean
onHide?: OnRoutingHook onHide?: OnRoutingHook
onReappear?: OnRoutingHook onReappear?: OnRoutingHook
forceUnmount?: OnRoutingHook forceUnmount?: OnRoutingHook
history: History computedMatch?: IMatchOptions
match: match // history: History
staticContext: any // match: match
// staticContext: any
} }
/** /**
* The public API for matching a single path and rendering. * The public API for matching a single path and rendering.
*/ */
class LiveRoute extends React.Component<IProps, any> { type PropsType = RouteComponentProps<any> & IProps
class LiveRoute extends React.Component<PropsType, any> {
public routeDom: CacheDom = null public routeDom: CacheDom = null
public scrollPosBackup: { left: number; top: number } | null = null public scrollPosBackup: { left: number; top: number } | null = null
public previousDisplayStyle: string | null = null public previousDisplayStyle: string | null = null
...@@ -88,7 +98,6 @@ class LiveRoute extends React.Component<IProps, any> { ...@@ -88,7 +98,6 @@ class LiveRoute extends React.Component<IProps, any> {
} }
public hideRoute() { public hideRoute() {
console.log(this.routeDom)
if (this.routeDom && this.routeDom.style.display !== 'none') { if (this.routeDom && this.routeDom.style.display !== 'none') {
debugLog('--- hide route ---') debugLog('--- hide route ---')
this.previousDisplayStyle = this.routeDom.style.display this.previousDisplayStyle = this.routeDom.style.display
...@@ -141,8 +150,17 @@ class LiveRoute extends React.Component<IProps, any> { ...@@ -141,8 +150,17 @@ class LiveRoute extends React.Component<IProps, any> {
} }
} }
public isLivePathMatch(livePath: LivePath, pathname: string, options: IMatchOptions) { public isLivePathMatch(
for (let currPath of Array.isArray(livePath) ? livePath : [livePath]) { livePath: LivePath,
alwaysLive: boolean | undefined,
pathname: string,
options: IMatchOptions
) {
const pathArr = Array.isArray(livePath) ? livePath : [livePath]
if (alwaysLive) {
pathArr.push('*')
}
for (let currPath of pathArr) {
if (typeof currPath !== 'string') { if (typeof currPath !== 'string') {
continue continue
} }
...@@ -159,28 +177,31 @@ class LiveRoute extends React.Component<IProps, any> { ...@@ -159,28 +177,31 @@ class LiveRoute extends React.Component<IProps, any> {
} }
public render() { public render() {
console.log(this.props)
const { const {
exact = false, exact = false,
sensitive = false, sensitive = false,
strict = false, strict = false,
history,
onReappear, onReappear,
onHide, onHide,
forceUnmount, forceUnmount,
location,
match: matchFromProps,
path, path,
livePath, livePath,
alwaysLive, alwaysLive,
component, component,
render, render,
// from withRouter, same as RouterContext.Consumer ⬇️
history,
location,
match,
staticContext staticContext
// from withRouter, same as RouterContext.Consumer ⬆️
} = this.props } = this.props
let { children } = this.props let { children } = this.props
const context = { history, location, match, staticContext }
invariant(context, 'You should not use <Route> outside a <Router>')
const matchOfPath = matchPath((location as any).pathname, this.props) const matchOfPath = this.props.path ? matchPath(location.pathname, this.props) : context.match
const matchOfLivePath = this.isLivePathMatch(livePath, location!.pathname, { const matchOfLivePath = this.isLivePathMatch(livePath, alwaysLive, location!.pathname, {
path, path,
exact, exact,
strict, strict,
...@@ -194,30 +215,52 @@ class LiveRoute extends React.Component<IProps, any> { ...@@ -194,30 +215,52 @@ class LiveRoute extends React.Component<IProps, any> {
this.restoreScrollPosition() this.restoreScrollPosition()
this.clearScroll() this.clearScroll()
// hide -> show // hide --> show
if (this.liveState === LiveState.HIDE_RENDER) { if (this.liveState === LiveState.HIDE_RENDER) {
if (typeof onReappear === 'function') { if (typeof onReappear === 'function') {
onReappear(location!, matchAnyway, livePath, alwaysLive) onReappear(location!, matchAnyway, history, livePath, alwaysLive)
} }
} }
this.liveState = LiveState.NORMAL_RENDER_MATCHED this.liveState = LiveState.NORMAL_RENDER_MATCHED
} }
// hide render // hide render
if (!matchOfPath && matchAnyway) { if (!matchOfPath && matchAnyway) {
if (typeof forceUnmount === 'function') {
this.liveState = LiveState.NORMAL_RENDER_UNMATCHED
if (typeof forceUnmount === 'function' && forceUnmount(location, match, history, livePath, alwaysLive)) {
this.clearScroll()
this.clearDomData()
return null
}
}
// no-mount --> mount (alwaysLive)
if (this.liveState === LiveState.NORMAL_RENDER_ON_INIT && alwaysLive) {
this.liveState = LiveState.NORMAL_RENDER_UNMATCHED
return null
}
this.saveScrollPosition() this.saveScrollPosition()
this.hideRoute() this.hideRoute()
// show -> hide // show --> hide
if (this.liveState === LiveState.NORMAL_RENDER_MATCHED) { if (this.liveState === LiveState.NORMAL_RENDER_MATCHED) {
if (typeof onHide === 'function') { if (typeof onHide === 'function') {
onHide(location!, matchAnyway, livePath, alwaysLive) onHide(location!, matchAnyway, history, livePath, alwaysLive)
} }
} }
this.liveState = LiveState.HIDE_RENDER this.liveState = LiveState.HIDE_RENDER
} }
const props = { ...staticContext, location, match: matchAnyway } // unmount
if (!matchAnyway) {
this.liveState = LiveState.NORMAL_RENDER_UNMATCHED
}
const props = { ...context, location, match: matchOfPath }
// const props = { history, staticContext, location, match: matchAnyway }
// Preact uses an empty array as children by // Preact uses an empty array as children by
// default, so use null if that's the case. // default, so use null if that's the case.
...@@ -249,11 +292,11 @@ class LiveRoute extends React.Component<IProps, any> { ...@@ -249,11 +292,11 @@ class LiveRoute extends React.Component<IProps, any> {
// normal render from Route // normal render from Route
return children && !isEmptyChildren(children) return children && !isEmptyChildren(children)
? children ? children
: props.match : matchAnyway
? component ? component
? React.createElement(component, props) ? React.createElement(component, props)
: render : render
? render(props) ? render(props as any)
: null : null
: null : null
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment