How to create a repeater field with Gutenberg

Written by Phil Birss: Group CEO.
· 4 minute read

Since WordPress 5.0 was released in December, we have been introduced to its new editor Gutenberg.

The content can now be added through “blocks”, which allow the user to create the page in a more visual way than the previous editor. WordPress supplies a library of basic blocks, such as buttons, images, a map etc. Also, it provides some APIs to build new blocks. Basically, you can build your backend as you please!

The editor is written entirely in Javascript. This is the major change in terms of development, as you now need to write code which is executed client side. Behind the editor there is the popular Javascript framework, React. Gutenberg adds an abstraction on top of React as it helps to keep the code centralised.

As the Gutenberg release happened only a few months ago, many are finding it difficult to locate helpful resources to sort common issues that arise when using Gutenberg. One of the issues I have encountered on my way was finding a proper way to create a repeater field. I wanted to build a block which did exactly what ACF does through its repeater field. In other words, allowing the user to insert multiple content instances of the same kind. So, if you are keen to do the same, this blog might be helpful to you.

Below, I will highlight the steps to achieve a repeater field. Our block will simply allow us to add multiple instances of a title. Try to focus on the field skeleton, rather than what is inside the instance of the content.

Let’s start by defining the attributes property:

attributes: {
  items: {        
    source: 'query',
    default: [],
    selector: '.item',
    query: {
      title: {
        type: 'string',
        source: 'text',
        selector: '.title'
      },
      index: {            
        type: 'number',
        source: 'attribute',
        attribute: 'data-index'            
      }           
    }
  }       
}

The instances will be contained in the “items” attribute, which has the “query” source, and will be an array. Its instances will be populated according to the structure defined in the “query” field. In other words, our array will look like [{title: ‘Some Text’, index: 0}, {title: ‘Some other text’, index: 1} … {…}].

Below is part of the edit function:

edit: function( props ) {

  var attributes = props.attributes;

  var itemList = attributes.items.sort(function(a , b) {
    return a.index - b.index;
  }).map(function(item){          
    return el('div', { className: 'item' },        
        el( RichText, {                
        tagName: 'h1',
        placeholder: 'Here the title goes...',                
        value: item.title,
        autoFocus: true,
        onChange: function( value ) {                                
          var newObject = Object.assign({}, item, {
            title: value
          });
          return props.setAttributes({
            items: [].concat(_cloneArray(props.attributes.items.filter(function (itemFilter) {
              return itemFilter.index != item.index;
            })), [newObject])
          });
        }
      }            
    ),
    )
  });

  ...

The instances are ordered by the index value, and then a list of RichText elements is created, where every one of them refers to an instance with the title attribute.
The “onChange” function represents the key of the repeater logic. First, we need to create a new object which contains the current instance with the new value for the title. Then, we need to update our “items” attribute by concatenating an array with all instances except the one we are updating, and our new object previously created. To make sure the whole thing works, it’s important that we don’t make any changes to the props variable – and by this I mean we don’t copy any objects or arrays by reference, but by value. So, something like the below would not work:

item[‘title’] = value;
var newItems = props.attributes.items;
props.setAttributes({items: newItems})

The following function comes in handy to clone (copy by value) an array of objects:

function _cloneArray(arr) { 
  if (Array.isArray(arr)) { 
    for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { 
      arr2[i] = arr[i]; 
    } 
    return arr2; 
  } else { 
    return Array.from(arr); 
  } 
}

The below code describes the value returned by the edit function:

return el(
  'div',
  { className: props.className },
  el('div', { className: 'item-list' },        
    itemList,
  ),
  el( components.Button, {
      className: 'button add-row',            
      onClick: function() {              
        return props.setAttributes({
          items: [].concat(_cloneArray(props.attributes.items), [{
            index: props.attributes.items.length,                  
            title: ""                  
          }])
        });                            
      }
    },
    'Add Row'
  )        
);

The value returned will be a React Element containing the list of RichText components previously defined, and a button which, when clicked, creates a new instance. Note that we copy our array by value again.

The save function is the last piece of the puzzle:

save: function( props ) {

  var attributes = props.attributes;            

  if (attributes.items.length > 0) {

    var itemList = attributes.items.map(function(item) {          
    
      return el('div', { className: 'item', 'data-index': item.index },        
        el( 'h1', {              
          className: 'title',                                 
        }, item.title)            
      );

    });

    return el(
      'div',
      { className: props.className },
      el('div', { className: 'item-list' },        
        itemList
      )              
    ); 

  } else {
    return null;
  }
}

Here we just define how our block will be shown on the website. It is crucial to follow the structure defined in the attributes property. In our case, every title will be saved in a div having a class called “title” (see selector: “.title” in the attributes definition), which in turn is contained in a div having a class called “item”.

That is only a starting point to create blocks with repeatable groups of fields. As you can see, you could add to the instance some more attributes as well as the title, and the scenario would be exactly the same.