Ext JS to React: Carousel

   JavaScript
Ext JS to React: Carousel

This is part of the Ext JS to React blog series. You can review the code from this article on the Ext JS to React Git repo.

The carousel component, popularized in Sencha Touch and now available in Ext JS in the modern toolkit, is similar to the tab panel in that views in a card layout are shown and hidden using a navigation bar. Unlike the tab panel, carousel navigation is simplified by dropping text and icons in favor of a simple nav element like a circle with “active” styling to show which child item in the card array is in view. In addition to interacting with the nav elements you can swipe to reveal neighboring cards in a carousel view.

Note: While not a requirement for React, this article’s code examples assume you’re starting with a starter app generated by create-react-app.

React Carousel Class

Let’s look at an example of the carousel view in React. We’ll start by defining a Carousel class. First, we’ll need to install the react-swipeable-views package:

npm install --save react-swipeable-views

The react-swipeable-views package enables the animated card-swapping action as you navigate between cards using the dot indicators as well as dragging / swiping between cards. Users of the React Material UI library may recognize its use from the “Swipeable example” in the tabs demo.

import React, { Component } from 'react';
import SwipeableViews from 'react-swipeable-views';
import './Carousel.css';

class Carousel extends Component {
  static defaultProps = {
    activecard: 0,
    className: '',
    position: 'bottom'
  }

  state = {
    activecard: this.props.activecard
  }

  render () {
    let { className } = this.props;
    className = className ? ` ${className}` : '';

    const { children, position } = this.props;
    const { activecard } = this.state;
    const xPositions = ['top', 'bottom'],
          axis = xPositions.includes(position) ? 'x' : 'y';

    return (
      <div
        {...this.props}
        className = {`carousel ${position}${className}`}
      >
        <div className={`nav-strip`}>
          {React.Children.map(children, (child, i) => {
            const isActive = (i === activecard) ? 'active' : '';

            return <div
              onClick={this.onNavClick.bind(this, i)}
              className={`nav ${isActive}`}
            >
              <span className="nav-dot"></span>
            </div>;
          })}
        </div>
        <SwipeableViews
          index={activecard}
          onChangeIndex={this.onNavClick.bind(this)}
          enableMouseEvents={true}
          axis={axis}
        >
          {React.Children.map(children, (child, i) => {
            let { className } = child.props;
            className = className ? ` ${className}` : '';

            const isActive  = (i === activecard) ? ' active' : '';
            const cardProps = {
              ...child.props,
              style     : {flex: 1},
              className : ` card${isActive}${className}`,
              cardindex : i,
              activecard
            };

            return React.cloneElement(child, cardProps);
          })}
        </SwipeableViews>
      </div>
    );
  }

  onNavClick (activecard) {
    this.setState({
      activecard
    });
  }
}

export default Carousel;


React Carousel Class Explained

Above the class definition, we’re importing react-swipeable-views which we’ll use to wrap the child card items in the render method (described below). The static defaultProps property sets the defaults for various props on the Carousel. The constructor method sets the initial state and activecard property and the onNavClick method handles the nav element click that sets the activecard property which then styles the active nav element and shows the associated child card. The render method:

  • Combines any className string passed in with those added by the class
  • Collects the activecard from the component state object to inform the nav elements / cards which is currently active / visible
  • In the return:
    • Create the wrapping Carousel element that will house the nav element container and card container
    • The nav element container is added and we iterate over the child nodes (cards) passed to the Carousel to create navigation elements. The active nav element is styled as active when the cardindex matches the activecard.
    • A react-swipeable-views instance, SwipeableViews, is added to enclose the child cards passed to the Carousel. SwipeableViews enables the swiping of cards into view in addition to interacting with the nav elements.
    • We loop over the child nodes, this time calling React.cloneElement in order to add a few props like className, cardindex, and activecard to the original nodes that were passed in. The cloneElement method allows us to effectively extend the child items by taking on additional props as needed. The cloned elements are returned in an array to be the child nodes of the SwipeableViews parent. The card whose cardindex matches the activeitem is shown while the other cards are hidden using CSS rules.

React Carousel CSS

The CSS used to render the Carousel view:

.carousel {
  position: relative;
}
 
.nav-strip {
  display: flex;
  justify-content: center;
  position: absolute;
  pointer-events: none;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 1;
}
.nav-strip + div,
.nav-strip + div .react-swipeable-view-container {
  height: 100%;
  width: 100%;
}
.carousel.top .nav-strip {
  bottom: auto;
}
.carousel.bottom .nav-strip {
  top: auto;
}
.carousel.left .nav-strip {
  right: auto;
}
.carousel.right .nav-strip {
  left: auto;
}
.carousel.top .nav-strip,
.carousel.bottom .nav-strip {
  flex-direction: row;
}
.carousel.left .nav-strip,
.carousel.right .nav-strip {
  flex-direction: column;
}
 
.react-swipeable-view-container > div {
  flex-basis: 100%;
  background: #f7f7f7;
}
 
.nav {
  text-align: center;
  cursor: pointer;
  pointer-events: all;
}
 
.carousel.top .nav,
.carousel.bottom .nav {
  padding: 12px 6px;
}
.carousel.left .nav,
.carousel.right .nav {
  padding: 6px 12px;
}
 
.nav-strip .nav-dot {
  background-color: #d2d2d2;
  border-radius: 50%;
  height: 12px;
  width: 12px;
  display: inline-block;
}
.nav-strip .nav:hover .nav-dot {
  background-color: #b5b5b5;
}
.nav-strip .nav.active .nav-dot {
  background-color: #1e8bfb;
}
 
.carousel .card {
  padding: 12px;
}
React Carousel Example
We can create a Carousel instance like:

import React, { Component } from 'react';
import Carousel from './Carousel';

class App extends Component {
  render() {
    return (
      <Carousel style={{ height: '400px', width: '600px' }}>
        <div>Content for the first panel</div>
        <div>... and the second panel</div>
      </Carousel>
    );
  }
}

export default App;


Ext JS to React: Carousel, Panel



We pass in the style prop to give the rendered component explicit dimensions. We can pass a position prop to position the navigation indicators on the “top” or “bottom”. An activecard prop can also be passed to designate the initially active card view.

Conclusion

Hopefully the example demonstrates how easy it will be to get a carousel view built for your React applications. The example is relatively basic, but gets the job done for the most common use cases. It would be fairly easy to enhance the example and allow the carousel instance to stipulate whether the carousel is oriented horizontally as shown, or vertical. However, if you’re looking for a pre-built slider, look no further than react-slick for a very performant and highly configurable carousel view.


Like What You See?

Got any questions?