Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a gatsby-page-transition component #5213

Closed
jlengstorf opened this issue May 1, 2018 · 39 comments
Closed

Add a gatsby-page-transition component #5213

jlengstorf opened this issue May 1, 2018 · 39 comments
Labels
help wanted Issue with a clear description that the community can help with. stale? Issue that may be closed soon due to the original author not responding any more.

Comments

@jlengstorf
Copy link
Contributor

jlengstorf commented May 1, 2018

One of the benefits of removing layouts (see gatsbyjs/rfcs#2) is that we have a much simpler path to adding page transitions, which were a bit tricky and/or hacky in V1 (see discussion in #1854). This could also help solve #4919 and/or #3674.

For a new site I'm working on using Gatsby v2, I was able to get page transitions in place with the following code (thanks for the help @m-allanson and @KyleAMathews!):

import React from 'react';
import PropTypes from 'prop-types';
import { css } from 'react-emotion';
import { TransitionGroup, CSSTransition } from 'react-transition-group';
import Grid from './Grid';

const topMargin = css`
  margin-top: 3rem;
`;

const position = css`
  left: 50%;
  position: absolute;
  top: 278px;
  transform: translateX(-50%);
`;

const enter = css`
  opacity: 0.01;
`;

const enterActive = css`
  opacity: 1;
  transition: opacity 250ms ease-in;
`;

const exit = css`
  ${position} opacity: 1;
`;

const exitActive = css`
  ${position} opacity: 0.01;
  transition: opacity 250ms ease-out;
`;

class TransitionHandler extends React.Component {
  shouldComponentUpdate() {
    return this.props.location.pathname === window.location.pathname;
  }

  render() {
    return this.props.children;
  }
}

const Main = ({ children, location }) => (
  <TransitionGroup>
    <CSSTransition
      classNames={{ enter, enterActive, exit, exitActive }}
      timeout={{ enter: 200, exit: 200 }}
      key={location.pathname}
    >
      <TransitionHandler location={location}>
        <Grid element="main" className={topMargin}>
          {children}
        </Grid>
      </TransitionHandler>
    </CSSTransition>
  </TransitionGroup>
);

Main.propTypes = {
  children: PropTypes.oneOfType([
    PropTypes.element,
    PropTypes.arrayOf(PropTypes.element),
  ]).isRequired,
};

export default Main;

The result is a fade transition between pages:

transitions

Default use case: no config, transition everything

The transitions are entirely contained within this component, which means we can probably package it up and create gatsby-page-transitions with an API that starts out with a sane default:

import React from 'react';
import PageTransition from 'gatsby-page-transitions';

export default ({ children, location }) => (
  <PageTransition location={location}>
	{children}
  </PageTransition>
);

Only add transitions for part of the page

This could be dropped into the middle of a layout to only transition the content that changes (this is what the page I'm building does):

import React from 'react';
import PageTransition from 'gatsby-page-transitions';
import Header from './Header';
import Footer from './Footer';

export default ({ children, location }) => (
  <div>
    <Header />
    <PageTransition location={location}>
      {children}
    </PageTransition>
    <Footer />
  </div>
);

Simplified customization: presets

If we make the component extendable, it would be possible for contributors to create presets for the component to allow simple-but-coarse customization of page transitions:

import React from 'react';
import PageTransition from 'gatsby-page-transitions';

export default ({ children, location }) => (
  <PageTransition location={location} effect="zoom">
	{children}
  </PageTransition>
);

Advanced customization: expose all underlying APIs

And it could be configured with all the animation options for power users:

import React from 'react';
import { css } from 'react-emotion';
import PageTransition from 'gatsby-page-transitions';

const enter = css`
  /* custom styles here */
`;

const enterActive = css`
  /* custom styles here */
`;

const exit = css`
  /* custom styles here */
`;

const exitActive = css`
  /* custom styles here */
`;

export default ({ children, location }) => (
  <PageTransition
    location={location}
    timeout={{ enter: 100, exit: 100 }}
    classNames={{ enter, enterActive, exit, exitActive }}
  >
	{children}
  </PageTransition>
);

Effectively we could expose the entire API of the underlying tech if someone were inclined to customize all of it, but by default we make it a drop-in solution (similar to gatsby-image and gatsby-link).

This isn't a blocking issue for anything, but it would be nice to have, and could be another flashy effect to attract more attention to Gatsby the way Using Gatsby Image does.

@jlengstorf jlengstorf added help wanted Issue with a clear description that the community can help with. 🏷 type: feature labels May 1, 2018
@thebarty
Copy link

thebarty commented May 2, 2018

How about supporting INDIVIDUAL TRANSITION-STYLES PER PAGE?

For example having the home-page "zoom", while having the about-us page sliding in from left-to-right?

It would be really cool to have an easy solution for this

@KyleAMathews
Copy link
Contributor

Yeah, that's the idea with the presets — you'd able to pick a preset for each page component.

@jlengstorf
Copy link
Contributor Author

jlengstorf commented May 2, 2018

@thebarty That should be possible with the above API. It might look something like this:

export default ({ children, location }) => {
  const transition = location.pathname === '/' ? 'zoom' : 'slide-left';
  return (
    <PageTransition effect={transition}>
      {children}
    </PageTransition>
  );
};

@LekoArts
Copy link
Contributor

@jlengstorf I tried your answer for individual pages for my templates but I can't seem to get it working. Can you use location in the templates directly?

@jlengstorf
Copy link
Contributor Author

@LekoArts location is only passed to page templates (e.g. a component in the pages directory or the component passed to createPage as the component prop.

Give that a shot and let me know if you have any trouble. If you're still having issues, please pass along a link to see your code so we can figure out what's up.

Good luck!

@LekoArts
Copy link
Contributor

LekoArts commented May 28, 2018

or the component passed to createPage as the component prop

Then it should work 🤔 Because I want to use location in doc.jsx @jlengstorf
"Only add transitions for part of the page" would describe my usecase. <Content> is a container with the text of the markdown. And when I switch pages via the SideNav I want this text to fade-in/fade-out.

gatsby-node.js

exports.createPages = ({ graphql, boundActionCreators }) => {
  const { createPage } = boundActionCreators;

  return new Promise((resolve, reject) => {
    const docPage = path.resolve('src/templates/doc.jsx');
    resolve(
      graphql(`
        {
         // Content
        }
      `).then(result => {
        if (result.errors) {
          console.log(result.errors);
          reject(result.errors);
        }
        const docsList = result.data.docs.edges;
        docsList.forEach(project => {
          createPage({
            path: project.node.fields.slug,
            component: docPage,
            context: {
              slug: project.node.fields.slug,
            },
          });
        });
      })
    );
  });
};

doc.jsx

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import styled from 'react-emotion';
import { css } from 'emotion';
import { TransitionGroup, CSSTransition } from 'react-transition-group';
import SEO from '../components/SEO';
import Wrapper from '../components/Wrapper';
import Content from '../components/Content';
import SideNav from '../components/SideNav';

const enter = css`
  opacity: 0.01;
`;

const enterActive = css`
  opacity: 1;
  transition: opacity 250ms ease-in;
`;

const exit = css`
  opacity: 1;
`;

const exitActive = css`
  opacity: 0.01;
  transition: opacity 250ms ease-out;
`;

const OuterHelper = styled.div`
  position: relative;
`;

class TransitionHandler extends Component {
  shouldComponentUpdate() {
    return this.props.location.pathname === window.location.pathname;
  }

  render() {
    return this.props.children;
  }
}

const Doc = ({ pathContext, data, location }) => {
  const docContent = data.markdownRemark;
  return (
    <React.Fragment>
      <SEO docPath={pathContext.slug} docNode={docContent} docSEO />
      <Wrapper>
        <SideNav categories={data.allMarkdownRemark.edges} pathCont={pathContext} />
        <TransitionGroup component={OuterHelper}>
          <CSSTransition
            classNames={{ enter, enterActive, exit, exitActive }}
            timeout={{ enter: 250, exit: 250 }}
            key={location.pathname}
          >
            <TransitionHandler location={location}>
              <Content dangerouslySetInnerHTML={{ __html: docContent.html }} />
            </TransitionHandler>
          </CSSTransition>
        </TransitionGroup>
      </Wrapper>
    </React.Fragment>
  );
};

export default Doc;

@jlengstorf
Copy link
Contributor Author

@LekoArts What version of Gatsby are you using? I think this may have changed in V1 -> V2, now that I'm thinking about it. @pieh @m-allanson @KyleAMathews can you confirm if location is available to page components in V1?

If it's not available in V1, you'll need to pass it down from your layout component.

@pieh
Copy link
Contributor

pieh commented May 28, 2018

location is available for pages and layout components in v1. You could also experiment with withRouter HOC from react-router to get access to location (which is what I did looong time ago to produce page transition animation)

@LekoArts
Copy link
Contributor

@jlengstorf @pieh
I'm using V1. And after including {location.pathname} somewhere in doc.jsx and looking at React Devtools I can confirm that location is passed through and theoretically all information are there.
But the animations just don't work.

I was able to reproduce it with the gatsby starter, you can clone it here:
https://github.com/LeKoArts/gatsby-transition-error-example

I'm doing the same there:
https://github.com/LeKoArts/gatsby-transition-error-example/blob/master/src/templates/blog-post.js

I'm expecting when clicking the links at the top of the template page, that the content below fades in/out.

@jlengstorf
Copy link
Contributor Author

@LekoArts I think the problem might be a missing wrapper div. I had a similar issue and was able to fix it with an extra div:

https://github.com/jlengstorf/marisamorby.com/blob/master/src/components/shared/PageTransition.js#L57

That component works, so if it's not the wrapper, you can potentially just pull that component out as-is and implement it as shown in my layout here:

https://github.com/jlengstorf/marisamorby.com/blob/master/src/components/shared/Layout.js#L51

@LekoArts
Copy link
Contributor

@jlengstorf I'm already wrapping it with an additional div:
https://github.com/LeKoArts/gatsby-transition-error-example/blob/master/src/templates/blog-post.js#L20

I also copied your component, no change either.

Please make sure to test it in an template, not layout/page file :)

@LekoArts
Copy link
Contributor

LekoArts commented Jun 2, 2018

@jlengstorf I tried to fiddle around with it a bit more, but without luck :(
I also tried it with files in the pages directory, but it doesn't work either.

It only works for me if I wrap children in the layout file.

@jlengstorf
Copy link
Contributor Author

@LekoArts I'm looking at this, and for some reason the React component lifecycle methods just... aren't firing from the page templates.

I don't know why, but I was able to get it working by moving the transitions to the layout component. (This kind of mystery is one of the reasons we wanted to remove layouts in V2.)

I opened a PR with transitions working on your error repo: https://github.com/LeKoArts/gatsby-transition-error-example/pull/1

@LekoArts
Copy link
Contributor

LekoArts commented Jun 5, 2018

I don't know why, but I was able to get it working by moving the transitions to the layout component. (This kind of mystery is one of the reasons we wanted to remove layouts in V2.)

@jlengstorf Yeah, I know that. But for my usecase it's no option to transition the whole page. I have a fixed sidebar navigation and when I switch pages via this Nav I only want the text of content box (which is on the right side) to fade. It would just look too weird if the static/fixed sidebar would transition.

I'm looking at this, and for some reason the React component lifecycle methods just... aren't firing from the page templates.

Then that's the thing which has to be inspected I guess.

@jlengstorf
Copy link
Contributor Author

Yeah. This goes away in V2, so if you have a specialized use case it may be worth just upgrading. The migration guide is ready; if you wanted to give that a shot and open an issue with any feedback, it would be a huge help to us (making the migration guide better) and would hopefully help you clear up this transitions issue.

@LekoArts
Copy link
Contributor

LekoArts commented Jun 5, 2018

I'll upgrade the example repo and see if it works as I want it to behave.

@LekoArts
Copy link
Contributor

LekoArts commented Jun 7, 2018

@jlengstorf Upgraded the repo to v2 and now it's working as intended :)
https://github.com/LeKoArts/gatsby-transition-error-example

@LekoArts
Copy link
Contributor

@jlengstorf Ok, discovered a new issue:

After the start of the dev server the transition doesn't work when you visit the page for the first page. You can reproduce that with the example repo above:

Start with gatsby develop, click on one of the blog items and switch between pages with the three links above the blog heading. When you visit e.g. "Hello World", the transition doesn't work. If you switch back and forth they'll work.

@jlengstorf
Copy link
Contributor Author

@LekoArts Does this happen after gatsby build as well? develop is doing hot reloading, etc. and that can cause some issues with transitions.

@LekoArts
Copy link
Contributor

Yeah, you were right. After gatsby build and gatsby serve it's working without problems.
Then probably a hint about that behavior in develop would be helpful :)

@jlengstorf
Copy link
Contributor Author

@LekoArts that's something we want to fix — it shouldn't be doing that. From a conversation with @pieh and @KyleAMathews, it sounds like we're waiting for React Suspense to patch it, but it will be patched in the future.

@riencoertjens
Copy link
Contributor

Hi @jlengstorf
I've been banging my head at this for the past few hours but somehow I don't seem to get it working. I just tried to do this in a fresh install of the default starter and still no luck. Am I missing something?
I added the component mentioned above (https://github.com/jlengstorf/marisamorby.com/blob/master/src/components/shared/PageTransition.js)
added it to the layout component
added the extra packages to the package.json file and gatsby config, still nothing happens
here's the repo of the test site: https://github.com/riencoertjens/gatsby-page-transitions-test

@TylerBarnes
Copy link
Contributor

TylerBarnes commented Oct 2, 2018

I like where this is going but I think the proposed API needs some work.
Putting all the logic into a single transition component and then checking against the pathname as described above is going to get really out of control for sites with a lot of different animations going on.

export default ({ children, location }) => {
  const transition = location.pathname === '/' ? 'zoom' : 'slide-left';
  return (
    <PageTransition effect={transition}>
      {children}
    </PageTransition>
  );
};

I can imagine a massive switch/if else already and it's not pretty.

The other problem is that with the proposed api, we can't roll with our own animation library if we don't want to use the built in gatsby animation library. I'd like be be able to switch it out as needed if a project calls for something else.

Because of those two issues, being able to put the animation logic inside the page component or the template component seems key to me. That way each page can be a self contained component including it's own exit and entry animations.

I did quite a bit of messing around trying to figure out how to do this recently and I discovered a couple things. The animation component seems to need to be wrapped around the route component and can't be a child of the route component. If the animation is inside the route it will suddenly unmount when the route switches and the exit animation will be cut off. Maybe there's a way around that but I couldn't figure it out. To me that means the transition has to be in the layout component.
So, I found what worked was adding a transition component as a default prop of the page component. I then got my layout component to check the props of the page and wrap the specified transition component around the page before the route changes.

In my page component:

import TransitionElement from './transitions';

const PageName = () => (
    <div><h1>This is my page</h1></div>
);

PageName.defaultProps = {
  transitionComponent: TransitionElement
};

export default PageName;

In my layout component:

export const WrapPageComponent = ({ element, props }) => {
  const { key } = props.location;
  const { transitionComponent: TransitionComponent } = element.props;

  return (
      <PoseGroup>
        <TransitionComponent key={key} {...props}>
          {element}
        </TransitionComponent>
      </PoseGroup>
  );
};

Here's an example site and example repo

As you can see, this works great! Using default props to do this seems super hacky though. A built in api for passing information from the page to the layout would be really great. Besides that the only other thing I can see that's missing is the ability for a page to tell the next page how long to delay before starting it's animation but that seems like it could be done easily with redux. Or maybe the context api?

Is there a better way that already exists for me to have my page component tell my layout component which transition to wrap my page in? I'm thinking an api similar to how page graphql queries work would be awesome. If a page or template could just export a transition component that it should be wrapped in there wouldn't be need for much else since we already have a lot of great react animation libraries we can use.

Actually if a page could export an object containing a transition component and how long to delay the next route and the layout component could use that, that would be pretty flexible. If not I can just keep using default props for this but it feels dirty.

@jlengstorf
Copy link
Contributor Author

@riencoertjens I just sent over a PR with changes to your repo that will get transitions running. The short answer is that V2 changed the way page components were mounted and unmounted, so you need to use the wrapPageElement API to attach the transition handlers.

@TylerBarnes I think you make a good point about the API, but I don't have a good solution off the top of my head. Maybe @pieh knows of a better way to pass elements up the tree to the page wrapper?

@janczizikow
Copy link
Contributor

janczizikow commented Oct 5, 2018

Not sure if that's still a problem, but I remember having an issue where on each page transition router would jump to the top of the screen before transitioning the current page ( see this for more details: #7921 ). I think when adding a page transition it would be also necessary to prevent the scroll to top using shouldUpdateScroll and use setTimeout to manually update the scroll to the top. #7921 has some example code for that.

@ryanwiemer
Copy link
Contributor

ryanwiemer commented Oct 9, 2018

@janczizikow I still have the same issue with the page jumping to the top prior to the transition finishing. This is particularly apparent when using a fixed nav bar.

@jlengstorf or @pieh is the example code from @stefanprobst on #7921 still the best option to handle this via the gatsby-browser.js? Or is there a better way?

const transitionDelay = 250

exports.shouldUpdateScroll = ({ pathname }) => {
  // // We use a temporary hack here, see #7758
  if (window.__navigatingToLink) {
    window.setTimeout(() => window.scrollTo(0, 0), transitionDelay)
  } else {
    const savedPosition = JSON.parse(
      window.sessionStorage.getItem(`@@scroll|${pathname}`)
    )
    window.setTimeout(() => window.scrollTo(...savedPosition), transitionDelay)
  }
  return false
}

@stefanprobst
Copy link
Contributor

@ryanwiemer We can do a little better because now the saved scroll position and location.action are passed to shouldUpdateScroll.

@ryanwiemer
Copy link
Contributor

@stefanprobst gotcha. So what would the improved snippet look like? Is there an example documented anywhere?

@stefanprobst
Copy link
Contributor

The issue with page transition animations is that they only happen after the route has changed. This is why exit animations are harder than enter animations, and they work only because TransitionGroup or similar keep the "undead" old page component around long enough for the transition to finish.

So some manual work is necessary, but should be quite straightforward with shouldUpdateScroll, which gives us

  • access to the last saved scroll position with getSavedScrollPosition, This is useful when the user navigates with browser back/forward buttons.
  • how can we differentiate between a click on Link and a browser button click? We get 'PUSH' or 'POP' on routerProps.location.action.
  • Also location.key seems to be 'initial' on first load.

Please report back if the docs should be clearer on this.
The example from the docs adapted to this usecase would look something like this:

const transitionDelay = 250

exports.shouldUpdateScroll = ({
  routerProps: { location },
  getSavedScrollPosition,
}) => {
  if (location.action === 'PUSH') {
    window.setTimeout(() => window.scrollTo(0, 0), transitionDelay)
  } else {
    const savedPosition = getSavedScrollPosition(location)
    window.setTimeout(
      () => window.scrollTo(...(savedPosition || [0, 0])),
      transitionDelay
    )
  }
  return false
}

@ryanwiemer
Copy link
Contributor

@stefanprobst Thank you very much for your detailed explanation and example. I'm sure this will be helpful for others as well.

ryanwiemer added a commit to ryanwiemer/gatsby-using-page-transitions that referenced this issue Oct 10, 2018
@TylerBarnes
Copy link
Contributor

@jlengstorf I just revisited this to see what else I could come up with and I found gatsby-transformer-javascript-frontmatter which theoretically is exactly what I want.

The idea is I could pass time to delay unmounting the current route and time to delay mounting the next component as frontmatter and have that available as page context and then the animation could be handled within the page easily without the layout needing to contain any transitions.

For example, I'm trying to get the following to start the next pages animation after 300ms and have the current page delay unmounting for 600ms, allowing for an overlapping fade.

export const frontmatter = {
  transition: {
    delayMountingNext: 300,
    delayUnmountingCurrent: 600
  }
};

I'm a little stumped as to how to actually add this javascript frontmatter to my pages though.
I figure I need to use gatsby-node.js to modify pages but I've spent a couple hours now trying to figure out how to actually add the js frontmatter to pages. Does anyone know to create this relationship?

@jlengstorf
Copy link
Contributor Author

@TylerBarnes I just played with this a bit, and the instructions mention, but don't explain, that you need to have gatsby-source-filesystem installed and configured as well.

I've opened #9077 to track this. Please post any follow-up questions there to keep this issue focused on a single thread.

Thanks!

@rchrdnsh
Copy link

I am suuuuuper duper interested in this whole thing, as I am struggling mightily to get Gatsby to do what I want it to do for a baseline SPAPWA experience. +infinity that y'all awesome peeps are working this out :-)

@TylerBarnes
Copy link
Contributor

Over the weekend and the last couple nights I created a working page transitions plugin and I'm excited to share it!

I had the idea that it actually makes sense for the Link component to take props describing what the transition should look like, since the link is already in charge of transitioning (although usually quite abruptly). So I came up with the idea of gatsby-plugin-transition-link which gives this power to page links.

This idea allows for a lot of control and flexibility, and many different animations when needed.
Transitions can easily be described in a one-to-one relationship between specific pages instead of just an entry/exit animation per page/site. Alternatively the same transition could still be used in many places.

There's an example site with smooth / seamless transitions using gsap.

@jlengstorf this is the code I mentioned which uses the context api for page transitions.

Anyone interested can find a messy readme at the plugin repo with more details. It's really just a working proof of concept and I think the internals and the api need work.

I'm new to react/gatsby so if anyone finds this interesting, please send ideas or PR's!

@jlengstorf
Copy link
Contributor Author

@TylerBarnes this is awesome work. Would you be up for writing a blog post about this and showing off how to implement it? I think it would go over really well with our community!

@TylerBarnes
Copy link
Contributor

@jlengstorf I would absolutely love to! I'll clean up the plugin and example site and write up a post! Do you guys have any guidelines for blog posts or should I just write it and submit a PR?

@jlengstorf
Copy link
Contributor Author

@TylerBarnes Great news! We've got guidelines for contributing to the blog written up here: https://www.gatsbyjs.org/docs/how-to-contribute/#contributing-to-the-blog

Thanks so much!

@gatsbot
Copy link

gatsbot bot commented Jan 26, 2019

Old issues will be closed after 30 days of inactivity. This issue has been quiet for 20 days and is being marked as stale. Reply here or add the label "not stale" to keep this issue open!

@gatsbot gatsbot bot added the stale? Issue that may be closed soon due to the original author not responding any more. label Jan 26, 2019
@gatsbot
Copy link

gatsbot bot commented Feb 6, 2019

Hey again!

It’s been 30 days since anything happened on this issue, so our friendly neighborhood robot (that’s me!) is going to close it.

Please keep in mind that I’m only a robot, so if I’ve closed this issue in error, I’m HUMAN_EMOTION_SORRY. Please feel free to reopen this issue or create a new one if you need anything else.

Thanks again for being part of the Gatsby community!

@gatsbot gatsbot bot closed this as completed Feb 6, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
help wanted Issue with a clear description that the community can help with. stale? Issue that may be closed soon due to the original author not responding any more.
Projects
None yet
Development

No branches or pull requests