maisano / react-router-transition

painless transitions built for react-router, powered by react-motion
http://maisano.github.io/react-router-transition/
MIT License
2.59k stars 138 forks source link

Transition without `position: absolute;` #3

Open arkist opened 8 years ago

arkist commented 8 years ago

In demos, <Lorem /> Component's position is set to absolute, So transition seems to work well. https://github.com/maisano/react-router-transition/blob/master/demos/index.html#L36

but If you can't set router's position to absolute, (like each router's height are different and it can be changed) transition wouldn't work well.

I've tried to set position: absolute on atLeave only, but it didn't work. Also, I couldn't figure out how to handle it using mapStyes.

Any ideas? :disappointed:

abelovic commented 8 years ago

I achieved this by applying the css to the inner div (created in renderRoute). To do this I added the css through mapStyles in my presets (see noTransition below):

const fadeTransitionConfig = { stiffness: 200, damping: 22 };

const popTransitionConfig = { stiffness: 360, damping: 25 };

const slideTransitionConfig = { stiffness: 330, damping: 30 };

noTransition = {

atEnter: {
    opacity: 1,
    scale: 1,
    offset: 0
},
atLeave: {
    opacity: spring(1, fadeTransitionConfig),
    scale: spring(1, popTransitionConfig),
    offset: spring(0, slideTransitionConfig)
},
atActive: {
    opacity: spring(1, fadeTransitionConfig),
    scale: spring(1, popTransitionConfig),
    offset: spring(0, slideTransitionConfig)
},
mapStyles(styles) {

    return {
        position: 'absolute',
        boxSizing: 'border-box',
        width: '100%',
        height: '100%',
        opacity: styles.opacity,
        transform: 'translateX(' + styles.offset + '%) scale(' + styles.scale + ')'
    }
}

};

fadeTransition = {

atEnter: Object.assign({}, noTransition.atEnter, { opacity: 0 }),

atLeave: Object.assign({}, noTransition.atLeave, { opacity: spring(0, fadeTransitionConfig) }),

atActive: Object.assign({}, noTransition.atLeave, { opacity: spring(1, fadeTransitionConfig) }),

mapStyles: noTransition.mapStyles

};

popTransition = {

atEnter: Object.assign({}, noTransition.atEnter, { scale: 0.8 }),

atLeave: Object.assign({}, noTransition.atLeave, { scale: spring(0.8, popTransitionConfig) }),

atActive: Object.assign({}, noTransition.atLeave, { scale: spring(1, popTransitionConfig) }),

mapStyles: noTransition.mapStyles

};

slideLeftTransition = {

atEnter: Object.assign({}, noTransition.atEnter, { offset: 100 }),

atLeave: Object.assign({}, noTransition.atLeave, { offset: spring(-100, slideTransitionConfig) }),

atActive: Object.assign({}, noTransition.atLeave, { offset: spring(0, slideTransitionConfig) }),

mapStyles: noTransition.mapStyles

};

slideRightTransition = {

atEnter: Object.assign({}, noTransition.atEnter, { offset: -100 }),

atLeave: Object.assign({}, noTransition.atLeave, { offset: spring(100, slideTransitionConfig) }),

atActive: Object.assign({}, noTransition.atLeave, { offset: spring(0, slideTransitionConfig) }),

mapStyles: noTransition.mapStyles

};

Implementation:

                    <RouteTransition pathname={this.props.location.pathname} {...this.state.transition}>
                        {
                            React.cloneElement(this.props.children, {setTransition: this.setTransition})
                        }
                    </RouteTransition>

From the child I pass the preset when I click on the react-router link for example, and set whatever transition state I want on RouteTransition

@maisano - I ended up doing the above (having a base i.e. 'noTransition') because

  1. My app base has a RouteTransition but I may not want to transition so basically that is the default. Avoids having to change the structure of the container
  2. More importantly, I found that if I only mapped the styles for the particular transition and I switched between "different ones" i.e. fade and slide it wouldn't transition correctly. I also avoided React complaining that I mutated the props because they are different ;) This fixed it and it seems to work well now

Would like to hear your thoughts on this approach i.e. performance considerations interpolating props that don't actually change. I suspect it shouldn't be an issue but not 100% sure.

Thanks for this wrapper - excellent work!

maisano commented 8 years ago

@arkist: hm, i haven't given this a ton of thought–to be honest, most of the times i have animated routes, they've been wrapped in such a way where height was not an issue (they made up the entire page or the overflow was scrollable, etc). can you give some more background into your layout/how you're using this?

@abelovic: i'm glad you're enjoying the project! thanks for the feedback. you're correct in your approach if you want to dynamically toggle between different transition settings–react-motion requires the same keys to interpolate, and introducing new ones/removing old ones won't work.

as per your question on performance, i don't see this being an issue. the api recently changed and i haven't looked at the internals since, though i recall TransitionMotion only needing to interpolate keys when their destination values change. in this example, nothing changes so you should be fine.

one small thing i did notice is that you're cloning children to pass in the setTransition callback–if you wanted to avoid this you could use state from history instead, pulling it out in your root component as needed, e.g. using a Link with a state prop (<Link to={path} state={{transition: 'slideLeft'}}>) where you reference that state in your handler via this.props.history.state.transition.

amorino commented 8 years ago

I think that the issue he's referring (it's just a guess) is this: https://gfycat.com/JadedUnrealisticBoto Without an absolute position, this will happen. (This is using the presets.pop)

