Get to know the Repeater!

Repeater is a multipurpose control provided by Fuel UX. It allows for easy rendering/searching/paging of multiple records or repeated data using a common template.

We will be using it to make this data grid

More options exist such as multiple views and on-demand rendering, but this tutorial focuses on creating a custom-column repeater. Most of the work goes into defining and manipulating a data-providing function and a column-rendering function.

Repeater diagram

Step 1Set up the template

In this tutorial, we reference Fuel UX and its dependencies from their CDNs. See the Getting Started page in the documentation for more info.

Create an HTML page. Paste the following for this demo, so it references CSS in the <head> and the JavaScript files Fuel UX, Bootstrap, and jQuery just above </body>.

<!DOCTYPE html>
<html lang="en" class="fuelux">
  <head>
    <meta charset="utf-8">
    <title>Fuel UX Tree</title>
    <link href="http://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css" rel="stylesheet"/>
    <link href="http://www.fuelcdn.com/fuelux/3.5.0/css/fuelux.min.css" rel="stylesheet"/>
  </head>
  <body>

    <script src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
    <script src="http://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js"></script>
    <script src="http://www.fuelcdn.com/fuelux/3.4.0/js/fuelux.min.js"></script>
  </body>
</html>

Open the page on your server or with these dependencies set up in a CodePen. If you view the output, you should see a blank page. If you open the browser console, there should be no errors.

Step 2Add repeater markup

Fuel UX binds to existing markup on the page (just like Bootstrap). Start by placing the repeater markup inside of the body tags:

<div class="repeater" id="myRepeater" data-staticheight="true" style="position:absolute; top:25px; right:25px; bottom:25px; left:25px;">
    <div class="repeater-header">
        <div class="repeater-header-left">
            <div class="repeater-search">
                <div class="search disabled input-group">
                    <input type="search" class="form-control" placeholder="Search">
                    <span class="input-group-btn">
                        <button class="btn btn-default" type="button">
                            <span class="glyphicon glyphicon-search"></span>
                            <span class="sr-only">Search</span>
                        </button>
                    </span>
                </div>
            </div>
        </div>
        <div class="repeater-header-right">
            <div class="btn-group selectlist disabled repeater-filters">
                <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
                    <span class="selected-label"> </span>
                    <span class="caret"></span>
                    <span class="sr-only">Toggle Filters</span>
                </button>
                <ul class="dropdown-menu pull-right" role="menu">
                    <li data-value="all" data-selected="true" data-property="all"><a href="#">All Items</a></li>
                    <li class="divider"></li>
                    <li data-value="draft" data-property="status"><a href="#">Draft</a></li>
                    <li data-value="archived" data-property="status"><a href="#">Archived</a></li>
                    <li data-value="active" data-property="status"><a href="#">Active</a></li>
                </ul>
                <input class="hidden hidden-field" name="filterSelection" readonly="readonly" aria-hidden="true" type="text">
            </div>
        </div>
    </div>
    <div class="repeater-viewport">
        <div class="repeater-canvas"></div>
        <div class="loader repeater-loader"></div>
     </div>
     <div class="repeater-footer">
         <div class="repeater-footer-left">
             <div class="repeater-itemization">
                 <span><span class="repeater-start"></span> - <span class="repeater-end"></span> of <span class="repeater-count"></span> items</span>
                 <div class="btn-group selectlist dropup" data-resize="auto">
                     <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
                         <span class="selected-label"> </span>
                         <span class="caret"></span>
                         <span class="sr-only">Toggle Dropdown</span>
                     </button>
                     <ul class="dropdown-menu" role="menu">
                         <li data-value="10" data-selected="true"><a href="#">10</a></li>
                         <li data-value="25"><a href="#">25</a></li>
                         <li data-value="50"><a href="#">50</a></li>
                     </ul>
                     <input class="hidden hidden-field" name="itemsPerPage" readonly="readonly" aria-hidden="true" type="text">
                 </div>
                 <span>Per Page</span>
             </div>
         </div>
         <div class="repeater-footer-right">
             <div class="repeater-pagination">
                 <button type="button" class="btn btn-default btn-sm repeater-prev">
                     <span class="glyphicon glyphicon-chevron-left"></span>
                     <span class="sr-only">Previous Page</span>
                 </button>
                 <label class="page-label" id="myPageLabel">Page</label>
                 <div class="repeater-primaryPaging active">
                     <div class="input-group input-append dropdown combobox dropup">
                         <input type="text" class="form-control" aria-labelledby="myPageLabel">
                         <div class="input-group-btn">
                             <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
                                 <span class="caret"></span>
                                 <span class="sr-only">Toggle Dropdown</span>
                             </button>
                             <ul class="dropdown-menu dropdown-menu-right"></ul>
                         </div>
                     </div>
                 </div>
                 <input type="text" class="form-control repeater-secondaryPaging" aria-labelledby="myPageLabel">
                 <span>of <span class="repeater-pages"></span></span>
                 <button type="button" class="btn btn-default btn-sm repeater-next">
                     <span class="glyphicon glyphicon-chevron-right"></span>
                     <span class="sr-only">Next Page</span>
                 </button>
             </div>
         </div>
     </div>
