/ collar

Code a carousel with collarjs

Carousel is a commonly used component in web applications. It represents a list of items (data). Only one item is represented at a time (called 'current' item), user can browse previous item or next one on the UI.

collarjs arousel example

In this post, I will implement a simple carousel with collarjs, The following diagram shows how the final version works. We will go through the whole process to build it.

carousel data flow

Note: you can find the code at github repository

Or you can check the example carousel here

Let's first think about how will we use the carousel:

// create the carousel with an element id
var myCarousel = carousel();
myCarousel.init('my-carousel');
// set the items of the carousel
myCarousel.setItems([
  { img: 'https://unsplash.it/600/?random&abc=1',
    title: 'Example Item Title 1',
    subtitle: 'subtitle for item 1' },
  { img: 'https://unsplash.it/600/?random&abc=2',
    title: 'Example Item Title 2',
    subtitle: 'subtitle for item 2' },
  { img: 'https://unsplash.it/600/?random&abc=3',
    title: 'Example Item Title 3',
    subtitle: 'subtitle for item 3'}
]);
// switch to the next item
myCarousel.showNext();
// switch to the previous item
myCarousel.showPrev();

As collarjs is message driven, these APIs can be represented as messages:

// 'init carousel' message
{ msg: "init carousel",
  id: "element id" }

