Image Management – Build A Fullscreen Slider CMS With React & ES7

Image Management – CMS Slider SPA

This post demonstrates a Reactjs SPA Gallery using Contentful as a CMS.

Check the demo here.

Check the repo here.

Defining our requirements

For the purposes of this post imagine we are trying to build a project for a client. The client requires a single page application where he/she can put in content for a image/video slider.

This project must be:

  • A single page web app (SPA)
  • Performant
  • Using a CMS

This project must be a fluid, instantaneous image management slider. The client wants to avoid loading between slides.

This is achieved with relative ease using React.js & Contentful.

Contentful will act as an external CMS for the application. Its quite a flexible service with a decent free tier so it fits a clients content managing requirements quite well.

Setting up Contentful

In order to build a data structure for the images and text for the gallery. We need to create an account with Contentful.

Once created, you should see a welcome screen like so:

Contentful API Reactjs

From here we want to create our own space. Click the top righthand corner:

image management

Add a space:

Image Management

Select the free tier:

image management

Name your space:

Image Management

And we are now ready to create our data modal for the gallery slides.

Data Modal

When trying to visualise what data is needed  for an image management system we must look at the functionality of the project.

We know that each slide will need a:

  • Image
  • Video
  • Title

So our Contentful Modal should look like this:

image management

This is a consistent feature across most CMS systems. They allow the user to define a particular data entity and give it a defined type.

For example, a typical data entity could be a blog post. This could contain a title, description, banner image etc.

So we want to add this gallery entity by navigating to the Content section and clicking add :

Image Management

We then want to upload the image required for the entity:

Create Images CMS Reactjs

This image will represent one slide of the fullscreen gallery.

Creating our React Components

With a fresh installation of create-react-app we want define two components.

  • Home.js
  • Media.js

Our parent component will be Home.js, it will send all data to Slide.js via props. Slide.js will only contain information for one slide at a time. That means that all manipulation of state will be done in Home.js.

When creating the state of our component thinking about what is likely to change on screen helps to define what is needed.

So we might need:

  • Array of slides
  • Boolean to track if a video is playing
  • A media index to track which slide should be sent down to Media.js
 this.state = {
      playVideo: false,
      videoId: '',
      videoUrl: '',
      images: [],
      mediaIndex: 0
    };
  }

Retrieving the data

First we need to install the Contentful React package.

npm install contentful

Then, we need to import it into Home.js

import * as contentful from 'contentful'

Since Home.js, will manage the slide data in its state, we want to retrieve it when it mounts.

We must get our api key from Contentful first.

Navigate to settings – API keys:

image management

You will need both the Space ID and the access token here.

Image Management

Once we have both of these we can retrieve our data. From Home.js create the the lifecycle method componentWillMount()

  componentWillMount() {
    var client = contentful.createClient({
      space: 'Your Space ID',
      accessToken: 'You Access token' })
    
      client.getEntries().then(entries => {
        entries.items.forEach(entry => {
          if(entry.fields) {
            console.log(entry.fields);
            this.setState({images: entry.fields})
          }
        })
      })
  }

We are using the getEntries() method that Contentful package provides for us with our account info.

This returns a promise and in .then() we iterate through our entries and apply them to the local state of Home.js.

Great, now we have all the custom data in our component and can now interact with it.

Defining actions

We can easily define what actions the Slide  component will have by imagining how the user will interact with the gallery.

An image management app will have actions for:

  • Going to the next slide
  • Going to the previous slide
  • Playing the video (Later on in this tutorial)
  • Closing the slide (To go back to main screen)

Remember, the Slide component will never handle these actions itself but rather it will send callbacks up to Home.js. to make adjustment to its state

The Slide components props look like this:

 <Slide
    onClose={() => this.closeSlide()}
    onNext={() => this.nextSlide() }
    onPrev={() => this.prevSlide()}
    playNext={() => this.nextSlide()
/>

Each one of these actions will manipulate our slide data array.

nextVideo()

This is event occurs on right arrow click. We make sure that there is another slide available and increment the index if so.

nextVideo() {
    // If not last slide
   if(this.state.mediaIndex < this.state.images.length -1 ) {
     this.setState({mediaIndex: this.state.mediaIndex + 1})
    }
  // Otherwise we go back to home screen
   else {
    this.setState({ playSlide: false, mediaIndex: 0 });
  }
}

prevVideo()

This is event occurs on left arrows click. Check to see that we are not on first slide, else decrement our position in the slides array to show the previous item.

   
 if( this.state.mediaIndex > 0 ) {
    this.setState({mediaIndex: this.state.mediaIndex - 1})
  }
    // Otherwise we go back to home screen
    else {
      this.setState({ playVideo: false });
    }
    
  }

closeSlide()

This is invoked from an event by clicking the close button on a slide.

Reset slide index and stop showing the slide component.

 closeSlide() {
    this.setState({ playVideo: false, mediaIndex: 0 });
 }

Previous/Next

This sets us up nicely with a basic image slider. But we want to hide the previous and next arrows if no slides exists based on the index of the slides array in our state.

This can be done by adding noLast and noFirst props so that our Slides component can show/hide these accordingly.

