DEV Community

andy
andy

Posted on

Building a pager component from scratch* -- part 2

If you missed the first part of this lesson, it can be found here: https://dev.to/ajsmth/building-a-pager-component-from-scratch-4nlh

In this part, we'll add on to the pager component we've already created by handling animations and gestures to page between child views

The first thing we'll add is spring animations when the activeIndex prop changes. In order to do so, let's bring in react-spring and import some of it's functions:

yarn add react-spring 

import {animated, useSpring} from 'react-spring'

Adding spring page transitions:

function Pager({ children, activeIndex, size }) {
  // the total offset of the container div -- based on activeIndex
  const translateX = `translateX(calc(${activeIndex * -100}%))`;

  // this will animate changes in activeIndex between pages:
  const animatedStyle = useSpring({ transform: translateX })

  return (
    <div ...>

      {/* Update to animated.div */}
      <animated.div
        style={{
          ...absoluteFill,
          // we will translate this container view to bring children into focus
          ...animatedStyle
        }}
      >
        {React.Children.map(children, (element, index) => (
          <PageView index={index} width={size}>
            {element}
          </PageView>
        ))}
      </animated.div>

    </div>
  );
}

Now we have a spring animation that transitions between page changes

Next, we'll want to add support for handling swipe gestures. Again, we'll need a helper library

yarn add react-use-gesture

import {useDrag} from 'react-use-gesture'

This will help us track the drag value on the container view:

function Pager({ children, activeIndex, size }) {
  // ...

  const [{ dx }, set] = useSpring(() => ({ dx: 0 }));

  const bind = useDrag(({ delta }) => {
    const [dx] = delta;
    set({ dx: dx });
  });

  const dragX = dx.interpolate(dx => `translateX(${dx}px)`);

  {/* Change this container to animated.div */}
  return (
    <animated.div
      {...bind()}
      style={{
        ...
        transform: dragX
      }}
    >
     {...}
    </animated.div>
  );
}

You'll notice that after releasing, the translation value needs to reset in order to recenter the view. To achieve this let's update the useDrag() callback we just wrote:

  const bind = useDrag(({ delta, last }) => {
    const [dx] = delta;
    set({ dx: dx });

    // last means they've released from dragging
    if (last) {
      set({ dx: 0 });
    }
  });

Now the view re-centers after release.

So far, so good. What we need to consider now is how far the user has dragged, and if it's beyond a certain threshold, let's update the activeIndex so the next / previous view becomes focused.

The first thing we'll want to do is determine the threshold for when we should change -- in our case I'll set it to an arbitrary value of +/- 100:

  const bind = useDrag(({ delta, last }) => {
    const [dx] = delta;
    set({ dx: dx });

    // last means they've released from dragging
    if (last) {
      if (dx > DX_THRESHOLD) {
        // transition to previous view
      }

      if (dx < -DX_THRESHOLD) {
        // transition to next view
      }

      set({ dx: 0 });
    }
  });

Now we can use a callback prop to update the activeIndex prop and properly focus the previous / next page:

// add an onChange prop:
function Pager({ ..., onChange }) {

  ...

  // callback to onChange prop with new active value:

  const bind = useDrag(({ delta, last }) => {
    const [dx] = delta;
    set({ dx: dx });

    // last means they've released from dragging
    if (last) {
      if (dx > DX_THRESHOLD) {
        // transition to previous view
        onChange(activeIndex - 1)
      }

      if (dx < -DX_THRESHOLD) {
        // transition to next view
        onChange(activeIndex + 1)
      }

      set({ dx: 0 });
    }
  });

  ...
}

The last thing that we can do is to remove the border around our container view (if you still have it in your styles) and add an overflow: hidden style, if you'd like to hide the unfocused views.

One last note -- in practice, we might want to compute the threshold as a percentage of the total width, or whatever value you think works best.

The source for this can be viewed here: https://codesandbox.io/s/intelligent-cache-5f366

We now have a serviceable pager component that handles gestures and animates page transitions

What we'll look at next is opening up the pager API to work as a controlled and an uncontrolled component, as well as a psuedo-virtualization for child views which might help with with your app's performance. We'll also take a look at some jank that occurs in our existing implementation

Top comments (0)