Formidable Forms repeater fields are a great product feature. While the repeaters work fine with standard math calculations available through Formidable's shortcodes, more complex operations, such as we're addressing in this tutorial, require custom jQuery code. Getting the code to work correctly can be somewhat elusive though. This tutorial demonstrates how to get Formidable's repeater field to work with jQuery calculations as new rows are added to the form.
An example of why this is needful is posted by Howard Jennings through the Formidable Community Forum. Mr. Jennings asked how he could hide certain dropdown options based on the value selected in another dropdown.
This is something that needs to be done in jQuery because it requires manipulating CSS in the browser in real-time, which can't be done from the server in PHP. He provided some code that he had cobbled together to demonstrate his requirement. I refactored the code and reached out to him via email. Click here to access the pastebin with the refactored code.
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 an example repeater form for you to play with that does not save entries to the database.
How Repeaters Work
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:
Let's take a closer look at the source code below. The hidden field is on line 3. The child form's id is 6 (value="6"). 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
Further examination of the source code reveals that repeater data rows are 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 notated as a multidimensional array 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 "" is the link to the parent form. This is the field id of the repeatable section.
The second dimension is the row id.
The third dimension is the id of the field that stores the data.
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:
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.
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:
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."
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. And since it's global, we can bind code to it in document ready. But what code do we bind to it?
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:
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:
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:
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 from the pastebin modified for our example form.
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():
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?
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.