<Slide
            onClose={() => this.closeVideo()}
            onNext={() => this.nextVideo() }
            onPrev={() => this.prevVideo()}
            playNext={() => this.nextVideo()}
            videoUrl={this.state.images[mediaIndex].video}
            imageUrl={this.state.images[mediaIndex].url}
            noLast={this.state.mediaIndex === this.state.images.length -1}
            noFirst={this.state.mediaIndex === 0}
          />

Its surprising just how much of the UI can be correctly manipulated by determining the current position an array in a component’s state.

By having these calculations take place in Home.js, the Slide  component can just rely on these explicit props to show/hide different parts of the UI.

Background Video

We have successfully created a image slider application with our CMS data.

Lets now create a video that plays over the background image.

First, create the play button:

import React from 'react';

const playButton = props => (
  <div onClick={() => props.onPlay()} class="play-button-outer">
    <div class="play-button" />
  </div>
);

export default playButton;

This a dumb stateless component, that we will import into Slide.js.

import PlayButton from './playButton';

Put in in our render method:

  <PlayButton onPlay={() => this.playVideo()} />

This will invoke a function to play the video:

  playVideo() {
    this.setState({ shouldPlay: true });
  }
As a result of this state change our background video will render:
{ this.state.shouldPlay ? (
          <video
            style={{ objectFit: 'cover', width: '100%',
            height: '100%'}}
            onPause={() => this.setState({paused: true})}
            onPlay={() => this.setState({paused: false})}
            controls  
            ref={element => (this.video = element)}
            key={vid} 
            autoPlay={true}
            onEnded={() => this.playNext()}
            id="background-video"
            autoPlay
          >
            <source src={vid} type="video/mp4" />
            Your browser does not support the video tag.
          </video>
       
        ) : null}

The video element has a considerable amount of events that you can call actions upon. Events like onPlay(), onPause(), onEnded() can be extremely useful when you want to change the state of your component to reflect the status video.

This video will play in the background with the following styles:

.video-wrapper {
    display: -ms-flexbox;
    display: flex;
    -ms-flex-align: center;
    align-items: center;
    -ms-flex-pack: center;
    justify-content: center;
    -ms-flex-direction: row;
    background-size:cover;
    background-repeat: no-repeat !important;
    flex-direction: row;
    min-height:800px;
    flex:1;
    position: absolute;
    right: 0;
    height: 100%;
    background-size: cover;
    left: 0;
    bottom: 0;
    top: 0;
    background-size: auto 100%;
}

You can check the full styling details in the repo.

Fullscreen Mode

Perhaps, one of the more tricky elements to this application is enabling fullscreen mode.

The client may want a fullscreen application to display the webapp on an iPad.

There are some considerations here, firstly, the JavaScript Full Screen API is only available to use upon user input.

This means that the user has to initiate an action before application can go fullscreen. This cannot be avoided due to security issues if a website is able to go full screen instantly.

So, when the user clicks the main How does it Work or Learn More buttons we will invoke the FullScreen API.

In Tile.js

<div className="options">
          <div className="play">
            <div onClick={() => this.props.onPlay( 0 )}>
             <img src={workButton} />
            </div>
        </div>
   </div>

The onClick() event will will go back up to Home.js and initiate the fullscreen code

 playSlide(index) {
    this.setState({ playVideo: true, mediaIndex:  index });
    var elem = document.body;

    if (elem.requestFullscreen) {
      elem.requestFullscreen();
    } else if (elem.mozRequestFullScreen) { /* Firefox */
      elem.mozRequestFullScreen();
    } else if (elem.webkitRequestFullscreen) { /* Chrome, Safari & Opera */
      document.body.webkitRequestFullscreen();
    } else if (elem.msRequestFullscreen) { /* IE/Edge */
      elem.msRequestFullscreen();
    }
}

This piece of functionality covers all browser cases and will initialise the full screen mode. This is ideal for a Gallery/Slider on an Ipad and in some cases on desktop too.

On Tablet it is important to remember to change the manifest.jsonfile for full screen capabilities. Specifically the property display need to be “fullscreen”.

{
  "short_name": "CMS Gallery",
  "name": "Create React App Sample",
  "icons": [
    {
      "src": "favicon.ico",
      "sizes": "64x64 32x32 24x24 16x16",
      "type": "image/x-icon"
    }
  ],
  "display": "fullscreen",
  "theme_color": "#000000",
  "background_color": "#ffffff"
}

Once, on the web app url using a Tablet, simple save the app to your home screen and launch it from there. This simulates opening a native app but in reality you are just opening up a the web apps URL in full screen.

Key take aways

  • Most our UI changes for our data can be based on the current index of our array coming from our Contentful CMS.
  • Most of our the actions of the gallery can take place in our parent component (Home.js), these changes to state will be drilled down to the Slide.js component via props.
  • We can specify exactly what happens when a slide starts, when slide finishes etc via React lifecycle hooks. (componentDidMount, componentWillMount, componentWillUnmount)
  • A client now has full control of the content of the application, he/she can add as many slides as desired. This show how reusable our Slide.js component is. And we can now reuse this component in any other project.

If your interested in an image management system for your website or webapp. Feel free to get in touch.

Gareth Dunne

Full Stack Developer and creator of JSdiaries. Passionate about the latest in web technologies and how it can provide value for my clients.