maisano commented 8 years ago

@amorino i debated for a while whether to add absolute positions to the presets, or even include the presets at all. it probably makes more sense to add position: absolute to the mapStyles of each preset. it's easy enough to override and they're sort of broken without. feel free to pr or i can update this sometime over the weekend.

arkist commented 8 years ago

@amorino exactly. that's the what I encountered. @maisano I'm a newbie in motion/transitioning so don't know this issue is common or not. see below codes, please.

What I learn from @abelovic's code is set position: absolute; width: 100%; height: 100% and put all the Component inside of <RouteTransition /> works fine. thanks! and noTransition-default strategy looks good.

but I think it seems to be still a problem: how about nested Component's transition? (not about path)

// <Header/> and <Footer/> don't need transition (always there)
// so they're on outside of <RouteTransition />.
// in this case, you can't set `position: absolute` to route because of Header&Footer.
<Header /> 
<RouteTransition
  pathname={this.props.location.pathname}
  atEnter={{opacity: 0, position: 'relative'}}
  atLeave={{opacity: 0, position: 'absolute'}}
  atActive={{opacity: 1, position: 'relative'}}
  mapStyles={styles => {
    return {
      position: styles.position,  // so I wanna something like this but it is impossible! :(
      opacity: styles.opacity
    };
  }}
>
  {this.props.children}
</RouteTransition>
<Footer />
maisano commented 8 years ago

@arkist: ah, got it. so the issue with the code posted is that the objects that get passed to atEnter, atLeave and atActive get passed to react-motion for numeric interpolation–this won't work with strings.

i'm not currently sure the best way to handle dynamic, unbounded route heights, though i'm sure this is a common use case. i'll have to give it some more thought.

arkist commented 8 years ago

@maisano good :) I'll try to find a way, too. thanks for this project!

abelovic commented 8 years ago

@maisano - thanks for the tip that is a much cleaner way to do it :) The API of react router changed in 2.0 so if anyone is interested this is what I did:

<Link to={{ pathname: '/Home', state: {transition: slideRight} }} />

The mixin ContextRoute is deprecated so you will have to get the location object by passing context (its explained in the 2.0.0 change docs)

@arkist - I actually transition my header so the label and buttons etc... are encapsulated inside the react component (my view). If you do this one trick I did is to make the container color the same color as the header otherwise you will see the white background (or whatever color your container is) as the transition occurs.

