A Polymer Image Carousel

I’m still working on my home page. What I want to do is create a Web Component (in Polymer – my preferred mechanism) to rotate images. I see this a lot on home pages and I think it would look cool. I’ve downloaded 4 fantasy wall papers in HD and resized them to be 1920×600 pixel resolution so that I have something to test this on. In an ideal world, these will change on a regular basis and have a nice transition between the different images.

On to the web component. I’ve adjusted my home page Areas/Main/Views/Home/Index.cshtml file to be the following:

@{ ViewBag.Title = "Home Page"; }

<div id="#Homepage">
    <div class="container-fluid">
        <div class="row">
            <dnd-carousel>
                <div class="s1"></div>
                <div class="s2"></div>
                <div class="s3"></div>
                <div class="s4"></div>
            </dnd-carousel>
        </div>
    </div>
</div>

@section htmlelements {
    <link rel="import" href="/jspm_packages/github/Polymer/polymer@0.8.0/dist/polymer.html">
    <link rel="import" href="~/elements/dnd-carousel.html">

    <style>
        html /deep/ .s1 {
            background-image: url("/images/backgrounds/homepage-s1.jpg");
        }
        html /deep/ .s2 {
            background-image: url("/images/backgrounds/homepage-s2.jpg");
        }
        html /deep/ .s3 {
            background-image: url("/images/backgrounds/homepage-s3.jpg");
        }
        html /deep/ .s4 {
            background-image: url("/images/backgrounds/homepage-s4.jpg");
        }
    </style>
}

The home page itself is relatively straight forward. The dnd-carousel will be my web component. Inside are four DIVs that I will rotate between.

Note that html /deep/ notation in the style sheet section. The child DIVs of the dnd-carousel element end up inside the shadow (or shady) DOM. This means that I have to cross that boundary between Shadow and Shady DOM to style those elements. I could have been explicit – adding a style property to the elements instead – especially considering that these are just images. The style sheet won’t be applied properly if I don’t include the html /deep/ syntax.

On to the web component. As normal, I’m putting my component under src/elements in a directory called dnd-carousel. There is a HTML file, a JS file and a LESS file. The basics are always the same. My HTML file is dnd-carousel.html:

<dom-module id="dnd-carousel">
    <link rel="stylesheet" href="dnd-carousel.css">
    <template>
        <div id="contentwrapper">
            <div id="carousel">
                <content></content>
            </div>
            <div id="carouselmarkers">
            </div>
        </div>
    </template>
</dom-module>

<script src="dnd-carousel.js"></script>

All I’ve changed here is the markup in the shadow DOM. I need a less file – dnd-carousel.less – to style this:

:host {
    display: block;
    width: 100%;
    height: 600px;
}

#contentwrapper {
    position: relative;
    width: 100%;
    height: 600px;
    left: 0;
    top: 0;

    #carousel {
        > ::content {
            display: block;
            width: 100%;
            height: 100%;

            div {
                display: none;
                width: 100%;
                height: 100%;
                background-repeat: no-repeat;
                background-size: cover;

                &.active {
                    display: block;
                    transition: all 1s ease-in-out;
            }
        }
    }
}

Note the active class. I’m going to add and remove this class from the elements as they come to the front and move to the back of my element. If the element is “active”, then it’s shown. If not, it isn’t.

Finally, I need a small Javascript file – dnd-carousel.js:

Polymer({
    is: "dnd-carousel",

    ready: function() {
        console.log("dnd-carousel: In ready()");
    }
});

With this markup, nothing is being shown. I can, however, check with the F12 Developer Tools that the right size and position is happening. Let’s start filling in the functionality:

Markers

In most of the carousels I see, there are numbers or blocks at the bottom – click on a number or block and you get taken to that image in the carousel. I’d like to emulate that. I added a carouselmarkers empty DIV into the HTML shadow DOM for this purpose. I now need to fill it in, which I do programatically:

    ready: function () {
        this.DIVSET = Polymer.dom(this).querySelectorAll("#carousel > div");
        if (this.DIVSET.length === 0) {
            console.log("No members - turning off");
            return;
        }

        for (var i = 1 ; i <= this.DIVSET.length ; i++) {
            var marker = document.createElement("span");
            marker.className = "marker";
            marker.innerHTML = i.toString();
            Polymer.dom(this.$.carouselmarkers).appendChild(marker);
        }
        Polymer.dom.flush();
    }

The first block gets all the DIVs that we passed in as children of the dnd-carousel and stored them for later. The next block constructs the elements I will need. Each digit will be added as a span with a class of marker.

Note the use of the Polymer.dom() API here. When you use this.$.id.appendChild(), it doesn’t add the style-scope necessary for using Shady DOM (which is the type of DOM used in webcomponents-lite.js that Polymer now uses). By utilizing the Polymer.dom() API for DOM interactions within the web component, that functionality gets implemented for us.