</div>

Save these changes and reload the page. At this point you should see an empty repeater example. We will create a datasource provider in the next step which will eventually be bound to the repeater to populate the data. Inline styling has been added for this tutorial.

The three sections of a repeater are repeater-header, repeater-viewport, and repeater-footer. Custom controls can be placed in repeater-header if needed. To simplify this tutorial, header controls have been disabled.

Step 3Create a static data function for the repeater:

When the repeater needs to load data, it will call your dataSource function with two arguments:

  • options - an object containing filter, search, paging, and other custom criteria.
  • callback - a function called when the data is ready for rendering.

If no options are passed, the defaults used will be page: 0, pages: 1, count: 0, end: 0, start: 0, items: [] with page to be 0-based and pages to be 1-based.

The callback function expects an object with this structure to be passed as an argument:

{
  'page':    [integer],
  'pages':   [integer],
  'count':   [integer],
  'start':   [integer],
  'end':     [integer],
  'columns': [array],
  'items':   [array]
}
			

Let's start by creating a function staticDataSource that handles this. Place this function within a <script> before the libraries load. The repeater still hasn't been initialized, so you will not see any changes yet.

function staticDataSource(options, callback) {

  // define the columns for the grid
  var columns = [
    {
      'label': 'Name',      // column header label
      'property': 'name',   // the JSON property you are binding to
      'sortable': true      // is the column sortable?
    },
    {
      'label': 'Description',
      'property': 'description',
      'sortable': true
    },
    {
      'label': 'Status',
      'property': 'status',
      'sortable': true
    },
    {
      'label': 'Category',
      'property': 'category',
      'sortable': true
    }
  ];

  // generate the rows in your dataset
  // note: the property names of your items should
  // match the column properties defined above
  //
  function generateRandomData() {
    var items = [];
    var statuses = ['archived', 'active', 'draft'];
    var categories = ['category1', 'category2', 'category3'];
    function getRandomStatus() {
      var min = 0;
      var max = 2;
      var index = Math.floor(Math.random() * (max - min + 1)) + min;
      return statuses[index];
    }
    function getRandomCategory() {
      var min = 0;
      var max = 2;
      var index = Math.floor(Math.random() * (max - min + 1)) + min;
      return categories[index];
    }

    for(var i=1; i<=100; i++) {
      var item = {
        id: i,
        name: 'item ' + i,
        description: 'desc ' + i,
        status: getRandomStatus(),
        category: getRandomCategory()
      }
      items.push(item);
    };
    console.log(JSON.stringify(items));
    return items;
  }

  items = generateRandomData();

  // transform array
  var pageIndex = options.pageIndex;
  var pageSize = options.pageSize;
  var totalItems = items.length;
  var totalPages = Math.ceil(totalItems / pageSize);
  var startIndex = (pageIndex * pageSize) + 1;
  var endIndex = (startIndex + pageSize) - 1;
  if (endIndex > items.length) {
    endIndex = items.length;
  }

  var rows = items.slice(startIndex - 1, endIndex);

  // define the datasource
  var dataSource = {
    'page': pageIndex,
    'pages': totalPages,
    'count': totalItems,
    'start': startIndex,
    'end': endIndex,
    'columns': columns,
    'items': rows
  };

  // pass the datasource back to the repeater
  callback(dataSource);
}

Step 4Initialize the repeater

Initialize the repeater, passing in the `staticDataSource` you just created as the `dataSource`:

$(function() {
  // initialize the repeater
  var repeater = $('#myRepeater');
  repeater.repeater({
    // setup your custom datasource to handle data retrieval;
    // responsible for any paging, sorting, filtering, searching logic
    dataSource: staticDataSource
  });
});

Here is this example on CodePen (for the sake of simplicity, search and filter dropdowns are not functional).

AsideExample with an API

If you wanted to change the data providing function to fetch the dataset rows from an API and calculate the datasource properties from the results, you could do something like this (we won't be using this for our tutorial though, since it requires a specific response from your server and won't work locally.):

