Pro Forms Date field inside an ACF-populated Repeater renders two visible inputs

Bug report — Pro Forms Date field inside an ACF-populated Repeater renders two visible inputs

Submitted by: HHH Consulting
Component: Pro Forms (nestable) — Repeater + Date field
Severity: Medium — the control still works, but a second empty date input is shown under it and the console throws
Type: Front-end JS / Flatpickr re-init


Environment

Bricksforge 4.0.0
Bricks 2.4
WordPress 7.1.1
PHP 8.2.33
ACF PRO 6.8.10
Browser Chromium (reproduced independently of layout CSS)
Theme Bricks Child Theme 1.1

Observed on a live edit form whose Repeater dataSource is ACF. The extra input is not present on Date fields outside a Repeater, and not present on Date fields inside a Repeater that has no ACF rows to populate.


Summary

A Pro Forms Date field is a single Bricks element. Flatpickr is configured with altInput: true, which is correct: one hidden original input plus one visible calendar input.

When that Date field sits inside a Repeater and the Repeater is populated from ACF (dataSource: acf with existing rows), handleDatePickers() runs again after populateRepeaterData. It initialises Flatpickr a second time without destroying the previous instance, so a second visible altInput is left in the DOM.

The first visible input opens the picker. The second is empty and does nothing when clicked.

A console error is thrown on the same pass, because the second loop also matches leftover .flatpickr-input nodes that have no data-bricks-datepicker-options.


Steps to reproduce

A relationship / post-object sibling field is not required. Any ACF repeater with a date subfield is enough.

  1. In ACF, create a Repeater on a post type. Give it one Date Picker subfield (name e.g. moved_in).
  2. Save at least one post with two repeater rows and dates filled in.
  3. In Bricks, add a Pro Form.
  4. Add a Repeater field:
    • Custom ID matching the ACF repeater name (or fieldName set to that name)
    • dataSource = ACF
    • postId = the post from step 2
  5. Inside the Repeater, add one Date field whose Custom ID matches the date subfield name. Leave Flatpickr defaults (altInput on).
  6. View the page on the front end for that post.

Expected: each Date field shows one visible calendar input.

Actual: each populated row shows two visible calendar inputs stacked. Only the first opens the picker. The second is empty and inert.

Repeat the same form with no ACF rows (or a Date field outside the Repeater): only one visible input. That is the control case.


Evidence

Rendered markup — Date inside the populated Repeater

One Bricks Date element (data-element-id="nqtllc", custom ID moved_in). After ACF populate it contains three inputs: the hidden original, a working alt input, and a leftover empty alt input.

<div class="brxe-nqtllc brxe-brf-pro-forms-field-date pro-forms-builder-field form-group"
     data-element-id="nqtllc"
     data-custom-id="moved_in">
  <label for="form-field-f67d88-0">Date</label>
  <input type="hidden"
         id="form-field-f67d88-0"
         name="brfr[addresss][0][moved_in]"
         class="flatpickr flatpickr-input"
         data-bricks-datepicker-options="{...,&quot;altInput&quot;:true,&quot;altFormat&quot;:&quot;d\/m\/Y&quot;,&quot;dateFormat&quot;:&quot;Y-m-d&quot;,...}"
         value="2005-06-01">
  <input class="flatpickr form-control input" placeholder="" tabindex="0" type="text" readonly="readonly">
  <input class="flatpickr form-control input" placeholder="" tabindex="0" type="text" readonly="readonly">
</div>

The first visible input holds the formatted date and is the live altInput. The second is empty, has no _flatpickr instance, and does not open a calendar.

The same extra empty input appears on Moved Out in the same rows.

Control — Date outside the Repeater (same form, same page)

<div class="brxe-iuwgqp brxe-brf-pro-forms-field-date pro-forms-builder-field form-group"
     data-custom-id="date_of_birth">
  <label for="form-field-343f23">Dob</label>
  <input type="hidden" class="flatpickr flatpickr-input" name="form-field-date_of_birth" value="1972-11-07">
  <input class="flatpickr form-control input" placeholder="" tabindex="0" type="text" readonly="readonly">
</div>

Hidden original + one visible alt input. Correct.

Date fields inside empty Repeaters on the same form (no ACF rows) also render correctly as 1 + 1.

Screenshot

See bricksforge-repeater-datepicker-duplicate.png in this folder. Each of Date (Moved In) and Date (Moved Out) has a filled input with a second empty input directly under it.

Console

