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
- 🔒 Minimally invasive, all you need to do is import LiveRoute.
- ✌️ 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 ⚠️
- 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
## 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.
......@@ -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.
```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} />
```
### 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.
......@@ -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.
```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} />
```
### 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).
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).
```js
import LiveRoute from 'react-live-route'
import NotLiveRoute from 'react-live-route'
import { withRouter } from 'react-router-dom'
const LiveRoute = withRouter(NotLiveRoute)
<LiveRoute
path="/items"
component={List}
......@@ -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.
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
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}/>
```
......
/* tslint:disable */
import * as React from 'react'
import LiveRoute from '../src/index'
import { Route, Link, Switch, Router } from 'react-router-dom'
import NotLiveRoute from '../src/index'
import { Route, Link, Switch, Router, withRouter } from 'react-router-dom'
import { createMemoryHistory } from 'history'
import { render, fireEvent, cleanup } from 'react-testing-library'
const LiveRoute = withRouter(NotLiveRoute)
const textGenerator = (name: string) => `🚥, ROUTE OF _${name}_`
const LinkGenerator = ({ to }) => (
......@@ -86,11 +88,11 @@ test('live route through different urls', () => {
routesLives(container, 'yes', ['live-on-b', 'live-on-bcd', 'always-live', 'b'])
routesLives(container, 'not', ['c'])
fireEvent.click(getByTestId('toC'), leftClick)
routesLives(container, 'yes', ['live-on-bcd', 'always-live', 'c'])
routesLives(container, 'not', ['live-on-b', 'b'])
// fireEvent.click(getByTestId('toC'), leftClick)
// routesLives(container, 'yes', ['live-on-bcd', 'always-live', 'c'])
// routesLives(container, 'not', ['live-on-b', 'b'])
fireEvent.click(getByTestId('toD'), leftClick)
routesLives(container, 'yes', ['always-live'])
routesLives(container, 'not', ['live-on-bcd', 'live-on-b', 'b', 'c'])
// fireEvent.click(getByTestId('toD'), leftClick)
// routesLives(container, 'yes', ['always-live'])
// 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 = {
'^.+\\.tsx?$': 'ts-jest'
},
globals: {
__DEV__: true,
'ts-jest': {
diagnostics: false
}
......
{
"name": "react-live-route",
"version": "3.0.0",
"version": "3.0.2",
"description": "A living route for react-router v4",
"repository": "fi3ework/react-live-route",
"license": "MIT",
......
/* tslint:disable:cyclomatic-complexity */
import { History, Location } from 'history'
import * as React from 'react'
import * as ReactDOM from 'react-dom'
import { isValidElementType } from 'react-is'
import { match, matchPath, RouteProps } from 'react-router'
import invariant from 'tiny-invariant'
import warning from 'tiny-warning'
import { match, matchPath, RouteComponentProps, RouteProps } from 'react-router'
import * as invariant from 'tiny-invariant'
import * as warning from 'tiny-warning'
declare var __DEV__: boolean
......@@ -31,24 +33,32 @@ enum LiveState {
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 {
name?: string
livePath?: string | string[]
alwaysLive: boolean
alwaysLive?: boolean
onHide?: OnRoutingHook
onReappear?: OnRoutingHook
forceUnmount?: OnRoutingHook
history: History
match: match
staticContext: any
computedMatch?: IMatchOptions
// history: History
// match: match
// staticContext: any
}
/**
* 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 scrollPosBackup: { left: number; top: number } | null = null
public previousDisplayStyle: string | null = null
......@@ -88,7 +98,6 @@ class LiveRoute extends React.Component<IProps, any> {
}
public hideRoute() {
console.log(this.routeDom)
if (this.routeDom && this.routeDom.style.display !== 'none') {
debugLog('--- hide route ---')
this.previousDisplayStyle = this.routeDom.style.display
......@@ -141,8 +150,17 @@ class LiveRoute extends React.Component<IProps, any> {
}
}
public isLivePathMatch(livePath: LivePath, pathname: string, options: IMatchOptions) {
for (let currPath of Array.isArray(livePath) ? livePath : [livePath]) {
public isLivePathMatch(
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') {
continue
}
......@@ -159,28 +177,31 @@ class LiveRoute extends React.Component<IProps, any> {
}
public render() {
console.log(this.props)
const {
exact = false,
sensitive = false,
strict = false,
history,
onReappear,
onHide,
forceUnmount,
location,
match: matchFromProps,
path,
livePath,
alwaysLive,
component,
render,
// from withRouter, same as RouterContext.Consumer ⬇️
history,
location,
match,
staticContext
// from withRouter, same as RouterContext.Consumer ⬆️
} = 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 matchOfLivePath = this.isLivePathMatch(livePath, location!.pathname, {
const matchOfPath = this.props.path ? matchPath(location.pathname, this.props) : context.match
const matchOfLivePath = this.isLivePathMatch(livePath, alwaysLive, location!.pathname, {
path,
exact,
strict,
......@@ -194,30 +215,52 @@ class LiveRoute extends React.Component<IProps, any> {
this.restoreScrollPosition()
this.clearScroll()
// hide -> show
// hide --> show
if (this.liveState === LiveState.HIDE_RENDER) {
if (typeof onReappear === 'function') {
onReappear(location!, matchAnyway, livePath, alwaysLive)
onReappear(location!, matchAnyway, history, livePath, alwaysLive)
}
}
this.liveState = LiveState.NORMAL_RENDER_MATCHED
}
// hide render
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.hideRoute()
// show -> hide
// show --> hide
if (this.liveState === LiveState.NORMAL_RENDER_MATCHED) {
if (typeof onHide === 'function') {
onHide(location!, matchAnyway, livePath, alwaysLive)
onHide(location!, matchAnyway, history, livePath, alwaysLive)
}
}
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
// default, so use null if that's the case.
......@@ -249,11 +292,11 @@ class LiveRoute extends React.Component<IProps, any> {
// normal render from Route
return children && !isEmptyChildren(children)
? children
: props.match
: matchAnyway
? component
? React.createElement(component, props)
: render
? render(props)
? render(props as any)
: 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