// 'set items' message
{ msg: "set items",
  items: [] // list of item objects }

// switch to previous item
{ msg: "prev item" }

// switch to next item
{ msg: "next item" }

Next, let's build pipeline to handle each of these messages. Before we start to handle the messages, we need to create an input node to accept messages and an output node to return the results.

// create a namespace
var ns = window.collar.ns('com.collartechs.example.carousel');
var input = ns.input('carousel input');
var output = ns.output('carousel output');

Handle 'init carousel' message

During the carousel initiation, we simply create a container to host the carousel items and initiate the sensor to watch the user input on the UI. A filter (when operator) is used to make sure only the 'init carousel' message is handled here. The element id retrieved from 'init carousel' message is stored in the id variable. The carousel container is defined in a template string. See the following code:

var id; // element id for container
var carouselTemplate = `
<div class="carousel">
  <ol class="carousel-content"></ol>
  <div class="carousel-prev">prev</div>
  <div class="carousel-next">next</div>
</div>
`;

var uiSensor = ns.sensor('carousel UI sensor', function (options) {
    var sensor = this;
    if (options === 'init carousel') {
      // watch clicking prev event, and send 'prev item' message
      document.querySelector('#' + id + ' .carousel-prev')
        .addEventListener('click', function () {
          sensor.send({ msg: 'prev item' });
        });
      // watch clicking next event, and send 'next item' message
      document.querySelector('#' + id + ' .carousel-prev')
        .addEventListener('click', function () {
          sensor.send({ msg: 'next item' });
        });
    }
  });

input
  .when('init carousel', function (signal) {
    return signal.get('msg') === 'init carousel';
  })
  .do('init carousel container', function (signal) {
    id = signal.get('id');
    var container = document.querySelector('#' + id);
    container.innerHTML = carouselTemplate;
  })
  .do('init UI sensor', function (signal) {
    uiSensor.watch('init carousel');
  })
  .to(output); // just output the same message

In the code above, We directly pipe the message to output, if your want to inform the external world the carousel is initiated, you can use map operator to generate a 'carousel initiated' message and put the extra data in the message, for example, the id of the carousel:

  .map('generate "carousel initatiated" message', function (signal) {
    return signal.new({
      msg: 'carousel initiated',
      id: signal.get('id')	
    });
  })
  .to(output);

Handle 'set items' message

After the carousel component is created, we need to setup it by feeding it a list of items. This is handled by the 'set items' message:

input
  .when('set items')
  .do('store items in memory')
  .do('create items elements')
  .do('change current index to 0')
  .do('show current item')
  .to(output);

To represent a carousel, we use the following data structure:

var data = {
  items: [], // the list of items
  current: 0 // the index of current item
}

The implementation is simple:

const carouselItemTemplate = '<li class="carousel-item {CURRENT}">' +
  '<img src="{IMG}"/>' +
  '<div class="carousel-desc">' +
     '<div class="carousel-title">' +
        '<h1>{TITLE}</h1>' +
     '</div>' +
     '<div class="carousel-calories">' +
        'Subtitle: <span>{SUBTITLE}</span>' +
     '</div>' +
  '</div>' +
'</li>';


input
  .when('set items', function (signal) {
    return signal.get('msg') === 'set items';
  })
  .do('store items in memory', function (signal) {
    data.items = signal.get('items');
  })
  .do('create items elements', function (signal) {
    var carouselElemItemStr = '';
    for (var i = 0; i < data.items.length; i++) {
      carouselElemItemStr += carouselItemTemplate
        .replace('{CURRENT}', '')
        .replace('{IMG}', data.items[i].img)
        .replace('{TITLE}', data.items[i].title)
        .replace('{SUBTITLE}', data.items[i].subtitle);
    }
    var carouselContentEle = document.querySelector('#' + id + ' .carousel-content');
    carouselContentEle.innerHTML = carouselElemItemStr;
  })
  .do('change current index to 0', function (signal) {
    data.current = 0;
  })
  .do('show current item', function (signal) {
    var oldCurrentItem = document.querySelector('.current');
    if (oldCurrentItem) oldCurrentItem.classList.remove('current');

    var newCurrentItem = document.querySelector('#' + id + ' li.carousel-item:nth-of-type(' + (data.current+1) + ')');
    if (newCurrentItem) oldCurrentItem.classList.add('current');
  })
  .map('generate "items changed" message', function (signal) {
    return signal.new({
      msg: 'items changed',
      items: signal.get('items')
    });
  })
  .to(output);

An 'items changed' message is sent throught the output node with the list of items in items field.

Handle 'prev item' and 'next item' message

When 'prev item' or 'next item' message is received, we change the current index in the data variable, and display the new current item by adding 'current' class to it.

input
  .when('next item')
  .do('change current index to next')
  .do('show current item')
  .map('generate "current item changed" message') 
  .to(output);

Here is the implementation:

input
  .when('next item', function (signal) {
    return signal.get('msg') === 'next item';
  })
  .do('change current index to next', function (signal) {
    data.current = (data.current + 1) % data.items.length;
  })
  .do('show current item', function (signal) {
    var oldCurrentItem = document.querySelector('.current');
    if (oldCurrentItem) oldCurrentItem.classList.remove('current');

    var newCurrentItem = document.querySelector('#' + id + ' li.carousel-item:nth-of-type(' + (data.current+1) + ')');
    if (newCurrentItem) oldCurrentItem.classList.add('current');
  })
  .map('genereate "current item changed" message', function (signal) {
    return signal.new({
      msg: 'current item changed',
      current: data.current	
    });
  })
  .to(output);

The handling of 'prev item' is similar to 'next item', the difference here is that we change the current index to previous one.

input
  .when('previous item', function (signal) {
    return signal.get('msg') === 'next item';
  })
  .do('change current index to next', function (signal) {
    data.current = data.current === 0 ? data.items.length - 1 : data.current - 1;
  })
  .do('show current item', function (signal) {
    var oldCurrentItem = document.querySelector('.current');
    if (oldCurrentItem) oldCurrentItem.classList.remove('current');

    var newCurrentItem = document.querySelector('#' + id + ' li.carousel-item:nth-of-type(' + (data.current+1) + ')');
    if (newCurrentItem) oldCurrentItem.classList.add('current');
  })
  .map('genereate "current item changed" message', function (signal) {
    return signal.new({
      msg: 'current item changed',
      current: data.current	
    });
  })
  .to(output);

Wrap message to NodeAPI

Now we have 4 messages for the carousel:

  1. init carousel
  2. set items
  3. next item
  4. previous item

We can wrap these 4 messages to node-like async callback API (by using toNode collar API):

collar.toNode(input, output) : Function

The toNode API accepts an input node and an output node, returns a node like async function.

function nodeFunc(message, callback, [non-interruptible]);

The callback accepts two arguments: an error and the output signal. Different from the standard node-like async function, the third argument tells collarjs if the message is interruptible. By default, collarjs will release the thread resource when the message is processed by a node, and give other messages a chance to be processed. Sometimes, you want the message finished processing before any other messages to be processed, this can be done by setting false as the third argument.

Let's create 4 APIs by wrapping the 4 messages:

var apiFunc = window.collar.toNode(input, output);

function carousel() {
  return {
    init : function (id, done) {
      apiFunc({
        msg: 'init carousel',
        id: id
      }, done, false);	// use the false argument to make the message non-interruptible			
    },
    setItems : function (items, done) {
      apiFunc({
        msg: 'set items',
        items: items
      }, done, false);				
    },
    next : function (done) {
      apiFunc({
        msg: 'next item'
      }, done, false);				
    },
    prev : function (done) {
      apiFunc({
        msg: 'previous item'
      }, done, false);				
    }
  }
}

Now we can use these API to setup a carousel:

var myCarousel = carousel();
myCarousel.init('carouselId');
myCarousel.setItems([
  {
    img: 'https://unsplash.it/600/?random&abc=1',
    title: 'Example Title 1',
    subtitle: 'subtitle',
  },
  {
    img: 'https://unsplash.it/600/?random&abc=2',
    title: 'Example Title 2',
    subtitle: 'subtitle',
  },
  {
    img: 'https://unsplash.it/600/?random&abc=3',
    title: 'Example Title 3',
    subtitle: 'subtitle',
  }
]);
myCarousel.next();

Understand how carousel works

First install collar-dev-server, and run it

sudo npm install collar-dev-server -g

collar-dev-server

// check dev tool from http://localhost:7500

Run the example

Clone from github
git clone https://github.com/bhou/collar-example-carousel.git
sudo npm install http-server -g
http-server .

Open the carousel in browser

http://localhost:8080/index.html
Or directly run the online example
http://collarjs.com/examples/carousel/index.html

Now check collar dev server page at http://localhost:7500 to see how carousel works. You can record the runtime signals by clicking the record button in collar dev tool.

Resources

The code can be found at github repository (collar-example-carousel)

Play with the example at collar.js website