Repeaters and Complex jQuery Operations

While testing a variation of this code for our Hide Previously Selected Dynamic Checkboxes in Repeaters article, we discovered that the Safari browser has a broken jQuery implementation. The code as originally written here works in all browsers except Safari. We've developed a work around that is explained in the above referenced article and included in that article's source code.

Snapshot

Gold inifinity symbol dowloaded from pixabay.com
"To Infinity and Beyond"—Buzz Lightyear

Formidable Forms repeater fields are a very useful and needful tool, but implementing them with complex calculations that work consistently as rows are added can provide challenges.

Repeaters work great with the standard math calculations that are available through Formidable's shortcodes. Anything beyond standard shortcodes requires some advanced custom jQuery, an understanding of the Document Object Model (DOM), knowledge of multidimensional arrays, and lots of caffeine!

This tutorial should help you cut down on your caffeine intake by demonstrating how Formidable's repeater field works with jQuery calculations as new rows are added to the form.

This article came about because of a post by Howard Jennings on the old Formidable Community Forum. Mr. Jennings asked how he could hide certain dropdown options based on the value selected in another dropdown.

Because this requires manipulating CSS in the browser in real-time, it can't be done from the server in PHP, so jQuery is the answer. He provided some cobbled code to demonstrate his requirement. I refactored the code and reached out to him via email. The refactored code follows:

Challenge

After Mr. Jennings received the code and verified it worked well, he then asked how to apply it to each row in a repeater. This question has more challenges than solutions. After conducting some research, I couldn't find any clear examples online to share with him.

Mr. Jennings asked a good question though, and I didn't have an answer ready at hand. After thinking about it for awhile, I decided to learn how to do this myself and write this tutorial while I studied the problem

In this tutorial, you'll learn:

  • How repeaters work from both the Formidable and Document Object Model (DOM) perspectives
  • How to overcome repeater challenges in the DOM
  • Writing code that works
  • There's also a working example of a form with a repeater field for you to play with.

How Repeaters Work

Formidable Perspective

First, let's understand what repeaters are from a Formidable perspective. Architecturally, repeaters are embedded forms. Whenever a user clicks on the add or remove button to add or remove a row on the form, the repeater is actually adding or removing a form entry for the embedded form.

In this article, we'll sometimes reference the repeater as the child form. The parent form is the form that displays the repeater's content.

Repeaters consist of a form section that functions as the container for the child form. The form section is a permanent part of the parent form. It has its own field id and key.

The data capture fields you add to the repeater section are actually being added to the child form. They aren't part of the parent form, other than in appearance. So in essence, when you add a repeater to your form, you are actually building two forms at once. Maybe more, if you have multiple repeaters.

When you view a form's source code in your browser, the id of the child form is stored in a hidden field beneath the section container and section title:

Screen capture of a Formidable form's source code as viewed in the Chrome inspector.
A Formidable Form's Source Code as Displayed in Chrome's Inspector

Let's take a closer look at the source code below that is copied from the form at the end of this article. The hidden field is on line 3. The child form's id is 11 (value="11"). You would use this form id if you search for data directly in the database with phpMyAdmin. Other than being found in this hidden field, the child form ID is not referenced again in the source code. It's only used with Formidable's back-end functions

Examination of the source code reveals that repeater data rows appear as multidimensional arrays. A multidimensional array is an array containing one or more arrays. PHP understands multidimensional arrays that are two, three, four, five, or more levels deep. With our repeaters, each field in a repeater row is a multidimensional array nested 3-levels deep.

As an example, let's say I have a repeatable section with an id of 87. There is a text field in this repeater with an id of 89. The source code for this input field as displayed in the browser is:

Let's break this down so we can understand what we are seeing. The input type is standard for any text field. Next is the field's id as referenced by the DOM. The DOM id is different from the Formidable field id that you see when building a form. The id in the DOM is composed of the word "field" followed by an underscore "_" plus the Formidable field's key.

In the example above, the field key for field 89 is "97w00". Yet, you see there is also a "-0" appended to the end of the DOM id beginning with "field_97w00". What do you think this means?

