Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

page sliding animation with React Router v4 and react-transition-group v2

I'm using React Router v4 and react-transition-group v2 to test the page sliding animation.

const RouterMap = () => (
  <Router>
    <Route render={({ location }) =>
      <TransitionGroup>
        <CSSTransition key={location.pathname.split('/')[1]} timeout={500} classNames="pageSlider" mountOnEnter={true} unmountOnExit={true}>
          <Switch location={location}>
            <Route path="/" exact component={ Index } />
            <Route path="/comments" component={ Comments } />
            <Route path="/opinions" component={ Opinions } />
            <Route path="/games" component={ Games } />
          </Switch>
        </CSSTransition>
      </TransitionGroup>
    } />
  </Router>
)

And the CSS:

.pageSlider-enter {
  transform: translate3d(100%, 0, 0);
}

.pageSlider-enter.pageSlider-enter-active {
  transform: translate3d(0, 0, 0);
  transition: all 600ms;
}
.pageSlider-exit {
  transform: translate3d(0, 0, 0);
}

.pageSlider-exit.pageSlider-exit-active {
  transform: translate3d(-100%, 0, 0);
  transition: all 600ms;
}

The animation is as bellow:

enter image description here

As you see, the animation that index page slide to the detail page is all right(right to left). But when I click the Back icon, I hope index page comes out from left to right.

I know if I change the CSS as bellow, the page will come out from left to right:

.pageSlider-enter {
  transform: translate3d(-100%, 0, 0);
}
.pageSlider-exit.pageSlider-exit-active {
  transform: translate3d(100%, 0, 0);
  transition: all 600ms;
}

But how combine the two animations together? Generally speaking, whenever user clicks the back icon, the animation should be from left to right.


Update: 2017.08.31

Thanks for @MatijaG, using the path depth is really an awesome idea. I followed it and got a new problem.

function getPathDepth(location) {
  let pathArr = (location || {}).pathname.split('/');
  pathArr = pathArr.filter(n => n !== '');
  return pathArr.length;
}

<Route render={({ location }) =>
  <TransitionGroup>
    <CSSTransition
      key={location.pathname.split('/')[1]}
      timeout={500}
      classNames={ getPathDepth(location) - this.state.prevDepth > 0 ? 'pageSliderLeft' : 'pageSliderRight' }
      mountOnEnter={true}
      unmountOnExit={true}
    >
      <Switch location={location}>
        <Route path="/" exact component={ Index } />
        <Route path="/comments" component={ Comments } />
        <Route path="/opinions" component={ Opinions } />
        <Route path="/games/lol" component={ LOL } /> // add a new route
        <Route path="/games" component={ Games } />
      </Switch>
    </CSSTransition>
  </TransitionGroup>
} />

And updated CSS:

.pageSliderLeft-enter {
  transform: translate3d(100%, 0, 0);
}
.pageSliderLeft-enter.pageSliderLeft-enter-active {
  transform: translate3d(0, 0, 0);
  transition: all 600ms;
}
.pageSliderLeft-exit {
  transform: translate3d(0, 0, 0);
}
.pageSliderLeft-exit.pageSliderLeft-exit-active {
  transform: translate3d(100%, 0, 0);
  transition: all 600ms;
}

.pageSliderRight-enter {
  transform: translate3d(-100%, 0, 0);
}
.pageSliderRight-enter.pageSliderRight-enter-active {
  transform: translate3d(0, 0, 0);
  transition: all 600ms;
}
.pageSliderRight-exit {
  transform: translate3d(0, 0, 0);
}
.pageSliderRight-exit.pageSliderRight-exit-active {
  transform: translate3d(-100%, 0, 0);
  transition: all 600ms;
}

The animation:

enter image description here

From '/' to '/games' is ok, and from '/games' back to '/' is still ok(type 1: route A -> route B, only 2 routes). But if firstly from '/' to '/games', and then from '/games' to '/games/lol', the second phase lose the animation(type 2: route A -> route B -> route C, 3 or more routes). We also see that from '/games/lol' back to '/games' and then back to '/', the slide animation is not same as type 1.

Anyone has any idea about this problem?

like image 872
jason Avatar asked Aug 29 '17 06:08

jason


Video Answer


2 Answers

Well the main problem is that even in your question you did not specify how should the app know which direction to use.

One way you could do it is by using the path depth/length: if the path you are navigating to is "deeper" than the current path, transition right to left, but if its shallower transition from left right?