However it sounds like you would rather have a fixed header where the content is the same for all views you transition to i.e. a common menu or application header

Could you not just do the following (untested)?

.header { height: 50px top: 0 position: absolute }

/* this is passed using mapStyles / .route-transition { height: calc(100% - 100px) / height of header and footer - since it is app specific probably ok to just add it to your presets? */ top: 50px position: absolute }

.footer { height: 50px bottom: 0 position: absolute }

abelovic commented 8 years ago

Sorry code snippet got removed

<Link to={{ pathname: '/Home', state: {transition: slideRight} }} />

arkist commented 8 years ago

@abelovic thanks for the awesome tip! :) browser support is a problem only. http://caniuse.com/#search=calc

abelovic commented 8 years ago

Well looks like you would mainly loose Opera mini (Blackberry I think) and IE 8 support but look at this:

https://facebook.github.io/react/blog/2016/01/12/discontinuing-ie8-support.html

For me this is not a problem and I use calc all the time. If you do need to support these browsers however you might be able to use something like:

https://github.com/souporserious/react-measure

and just do the calculations in react :)

JoeMattie commented 8 years ago

Just my $0.02:

Instead of percentage I am using:

height:calc(100vh - 50px);

in my mapStyles() function instead of percentage. This makes it so that you don't have to worry about the geometry of the parent component. (100vh is the height of the window in pixels, 50px is the height of my header)

haustraliaer commented 8 years ago

This may not be the best solution - but I managed to get @arkist's desired effect by swapping the non-numeric values when one of the animating properties reached a desired value inside the mapStyles function:

<RouteTransition
  pathname={this.props.location.pathname}
  atEnter={{ opacity: 0 }}
  atLeave={{ opacity: 0 }}
  atActive={{ opacity: 1 }}
  mapStyles={(styles) => {
    return {
      position: (styles.opacity === 1) ? undefined: 'absolute',
      width: (styles.opacity === 1) ? undefined : '100%',
      height: (styles.opacity === 1) ? undefined : '100%',
      opacity: styles.opacity,
    }
  }}>
    {this.props.children}
</RouteTransition>

Now I don't need to worry about my transitioning elements blowing out the layout, but also as soon as they're done animating in I can remove the style and they'll revert to their original css.

albermav commented 8 years ago

I have a similar solution, it just fades only the appearing div, and hides the older one:

<RouteTransition
  pathname={this.props.location.pathname}
  atEnter={{ opacity: 0 }}
  atLeave={{ opacity: 2 }}
  atActive={{ opacity: 1 }}
  mapStyles={styles => {
    if(styles.opacity > 1){
      return { display: 'none'}
    }
    return { opacity: styles.opacity}
  }}
>
  {this.props.children}
</RouteTransition>
akeelm commented 7 years ago

I had issues with this too, but even with some of the workarounds here, couldn't get it working properly.

So here's my amendment to one of the workarounds. Notice I do the switch to absolute positioning just before the div disappears.

<RouteTransition
    pathname={this.props.location.pathname}
    atEnter={{ opacity: 0 }}
    atLeave={{ opacity: 0 }}
    atActive={{ opacity: 1 }}
    mapStyles={(styles) => {
      return {
        position: (styles.opacity > 0.3) ? 'relative': 'absolute',
        boxSizing: 'border-box',
        width: '100%',
        height: '100%',
        opacity: styles.opacity,
        transform: 'translateX(' + styles.offset + '%) scale(' + styles.scale + ')'
      }
    }}
  >
jaybe78 commented 7 years ago

Hello,

@akeelm The fade effect works quite well with your code. However do you think we could achieve the same for slide effect without absolute positionning ?

JurJean commented 7 years ago