Of course, I’ll need some styling for this:

    #carouselmarkers {
        position: relative;
        top: -50px;
        width: 100%;
        text-align: center;
        z-index: 5;

        span.marker {
            border: 1px solid white;
            border-radius: 50%;
            color: white;
            padding: 0 4px;
            margin: 0 4px;
            cursor: pointer;

            &:hover {
                border: 1px solid #ff6a00;
                color: #ff6a00;
            }
        }
    }

This block is inside of the #contentwrapper at the same level as #carousel in the less file. I’ll also want to display the first carousel member to test this. Since I’ll be activating carousel elements, I may as well add a function to the Polymer object for that purpose:

Polymer({
    is: "dnd-carousel",

    ready: function () {
        this.DIVSET = Polymer.dom(this).querySelectorAll("#carousel > div");
        if (this.DIVSET.length === 0) {
            console.log("No members - turning off");
            return;
        }

        for (var i = 1 ; i <= this.DIVSET.length ; i++) {
            var marker = document.createElement("span");
            marker.className = "marker";
            marker.innerHTML = i.toString();
            Polymer.dom(this.$.carouselmarkers).appendChild(marker);
        }
        Polymer.dom.flush();

        // Reset the carousel active element
        this.activeElement = -1;

        // Display the first carousel
        this.displayCarousel(0);
    },

    // Private methods
    displayCarousel: function (idx) {
        if (idx === this.activeElement) {
            console.log("displayCarousel: Skipping as requested EL is the same as Active EL");
            return;
        }

        console.log("displayCarousel: displaying carousel element %d", idx);
        for (var i = 0 ; i < this.DIVSET.length ; i++) {
            var el = this.DIVSET[i];
            if (i === idx) {
                this.addClass(el, "active");
            } else {
                this.removeClass(el, "active");
            }
        }

        this.activeElement = idx;
    }
});

With this code and the styling applied, I can see the first element (the one labelled class="s1") and there will be the numbers 1 through 4 on top of the picture in white circles. If I roll over the numbers, they should turn orange. I’ve added some code from youmightnotneedjquery for addition and removal of classes as well:

    //#region CSS Class Handling Functions
    hasClass: function(el, className) {
        if (el.classList) {
            return el.classList.contains(className);
        } else {
            return new RegExp("(^| )" + className + "( |$)", "gi").test(el.className);
        }
    },

    addClass: function(el, className) {
        if (this.hasClass(el, className)) {
            return;
        }
        if (el.classList) {
            el.classList.add(className);
        } else {
            el.className += " " + className;
        }
    },

    removeClass: function(el, className) {
        if (!this.hasClass(el, className)) {
            return;
        }
        if (el.classList) {
            el.classList.remove(className);
        } else {
            el.className = el.className.replace(new RegExp("(^|\\b)" + className.split(" ").join("|") + "(\\b|$)", "gi"), " ");
        }
    },
    //#endregion

This is library code for me, but it’s so small that I include it where I need it.

Making the Markers Clickable

My next step is to make the markers clickable. The best way to do this is to add an event handler that calls my displayCarousel() method when the user clicks on the number elements. I can do this during the marker creation process in the ready() method. Here is the complete marker production loop:

        for (var that = this, i = 1 ; i <= this.DIVSET.length ; i++) {
            var marker = document.createElement("span");
            marker.className = "marker";
            marker.innerHTML = i.toString();
            Polymer.dom(this.$.carouselmarkers).appendChild(marker);

            marker.addEventListener("click", function (evt) {
                var newIndex = parseInt(evt.currentTarget.innerText);
                that.displayCarousel(newIndex - 1);
            });
        }
        Polymer.dom.flush();

The transition is a little harsh right now – I don’t think my transition CSS property is taking effect at the moment, but I can deal with that later.

Cycling the images

The other thing I want my carousel to do is to cycle through the images on a regular basis. For this, I want to add a property “interval” to my dnd-carousel, with a default timing of 30 seconds. Then I will use that in the ready() method to set up my event listener for transitioning the carousel. I’ll use the same displayCarousel() method to change the image. To handle the property, I have a new element within the Polymer object:

    properties: {
        interval: {
            type: Number,
            value: 30
        }
    },

You can check out the documentation on Polymer properties for more information. To set up the timer and call the displayCarousel() method, I added the following code to the ready() method:

        this.intervalTimer = setInterval(function () {
             var newIndex = (that.activeElement + 1) % that.DIVSET.length;
            that.displayCarousel(newIndex);
        }, this.interval * 1000);

I’ve already started tracking the active element (in this.activeElement), so all I need to do is increment the active element, adjust for the number of elements, then call displayCarousel().

Finishing off the Carousel

I did some more work before checking it in, but the basic functionality is there now. The things I did before checking in included:

  1. Highlighting the color marker when the image is shown
  2. Improving transitions between images by using opacity instead of display
  3. Some general code clean-up

You can get this code at tag cs-0.0.9. However, all the code is isolated to the src/elements/dnd-carousel directory, except for the view change on the home page.