Having ACF-like repeater fields on Gravity Forms can be a bit tricky. Up until recently there was a free plugin which did the job, but, according to its author, “it is no longer maintained and is probably broken”. Currently, the best plug-n’-play alternative seems to be Gravity Forms Nested Forms, which is part of the paid Gravity Perks.

The good news, though, is that there is another, free and official alternative: Since version 2.4, Gravity Forms includes native support for repeater fields. Sadly, this feature seems to be stuck in beta for too long, with no estimate on when it will be ready for prime time. So, currently there is no Form Editor UI component built for this field yet, which means that you cannot add it to a form using standard Form Editor drag and drop methods. Practically, this field is only intended for developers who can build their forms programmatically, or through other methods.

The official documentation includes usage examples, demonstrating how you can implement the functionality on your forms. When I tried implementing them, though, undocumented issues came up and no workarounds could be found on Google, Stack Overflow or the official Gravity Forms Forum. Lets take a closer look at the official example:

// Adjust your form ID
add_filter( 'gform_form_post_get_meta_149', 'add_my_field' );
function add_my_field( $form ) {
 
    // Create a Single Line text field for the team member's name
    $name = GF_Fields::create( array(
        'type'   => 'text',
        'id'     => 1002, // The Field ID must be unique on the form
        'formId' => $form['id'],
        'label'  => 'Name',
        'pageNumber'  => 1, // Ensure this is correct
    ) );
 
    // Create an email field for the team member's email address
    $email = GF_Fields::create( array(
        'type'   => 'email',
        'id'     => 1001, // The Field ID must be unique on the form
        'formId' => $form['id'],
        'label'  => 'Email',
        'pageNumber'  => 1, // Ensure this is correct
    ) );
 
    // Create a repeater for the team members and add the name and email fields as the fields to display inside the repeater.
    $team = GF_Fields::create( array(
        'type'             => 'repeater',
        'description'      => 'Maximum of 3 team members  - set by the maxItems property',
        'id'               => 1000, // The Field ID must be unique on the form
        'formId'           => $form['id'],
        'label'            => 'Team Members',
        'addButtonText'    => 'Add team member', // Optional
        'removeButtonText' => 'Remove team member', // Optional
        'maxItems'         => 3, // Optional
        'pageNumber'       => 1, // Ensure this is correct
        'fields'           => array( $name, $email ), // Add the fields here.
    ) );
 
    $form['fields'][] = $team;
 
    return $form;
}
 
// Remove the field before the form is saved. Adjust your form ID
add_filter( 'gform_form_update_meta_149', 'remove_my_field', 10, 3 );
function remove_my_field( $form_meta, $form_id, $meta_name ) {
 
    if ( $meta_name == 'display_meta' ) {
        // Remove the Repeater field: ID 1000
        $form_meta['fields'] = wp_list_filter( $form_meta['fields'], array( 'id' => 1000 ), 'NOT' );
    }
 
    return $form_meta;
}

The filter on line #2 takes the results of the add_my_field() function and passes it to the gform_form_post_get_meta_{form_id}, which is executed when the form meta is retrieved ({form_id} should be replaced by the id of the form that you want to add the repeater on). The problem here is that with that function alone, every time that you edit your form’s fields from the backend and hit “save”, a new repeater field gets created.

(this could go on forever)

That’s why we have the second function of the example: remove_my_field() removes the field before the form is saved while add_my_field() adds it, guaranteeing that way that the field gets added only once. Problem solved? – Well, not so fast. Removing the existing reapeater field and then creating it again prevents you from moving it wherever you like. So, unless you are OK by always having your repeater field last, this example would cause you trouble. As you can see on the following gif, using the remove_my_field() function prevents the field-copying issue, but now you can’t reorder the fields.

(every time you reorder the repeater field, hitting “update” will drag it back at the end of the form).

So, let see how we could deal with those issues and do some more refactoring of the form along the way. First, we create our function based on the official example:

$form_id = 3; 
add_filter( 'gform_form_post_get_meta_' . $form_id, 'add_repeater_field' ); 
function add_repeater_field( $form ) { 

  $fields = [];
  $repeater = GF_Fields::create(
    [
      'type'             => 'repeater',
      'description'      => __( 'Maximum of 3 team members - set by the maxItems property', 'theme-prefix' ),
      'id'               => $field_id,
      'formId'           => $form['id'],
      'label'            => __( 'Team Members', 'theme-prefix' ),
      'addButtonText'    => __( 'Add team member', 'theme-prefix' ),
      'removeButtonText' => __( 'Remove team member', 'theme-prefix' ),
      'maxItems'         => 5,
      'pageNumber'       => 1,
      'fields'           => $fields,
    ]
  );

  return $form; 
}

As you can see, I prefer having the form ID on a variable called $form_id. This is more convenient because you might need to use the same ID elsewhere on your code and it makes reading the code easier. In the end, though, it is a matter of personal preference. Then, We have to call the repeater field and return the form. The only actual difference compared to the official example is that instead of creating the fields as separate objects, we put them inside a $fields array. That way, everything inside the $fields array will be included on our repeater automatically. So, let’s add some fields:

$form_id = 3; 
add_filter( 'gform_form_post_get_meta_' . $form_id, 'add_repeater_field' ); 
function add_repeater_field( $form ) { 
  
  $field_id = 1000;

  $fields = [
    GF_Fields::create(
        [
          'type'       => 'text',
          'id'         => $field_id += 1,
          'formId'     => $form['id'],
          'label'      => __( 'Field 1', 'theme-prefix' ),
          'pageNumber' => 1,
        ]
      ),
      GF_Fields::create(
        [
          'type'       => 'text',
          'id'         => $field_id += 1,
          'formId'     => $form['id'],
          'label'      => __( 'Field 2', 'theme-prefix' ),
          'pageNumber' => 1,
        ]
      ),
      GF_Fields::create(
      [
        'type'       => 'select',
        'id'         => $field_id += 1,
        'formId'     => $form['id'],
        'label'      => __( 'Field 3 (dropdown)', 'theme-prefix' ),
        'choices'    => [
          [
            'text'       => 'Option 1',
            'value'      => 'option_1',
            'isSelected' => true,
          ],
          [
            'text'       => 'Option 2',
            'value'      => 'option_2',
            'isSelected' => false,
          ],
          [
            'text'       => 'Option 3',
            'value'      => 'option_3',
            'isSelected' => false,
          ],
        ],
        'pageNumber' => 2,
      ]
    ),
  ];
  $repeater = GF_Fields::create(
    [
      'type'             => 'repeater',
      'description'      => __( 'Maximum of 3 team members - set by the maxItems property', 'theme-prefix' ),
      'id'               => $field_id,
      'formId'           => $form['id'],
      'label'            => __( 'Team Members', 'theme-prefix' ),
      'addButtonText'    => __( 'Add team member', 'theme-prefix' ),
      'removeButtonText' => __( 'Remove team member', 'theme-prefix' ),
      'maxItems'         => 5,
      'pageNumber'       => 1,
      'fields'           => $fields,
    ]
  );

  return $form; 
}

One thing that I didn’t like on the official example was that you had to manually add each field’s id by hand. So, to avoid that, we create a $field_id variable and we give it a high enough number (like 1000). Then, on each field, we call $field_id +=1 to increment that number. As a bonus, along with the text field I’ve included an example of a dropdown.

What we need to do now is to check if the repeater field already exists on the form. If it isn’t, we add it, otherwise we leave the form intact. We do so by adding the following lines:

$repeater_exists = false;
foreach ( $form['fields'] as $field ) {
  if ( 'repeater' === $field->type && $field->id === $field_id ) {
    $repeater_exists = true;
  }
}
if ( ! $repeater_exists ) {
  $form['fields'][] = $repeater;
}

The first line creates a $repeater_exists variable which checks for the existence of the fields and is by default set to false. Then, we use a foreach loop to see if a repeater field with the same id already exists on our form. If it does, we set the variable’s value to true. When it all ends, we check the value of the variable and if it isn’t set to true (meaning that the repeater field does not exist on the form), we add our repeater field to the form.