function dynamicDataSource(options, callback) {

  // define the columns for the grid
  var columns = [
    {
      'label': 'Name',      // column header label
      'property': 'name',   // the JSON property you are binding to
      'sortable': true      // is the column sortable?
    },
    {
      'label': 'Description',
      'property': 'description',
      'sortable': true
    },
    {
      'label': 'Status',
      'property': 'status',
      'sortable': true
    },
    {
      'label': 'Category',
      'property': 'category',
      'sortable': true
    }
  ];

  // set options
  var pageIndex = options.pageIndex;
  var pageSize = options.pageSize;
  var options = {
    'pageIndex': pageIndex,
    'pageSize': pageSize,
    'sortDirection': options.sortDirection,
    'sortBy': options.sortProperty,
    'filterBy': options.filter.value || '',
    'searchBy': options.search || ''
  };

  // call API, posting options
  $.ajax({
    'type': 'post',
    'url': '/repeater/data',
    'data': options
  })
  .done(function(data) {

    var items = data.items;
    var totalItems = data.total;
    var totalPages = Math.ceil(totalItems / pageSize);
    var startIndex = (pageIndex * pageSize) + 1;
    var endIndex = (startIndex + pageSize) - 1;

    if(endIndex > items.length) {
      endIndex = items.length;
    }

    // configure datasource
    var dataSource = {
      'page':    pageIndex,
      'pages':   totalPages,
      'count':   totalItems,
      'start':   startIndex,
      'end':     endIndex,
      'columns': columns,
      'items':   items
    };

    // pass the datasource back to the repeater
    callback(dataSource);
  });
}

Example JSON payload

{
  "total": 100,
  "items": [
    {
      "name": "Name 1",
      "description": "Desc 1",
      "status": "draft",
      "category": "category2"
    },
    ...
    {
      "name": "Name 5",
      "description": "Desc 5",
      "status": "active",
      "category": "category3"
    }
  ]
}
        

In this example, there are 100 total items, but only 5 are returned, since the pageSize is set to 5.

The API response contains an object with 2 properties: total and items. total is an integer indicating the total number of items that exist. items is an array of items that match your filter criteria.

Let's return to the staticDataSource function and add custom column rendering.

Step 5Add custom rendering

To inject markup or combine multiple API properties into a single column, create and pass a function into the renderer. The custom render function must accept these two properties:

  1. helpers - an object that contains the column data.
  2. callback - a function that you call when you're ready for the column to be rendered.

So let's start by creating a custom column rendering function that handles this:

function customColumnRenderer(helpers, callback) {
        callback();
      }

We are again using the staticDataSource defined in Step 3. Pass the new custom column renderer function to the control by adding a list_columnRendered option to your repeater initialization:

$(function() {
        // initialize the repeater
        var repeater = $('#myRepeater');
        repeater.repeater({
          // use the static data provider from previous steps
          dataSource: staticDataSource,
          // setup custom column renderer for the list view
          list_columnRendered: customColumnRenderer
        });
      });

Nothing should have changed with your repeater at this point. In the next step, we will manipulate the table cells.

Step 6Fill out custom column renderer

Add logic to the customColumnRenderer function to override the markup to create a link on the name and change the status color:

function customColumnRenderer(helpers, callback) {
        // Determine what column is being rendered and review 
        // http://getfuelux.com/extensions.html#bundled-extensions-list-options
        // for more information on the helpers object.
        var column = helpers.columnAttr;

        // get all the data for the entire row
        var rowData = helpers.rowData;
        var customMarkup = '';

        // Only override the output for specific columns.
        // This will default to output the text value of the row item
        switch(column) {
          case 'name':
            // let's combine name and description into a single column
            customMarkup = '<a href="#">' + rowData.name + '</a><div class="small text-muted">' + rowData.description + '</div>';
            break;

          case 'status':
            // let's change the text color based on status
            var color = '#000';
            switch(helpers.item.text()) {
              case 'draft':
                color = '#4EB1CB';
                break
              case 'active':
                color = '#4AB471';
                break;
              case 'archived':
                color = '#CF5C60';
                break;
            }
            customMarkup = '<span style="color:' + color + '">' + helpers.item.text() + '</span>';
            break;

          default:
            // otherwise, just use the existing text value
            customMarkup = helpers.item.text();
            break;
        }

        helpers.item.html(customMarkup);

        callback();
      }

Here is this example on CodePen.

Repeater with custom rendering

That's all folks!

Now that you've created a simple datagrid, we'd love to get feedback on Twitter about this tutorial or any of the other 15 Fuel UX controls. We love fixing issues and receiving pull requests on Github. Open source for-the-win!

Sincerely yours,
Stephen James
and the Fuel UX team

Github Twitter