DOM ids must be unique for all elements on a page. So, Formidable appends the repeater row id to the end of the DOM id to ensure its uniqueness.

Let's look at the input field name. This is the most critical attribute for us to understand. It lies at the heart of the jQuery function we're going to build. This is the reference to the multidimensional array that will be used on the back end. Each array element is wrapped in square brackets [].

The first dimension "[87]" is the link to the parent form. This is the field id of the repeatable section.

name="item_meta[87][0][89]"

The second dimension is the row id.

name="item_meta[87][0][89]"

The third dimension is the id of the field that stores the data.

name="item_meta[87][0][89]"

The first and third dimensions are static. They don't change as rows are added. The second dimension increments for every row that's added. Removing a row does not change row ids after they've been assigned in the DOM.

Arrays are always indexed from 0. The first row id is always 0 in an array. Now, if I add a row to the repeater, the text field on the new row is named:

name="item_meta[87][1][89]"

See how the row id increments?

The remaining properties for this input field are value, data-invmsg, and data-sectionid. Don't be concerned with these for now. They don't come into play with our jQuery function.

DOM Perspective

Repeaters are challenging to manipulate with jQuery because of how the DOM and jQuery work together. Normally, the first line in a jQuery function is:

jQuery(document).ready(function($) {})

Translated into English, this means, "Don't apply the functions in this script until the DOM is ready, then execute this code and bind it to whatever elements it affects."

A page can't be manipulated safely until the document is "ready." jQuery detects this state of readiness for you. Code included inside $( document ).ready() will only run once the DOM is ready for JavaScript code to execute.

When the DOM is loaded and ready for a new form's entry, additional repeater rows don't exist yet, only the first row does. Therefore, the jQuery function is only bound to the first row. The jQuery only runs once when the DOM has reached its ready state. You can't bind code to elements that don't exist yet.

Adding rows to a repeater does not change the DOM's ready state. Once the DOM is loaded, it's always ready until the page is refreshed and reloaded. Because new repeater rows are added through jQuery Ajax calls and Ajax does not reload the page, the DOM doesn't get reloaded. It remains in its initial "ready" state and any jQuery associated with document ready will not refire. This means that code meant to manipulate a repeater row, will only continue to work on the first row.

If the entire page was refreshed and the DOM reloaded, the code would rebind to the new rows assuming the jQuery function references the HTML elements with wild cards or variables instead of direct field names.

It's not possible to know from the onset how many rows a user may add to a repeater. So, we have to figure out some way of counting how many repeater rows we have so we can make them work with our jQuery function.

Understanding how this all fits together reveals that the greatest challenge with repeater rows can be phrased as, "If the page doesn't get refreshed, how do we get our jQuery to refire as rows are added to a repeater?"

Writing code that works

Just as jQuery can detect the DOM's ready state, it can also detect other global states and events. The event we're going to rely upon for new repeater rows is ajaxComplete(). This function fires globally whenever an Ajax call completes its duty. That's any Ajax call, whether it's the one that adds rows to the repeater or some other Ajax call that you built or some other plugin uses. And since it's global, we can bind code to it in document ready. But what code do we bind to it and how do we know if it's the repeater's Ajax that is firing?

We've already learned that code added to document ready fires when the DOM is loaded and ready to execute JavaScript. But that code only fires once. ajaxComplete() fires every time an Ajax call finishes its job.

We want the same code to run every time a form with a repeater is loaded and when a new row is added to a repeater. Following good coding practices, we would place this code into its own function so it can be executed upon document ready and ajaxComplete(). We'll call this function repeater_row_init(). As we begin to build our functions, the basic shell looks like this:

Since it's possible that we could build a form with multiple repeater sections, it's a good idea to pass a repeater section id to the init function so we know which repeater to initialize. It's also a good idea to pass the section ids in an array so we can initialize all of the repeaters on our form at one time by looping through the array. Now our repeater_row_init function looks like this:

If you'll notice when you read through the code, the repeater_row_init function is calling another function named repeater_section_87_init. This function is an element of the callbacks array. Here is how you create the callbacks array:

/* setup an array of repeater callback functions */
/* you'll want a different callback for each repeater section */
var callbacks = {
    repeater_section_87_init : function() {
         /* there's nothing in here yet */   
    }
};

Let's build out the callback. This is where we have to traverse the DOM and find the right repeater rows to which to apply our code. A big challenge here occurs when you delete a row from a repeater. Normally, rows are added in numerical sequence. The first row is always 0. If we add a new row, it's 1, then 2, then 3, etc.

If all we were doing were adding rows it's not difficult to construct a loop and run through it by incremental counter. However, lets say we delete row 2. Row ids don't adjust for this. Our row ids will have a gap. The 0, 1, 2, 3 now becomes 0, 1, 3. You can't depend on an incremental counter loop when there is a gap in the numbers.

Moreover, when you delete a row from a repeater, Ajax is not invoked. So, there's no ajaxComplete() event to hook into. It doesn't matter, though. We can safely ignore delete events and there's no impact to our code if we do. Our only concern then is adding new rows.

This is the source code from a repeater field where I've deleted a row in the middle. There are 2 rows remaining: 0 and 2.

As we examine this source, we have to find an element that will clue us into the actual row id so we can determine that second dimension in the field name. There's only one element in this source code that contains the row id and it repeats for every row:

<input type="hidden" name="item_meta[87][row_ids][]" value="2">

This means we have to loop through these hidden fields, store their values, and pass those values back to the repeater section callback init function. To loop through the hidden fields and return the rowids, I've created a helper function called get_row_ids().

Good coding practice teaches that functions should only do one thing. To make sure get_row_ids() works correctly across any repeater field, we pass the section id as a parameter. Now our call back code looks like this:

/* setup an array of repeater callback functions */
    /* you'll want a different callback for each repeater section */
    var callbacks = {
        repeater_section_87_init : function() {
            var section_id = "87",
                rowids = get_row_ids( section_id );
            
            /* now that we have the row ids for this section, we can apply the code to each of the correct elements */
            
        }
    };
    
    function get_row_ids( section_id ) {
        
        /* loop through the hidden fields that have the row_ids and push them into an array */
        var row_ids = [];
        $('input[name^="item_meta[' + section_id + '][row_ids]"]').each(function(index, el) {
             row_ids.push(el.value);
        });
        
        /* return the row_ids array to the calling init function */
        return row_ids;
    }

The final initialization step is applying the code we want to each of the newly added repeater rows. For this example, we'll use a version of Mr. Jennings refactored code.

The last thing we have to wrap up is hooking the init into the ajax.Complete() function. For this, we have to know which repeater section invoked the Ajax call so we can execute the correct init callback.

Since ajaxComplete() fires globally for all Ajax calls, there has to be a way for us to determine which element on our page invoked the call. This part is very technical and requires a much deeper explanation than I want to provide in this article, but suffice it to say that you can find the repeater row id in the ajax.Complete() "event" object. It's nested 5-levels deep and not easy to find. Rather than explaining it all here, here's the code tied to ajax.Complete():

/* initialize the repeater for every added row */
    $( document ).ajaxComplete( function( event, xhr, settings ) {

        var complete_event = jQuery.makeArray(event);
        var active_elem = complete_event[0]['target']['activeElement']['dataset']['parent'],
            repeater_section = [active_elem];

        if (active_elem == '87') {

            repeater_row_init( repeater_section );
        }

    });

});

The completed code as written for the demo form below is:

As with any other Formidable Forms custom jQuery scripts, insert your script into the After Fields section on your form's Customize HTML page.

Now, do you want to see it work?

Working Example

The Deductible dropdown as defined in the form has five values. When each row is initialized, you'll only see 3 options. If you select "Michigan" as the state, the Deductible dropdown will display 2 options. Choose any other state, and the options return to the original three. This will continue to work for all rows added to the repeater.

Repeater Section Calculations

Demo for Repeaters and Complex jQuery Operations

Repeater Section: id=137

Reader Interactions

Leave a Reply

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