TypeError: Cannot set properties of undefined (setting 'onChange')
    at bricksforge_elements.js:1:92071
    at NodeList.forEach (<anonymous>)
    at BrfProForms.handleDatePickers (bricksforge_elements.js)
    at BrfProForms.updateRepeaterItemIndexes (bricksforge_elements.js)
    at BrfProForms.syncRepeaterFields (bricksforge_elements.js)
    at BrfProForms.populateRepeaterData (bricksforge_elements.js)
    at BrfProForms.handleRepeaters (bricksforge_elements.js)

Call path: handleRepeaters → populateRepeaterData → syncRepeaterFields → updateRepeaterItemIndexes → handleDatePickers.


Cause

In bricksforge/assets/js/bricksforge_elements.js (4.0.0), handleDatePickers() initialises Flatpickr twice in one call, and again whenever repeater indexes are updated:

  1. It runs flatpickr() on .flatpickr nodes that have data-bricks-datepicker-options.
  2. On the front end it then queries all .flatpickr-input nodes and runs flatpickr() again.

The second pass does not:

  • skip nodes that already have element._flatpickr
  • destroy() the existing instance (which would remove the previous altInput)
  • skip nodes that have no data-bricks-datepicker-options

altInput: true means each successful flatpickr() inserts a new visible input. A second init therefore leaves the old visible input in place.

The TypeError is the same function: leftover alt inputs also match .flatpickr-input but have no options object, so t is undefined when the code does t.onChange = ….

This is independent of other subfield types in the Repeater. Changing a sibling subfield from text to a post-object / relationship does not create the extra Date input. The extra nodes are Flatpickr altInput elements, created in JS after ACF populate.


Suggested fix

In handleDatePickers, only initialise original Date inputs, and only once:

// skip leftover alt inputs and already-live instances
if (!e.dataset.bricksDatepickerOptions) return;
if (e._flatpickr) {
  e._flatpickr.destroy();
}
const t = JSON.parse(e.dataset.bricksDatepickerOptions);
t.onChange = /* existing handler */;
flatpickr(e, t);

destroy() removes the previous altInput before a re-init, which is what updateRepeaterItemIndexes needs after cloning/indexing rows.

Alternatively, keep a single init path (the first .flatpickr loop already does the job) and do not query .flatpickr-input again on the front end.


What this is not

  • Not a duplicate Date element in the Bricks tree. There is one Date field per label.
  • Not caused by a sibling Select / post-object / relationship field in the same Repeater row.
  • Not caused by theme or form CSS. The second <input class="flatpickr form-control input"> is in the DOM.

Happy to test a patch on a staging install.

Hi Pete,

thanks for the very thorough report, that made it easy to trace. I can confirm the bug. The symptom is exactly as you describe, the mechanism is slightly different.

The extra input does not come from a second flatpickr init on the same element. When flatpickr is initialised again on an input, it destroys the previous instance first and removes its alt input, so that part is clean. What actually happens: when the repeater is populated from ACF, the row template is cloned once per row. The manual “Add row” path cleans that clone up first (it destroys the flatpickr instance and removes the extra inputs flatpickr had created), but the ACF populate path does not. And because the datepicker init runs for the whole form every time repeater indexes are updated, the template row is usually already a datepicker at that point. So the clone carries the old visible alt input along as a dead node, and the fresh init then adds a working alt input in front of it. That is precisely the markup you posted: hidden original, working alt input, empty inert alt input. The console error has the same origin: the cloned hidden input already carries the flatpickr-input class, so the new alt input inherits it, and our second frontend pass then picks up an input without options.

The fix is to give the ACF populate path the same cleanup the “Add row” path already has, and to make that second pass skip inputs without options. I’ll post here once it’s in a release.

Until then it’s cosmetic: the first visible input is the live picker, and the hidden original is what gets submitted, so the values are correct.

Best,
Daniele

Just to be sure I have it right, are you saying the problem is ACFs problem? If so, is the bug report enough
to ask them to fix it?

Hi Pete,

no, this is not an ACF problem, so there is nothing to report to them. ACF only hands us the saved rows. Everything that goes wrong afterwards happens in our own JavaScript: when the repeater is filled from that data, Bricksforge clones its row template once per row, and that clone path skips the datepicker cleanup the manual “Add row” path already does. That is where the dead second input and the console error come from.

So the fix is entirely on our side, and I will post here once it is in a release. Until then the first visible input is the live picker and the submitted values are correct.

Best,
Daniele