The other problem is that router only gives you the location you are going to, so you should probably save the previous location (or depth) so you can compare it to.

After that is just a matter of switching between css classnames, something along this lines:

import React from 'common/react'
import { withRouter, Route, Switch } from 'react-router'

function getPathDepth (location) {
    return (location || {} ).pathname.split('/').length
}
class RouterMap extends React.PureComponent {
    constructor (props, context) {
        super(props, context)
        this.state = {
            prevDepth: getPathDepth(props.location),
        }
    }

    componentWillReceiveProps () {
        this.setState({ prevDepth: getPathDepth(this.props.location) })
    }
    render () {

        return (
            <Router>
                <Route render={ ({ location }) =>
                    (<TransitionGroup>
                        <CSSTransition
                            key={ location.pathname.split('/')[1] }
                            timeout={ 500 }
                            classNames={ getPathDepth(location) - this.state.prevDepth ? 'pageSliderLeft' : 'pageSliderRight' } mountOnEnter={ true } unmountOnExit={ true }
                        >
                            <Switch location={ location }>
                                <Route path='/' exact component={ Index } />
                                <Route path='/comments' component={ Comments } />
                                <Route path='/opinions' component={ Opinions } />
                                <Route path='/games' component={ Games } />
                            </Switch>
                        </CSSTransition>
                    </TransitionGroup>)
                }
                />
            </Router>
        )
    }
}

export default withRouter(RouterMap)

Codepen example: https://codepen.io/matijag/pen/ayXpGr?editors=0110

like image 76
MatijaG Avatar answered Oct 03 '22 21:10

MatijaG


I'm bringing here simplified and "hooked" version of @Darbley answer which works reliable

App.js

import React, { useEffect, useState } from 'react'
import styled from 'styled-components/macro'
import { hot } from 'react-hot-loader/root'
import { setConfig } from 'react-hot-loader'
import { Switch, Route } from 'react-router-dom'
import { TransitionGroup, CSSTransition } from 'react-transition-group'

import { compose } from 'redux'
import { withRouter } from '@hoc'

import Home from '@components/Home/Home'
import About from './About/About'
import Contact from './Contact/Contact'

import Navbar from './Navbar/Navbar'

setConfig({
    reloadHooks: false
})

const AppContainer = styled.main``

const pages = [
    { path: '/', order: 1 },
    { path: '/about', order: 2 },
    { path: '/contact', order: 3 }
]

const App = ({ location }) => {
    const [pageDirection, setPageDirection] = useState()
    const [currentPath, setCurrentPath] = useState(location.pathname)
    const [currentPathOrder, setCurrentPathOrder] = useState(
        pages.filter(({ path }) => path === location.pathname)[0].order
    )
    const currentKey = location.pathname.split('/')[1] || '/'
    useEffect(() => {
        const newPath = location.pathname
        const newPathOrder = pages.filter(({ path }) => path === newPath)[0].order
        if (newPath !== currentPath) {
            const direction = currentPathOrder < newPathOrder ? 'left' : 'right'
            setCurrentPath(newPath)
            setCurrentPathOrder(newPathOrder)
            setPageDirection(direction)
        }
    })
    return (
        <AppContainer>
            <Navbar />
            <TransitionGroup className={`${pageDirection}`}>
                <CSSTransition key={currentKey} timeout={1000} classNames={'route'}>
                    <div className="route__container">
                        <Switch location={location}>
                            <Route path="/" exact component={Home} />
                            <Route path="/about" component={About} />
                            <Route path="/contact" component={Contact} />
                        </Switch>
                    </div>
                </CSSTransition>
            </TransitionGroup>
        </AppContainer>
    )
}

export default process.env.NODE_ENV === 'development'
    ? compose(withRouter)(hot(App))
    : compose(withRouter)(App)

index.scss

.route__container {
    width: 100%;
    height: 100vh;
    position: absolute;
    top: 0px;
    left: 0px;
    transition: 1s;
}

.left .route-enter {
    transform: translateX(100%);
}

.left .route-enter-active {
    transform: translateX(0%);
}

.left .route-exit {
    transform: translateX(-100%);
}

.left .route-exit-active {
    transform: translateX(-100%);
}

.right .route-enter {
    transform: translateX(-100%);
}

.right .route-enter-active {
    transform: translateX(0%);
}

.right .route-exit {
    transform: translateX(100%);
}

.right .route-exit-active {
    transform: translateX(100%);
}
like image 32
toxxiczny Avatar answered Oct 03 '22 22:10

toxxiczny