Eventually, our complete code should look like that:

Bonus tip: One more annoying thing about Gravity Forms repeater field is that when the user hits the remove button to delete a field that they previously added, it pops-up an ugly JavaScript alert:

In principle, this isn’t bad. On the contrary, it is a good practice to warn users when they are about to delete something. This alert, though, is ugly, not customizable and not translatable. Also, currently there isn’t a hook or filter to remove or modify it. So, if you want to remove it you will have to add the following jQuery code somewhere on your custom scripts:

$(document).on('click', '.add_repeater_item', function () {
  $('.remove_repeater_item').attr('onclick', 'gformDeleteRepeaterItem(this)');
});

What it does it that every time the user adds a new field (and, therefore, the need to remove it appears), it will modify the remove button’s onclick event and remove the alert.

 

So far, Gravity Forms’ native repeater field, even on its current beta state, already is a powerful tool, but it is more or less an uncharted territory, with very few documentation available. If you have any experience with it and came up with tips of your own, I would be happy to hear about it.

19 thoughts on “Refactoring the Gravity Forms native repeater field

  1. Great description. One issue that I found out is. I am using a datepicker in a form and if i use this repeater before datepicker then datepicker has issue with popup. I have checked console, there are no any errors there. Can you check once.

  2. Thanks for the tutorial.

    I’m trying to modify lables and input fields styles, to match my overall form, but it seems they ignore any custom css.
    Any ideas?

  3. Thanks for the code.
    Quick note, if you use the
    ‘id’ => $field_id += 1,

    Then you CANNOT make the fields required.
    To make required, you must set the ‘id’ to a unique number.
    Then add this:
    ‘isRequired’ => true,

    1. Adding +=1 to the id is exactly for that, to add an extra increment to the id, in order to make it unique for each field. So the first field’s id will become 1001, the second 1002 etc.

  4. Hi James! I find your version great! But what if you have multiple repeaters in 1 form. How do you pass multiple parameters in add_filter? Thanks!

    1. You just create one function for each repeater and call it the same way using
      add_filter( 'gform_form_post_get_meta', 'my_repeater_1' );
      add_filter( 'gform_form_post_get_meta', 'my_repeater_2' );

      You only need to make sure that you set a different ID for each repeater. So, if the first one starts from 1000, the second one could start from 2000.

    1. Did you read the repeater page on Gravity Forms? It says this: “Note: This field type, released with Gravity Forms 2.4, is currently in BETA.” It won’t work with 2.3.3.

  5. Hi GIORGOS!
    Thank you for that. Very useful changes.
    I’m trying to get the values โ€‹โ€‹with javascript, but I can only get the value of the first field, not the others.
    Would you know how to do it?

    1. It depends on how you are trying to get them. For example, if you use document.querySelector(selectors), it will get only the first value, while querySelectorAll() would fetch all of them. You can read more about that on MDN.

  6. Brilliant work.

    One thing that was initially unclear is that we need to keep the “remove_my_field” function in the complete code. Otherwise… well things don’t work out so well.

  7. Hi Giorgos, I’m having some issues that my repeater entries aren’t showing in neither the backend or in the notification email. Any known issues with that on your side?

    1. Hi Christian,

      No, in my case the entries appear on the backend, like any other field. Which version of Gravity Forms are you using? I haven’t updated yet to Gravity Forms 2.5 to test it (I will probably do it in a few days). Is your issue occur with 2.5 specifically or with older versions as well?

  8. A little confusion here about positioning and removing the repeater with your code. In my case, when I open the form in the backend, there is another repeater instance at the bottom which I don’t seem to get rid of. Do I still need the remove_my_fields function provided by Gravity Forms on their site? If I remove it, GF adds a new instance of the repeater each time I reload the form in the backend. If I keep it in, it only creates one new instance at the bottom, but the first one at my preferred position stays put.

Leave a Reply

Your email address will not be published. Required fields are marked *