The solution from @akeelm is very nice, but I still had a problem with the footer of the page to toggle up and down. Using code below this is fixed.

                <RouteTransition
                    pathname={this.props.location.pathname}
                    atEnter={{ opacity: 0, foo: 0 }}
                    atLeave={{ opacity: 0, foo: 2 }}
                    atActive={{ opacity: 1, foo: 1 }}
                    mapStyles={(styles) => {
                    return {
                        position: (styles.foo <= 1) ? 'relative': 'absolute',
                        width: '100%',
                        height: '100%',
                        opacity: styles.opacity
                    }
                    }}>

I only still have the footer problem when I toggle routes really quick.

aakarim commented 7 years ago

I have modified @JurJean 's solution with a pop config. Works great. Same issue with fast route transitions, though (which is a shame, as one of the motivations for react-motion was to improve UX for cancelled transitions).

const popConfig = { stiffness: 360, damping: 25 };

export const pop = {
  atEnter: {
    transitionIndex: 0,
    scale: 0.8,
    opacity: 0,
  },
  atLeave: {
    scale: spring(0.8, popConfig),
    opacity: spring(0, popConfig),
    transitionIndex: 2,
  },
  atActive: {
    scale: spring(1, popConfig),
    opacity: 1,
    transitionIndex: 1,
  },
  mapStyles: styles => ({
    position: styles.transitionIndex <= 1 ? 'relative' : 'absolute',
    width: '100%',
    height: '100%',
    transform: `scale(${styles.scale})`,
    opacity: styles.opacity,
  }),
};
uxlayouts commented 7 years ago

It would be so helpful to have a codepen example for some of these. I've switched from easy-transition to react-motion to react-router-transition but transitionX fails for all of them. I know this can be fixed. Has someone tried setting the height of the children components\pages with JS or using a css property that does not require position:absolute?

markusv commented 7 years ago

I have tried @JurJean solution works great. Except for when switching route back before the first animation is done. Then you have both elements having position absolute, which means the container collapses until the animations are done. Any solution for this?

Xerios commented 7 years ago

For those looking for an answer to this problem, look no further! I have found a solution of making the transitions overlap without using position:absolute or floats.

How did I do it? Simple, use CSS Grid on the parent, then have both of the childs use the same grid-area. This will make both of the elements use the same area, allowing them to overlap while keeping the layout completely functional as before.

The only drawback is IE and it's partial support for CSS Grids, but if you live in the present like most of us then it shouldn't be a big of an issue. Otherwise, you can simply disable the transition for the unsupported browsers.

RomainLanz commented 7 years ago

Hey @Xerios 👋

Could you share some code with your solution?

On my project I'm using flexbox everywhere and I have some sort of "jumping" when I use @aakarim solution.

Xerios commented 7 years ago

Unfortunately, I didn't save my code because I had changed my mind about using transitions, but you should be able to do it by doing something like:

// Name of the container, forgot the name
.container {
  display: grid;
  grid-template-areas:"MyCustomAreaName";
}

// Forces all children under container to be in the same space/area
.container > div{
  grid-area: MyCustomAreaName;
}

Note that I don't remember the names of the classes, so I suggest you check them using the Inspector, the child class should be the one that has opacity and all the stuff, while the parent is the container.

Also if you want a wider browser support, you should do the same thing without using "area" feature. ( e.g. grid-template-columns and things like that )

markusv commented 7 years ago

good idea. I am now using grid if support. I check for grid with css feature detection and fallback to position absolute/relative if not supported. Thanks

sazedulhaque commented 6 years ago

Thanks @akeelm For the solution. I face slightly different type of problem my Carousel was not working and after applying your following code

mapStyles={(styles) => { return { position: (styles.opacity > 0.3) ? 'relative': 'absolute', boxSizing: 'border-box', width: '100%', height: '100%', opacity: styles.opacity, transform: 'translateX(' + styles.offset + '%) scale(' + styles.scale + ')' } }}

ueeieiie commented 6 years ago

here's a very simple example to get the basics

https://github.com/ueeieiie/simple-route-transitions