redux-framework icon indicating copy to clipboard operation
redux-framework copied to clipboard

Field dependencies not working after page refresh in repeater fields

Open blackandcode opened this issue 6 months ago • 5 comments

Steps to reproduce

  1. Create a Redux configuration with a repeater field containing switch fields with required dependencies
  2. Add the following field structure inside a repeater:
    array(
        'type' => 'repeater',
        'fields' => array(
            array(
                'id' => 'show_link',
                'type' => 'switch',
                'title' => 'Show Link',
                'default' => '0',
            ),
            array(
                'id' => 'absolute_or_relative_url',
                'type' => 'switch',
                'title' => 'Absolute URL',
                'default' => '0',
                'required' => array('show_link', '=', '1'),
                'force_output' => true,
            ),
            array(
                'id' => 'link_to_page',
                'type' => 'select',
                'data' => 'pages',
                'required' => array(
                    array('show_link', '=', '1'),
                    array('absolute_or_relative_url', '=', '1'),
                ),
                'force_output' => true,
            ),
            array(
                'id' => 'link_to_absolute_url',
                'type' => 'text',
                'required' => array(
                    array('show_link', '=', '1'),
                    array('absolute_or_relative_url', '=', '0'),
                ),
                'force_output' => true,
            ),
        ),
    )
    
  3. Save the Redux configuration and navigate to the admin page
  4. Enable the first switch (show_link) - dependent fields appear correctly
  5. Configure the dependent fields and save
  6. Refresh the page or navigate away and return

Expected Behavior

  • After page refresh, dependent fields should remain visible when their parent conditions are met
  • Field visibility should be restored based on saved values
  • The same behavior should work consistently both inside and outside repeater fields

Actual Behavior

  • Outside repeater: Fields work correctly - dependent fields remain visible after page refresh when conditions are met
  • Inside repeater: Dependent fields become hidden after page refresh, even when their parent conditions are satisfied and values are saved correctly
  • Field dependencies work on first interaction but fail to restore visibility state after page reload

Any Error Details (PHP/JavaScript)

No PHP errors are generated. JavaScript console shows no errors in dev mode.

Additional Context

Working Configuration (Outside Repeater)

The identical field configuration works perfectly when placed outside of a repeater field. Fields remain visible after page refresh when their conditions are met.

Attempted Solutions

Tried multiple approaches that should work according to Redux documentation:

  • Using force_output => true on dependent fields
  • String values ('1', '0') instead of integers for comparisons
  • Nested required arrays: array(array('field1', '=', '1'), array('field2', '=', '0'))
  • Single-level required arrays: array('field1', '=', '1')
  • Various comparison operators (=, equals, !=)

Environment

  • Redux Framework latest
  • WordPress latest version
  • PHP 8.3+
  • Modern browsers (Chrome, Firefox, Safari) - issue occurs in all

Impact

This appears to be a fundamental issue with how Redux handles field dependencies within repeater contexts after page initialization. The field dependency JavaScript logic seems to not properly restore field visibility states for repeater field children when the page loads with existing saved values.

Suggested Investigation Areas

  1. Redux JavaScript initialization sequence for repeater fields
  2. Field dependency state restoration logic in repeater contexts
  3. DOM event binding for dependent fields within dynamically generated repeater items
  4. Potential timing issues between repeater field rendering and dependency evaluation

This bug significantly impacts the usability of complex repeater configurations that rely on conditional field visibility.

blackandcode avatar Jul 29 '25 13:07 blackandcode

Technical Details

  • Issue: Field dependencies worked on first interaction but failed to restore visibility state after page reload within repeater contexts. Additionally, multi-level dependency chains (A → B → C → D) were broken after the first level.
  • Root Cause:
    1. Dependency checks were happening before repeater field values were properly restored from saved data during page initialization
    2. When first-level dependencies were resolved, second and deeper level dependencies weren't being re-evaluated
  • Solution:
    1. Added delayed dependency checks after repeater initialization completes
    2. Implemented recursive dependency evaluation to handle multi-level chains
    3. Enhanced accordion panel activation to trigger dependency evaluation
    4. Added change detection to continue checking until all dependency levels are resolved

Files Modified

  • redux-core/inc/extensions/repeater/repeater/redux-repeater.js
    • Added checkAllDependenciesRecursive() function with iterative dependency resolution
    • Modified repeater initialization to use recursive dependency checking (100ms delay)
    • Enhanced accordion activate callback to recursively check dependencies when panels open (50ms delay)
    • Added recursive check trigger when fields become visible to evaluate nested dependencies (50ms delay)
    • Implemented change detection and loop prevention (max 5 iterations)

Behavior Changes

  • Before:
    • Level 1 dependencies: Became hidden after page refresh regardless of saved values
    • Level 2+ dependencies: Even when Level 1 was fixed, deeper levels remained broken
  • After:
    • All dependency levels properly restore visibility based on saved values
    • Complex nested dependency chains work consistently
    • Behavior is consistent with non-repeater field dependencies

Multi-Level Dependency Support

The fix now properly handles complex dependency chains such as:

  • Level 1: show_link (switch) = 1
  • Level 2: link_type (select) = 'internal' (requires Level 1)
  • Level 3: internal_link_type (select) = 'page' (requires Level 1 + 2)
  • Level 4: link_to_page (page select) (requires Level 1 + 2 + 3)

Testing

  • ✅ Simple 2-level dependencies work correctly
  • ✅ Complex 4+ level dependencies work correctly
  • ✅ Page refresh maintains proper field visibility states for all levels
  • ✅ New repeater items continue to work correctly
  • ✅ Accordion panel switching preserves all dependency chain states
  • ✅ Dynamic field changes properly cascade through all dependency levels

Backward Compatibility

  • ✅ Fully backward compatible
  • ✅ No configuration changes required
  • ✅ No API changes
  • ✅ Existing functionality preserved

Performance Impact

  • ✅ Minimal performance impact (two small setTimeout delays)
  • ✅ Only affects repeater fields with dependencies
  • ✅ No impact on fields without dependencies

blackandcode avatar Jul 29 '25 13:07 blackandcode

Here you can find a redux-repeater.js file where you can see that problem is solved. And here is also Redux config so you can test it easier. https://pastes.io/redux-js-repeater-fixed

Here is array for testing the configuration:

`

// Redux configuration for testing repeater dependencies

Redux::set_section( 'your_opt_name', array(

'title'  => 'Repeater Dependencies Test',
'id'     => 'repeater_test',
'desc'   => 'Test section for reproducing repeater field dependency issues',
'fields' => array(

    array(
        'id'         => 'test_repeater',
        'type'       => 'repeater',
        'title'      => 'Test Repeater with Multi-Level Dependencies',
        'subtitle'   => 'This demonstrates nested dependency chains within repeaters',
        'item_name'  => 'Item',
        'sortable'   => true,
        'active'     => false,
        'collapsible' => false,
        'fields'     => array(
            // Level 1: Main switch
            array(
                'id'      => 'show_link',
                'type'    => 'switch',
                'title'   => 'Show Link (Level 1)',
                'default' => '0',
            ),
            // Level 2: Depends on Level 1
            array(
                'id'           => 'link_type',
                'type'         => 'select',
                'title'        => 'Link Type (Level 2)',
                'options'      => array(
                    'internal' => 'Internal Link',
                    'external' => 'External Link',
                    'file'     => 'File Download',
                ),
                'default'      => 'internal',
                'required'     => array('show_link', '=', '1'),

            ),
            // Level 3: Depends on Level 2 being 'internal'
            array(
                'id'           => 'internal_link_type',
                'type'         => 'select',
                'title'        => 'Internal Link Type (Level 3)',
                'options'      => array(
                    'page' => 'Link to Page',
                    'post' => 'Link to Post',
                    'custom' => 'Custom URL',
                ),
                'default'      => 'page',
                'required'     => array(
                    array('show_link', '=', '1'),
                    array('link_type', '=', 'internal'),
                ),

            ),
            // Level 4: Depends on Level 3 being 'page'
            array(
                'id'           => 'link_to_page',
                'type'         => 'select',
                'title'        => 'Select Page (Level 4)',
                'data'         => 'pages',
                'required'     => array(
                    array('show_link', '=', '1'),
                    array('link_type', '=', 'internal'),
                    array('internal_link_type', '=', 'page'),
                ),

            ),
            // Level 4 Alternative: Depends on Level 3 being 'post'
            array(
                'id'           => 'link_to_post',
                'type'         => 'select',
                'title'        => 'Select Post (Level 4)',
                'data'         => 'posts',
                'required'     => array(
                    array('show_link', '=', '1'),
                    array('link_type', '=', 'internal'),
                    array('internal_link_type', '=', 'post'),
                ),

            ),
            // Level 4 Alternative: Depends on Level 3 being 'custom'
            array(
                'id'           => 'custom_url',
                'type'         => 'text',
                'title'        => 'Custom URL (Level 4)',
                'required'     => array(
                    array('show_link', '=', '1'),
                    array('link_type', '=', 'internal'),
                    array('internal_link_type', '=', 'custom'),
                ),

            ),
            // Level 3 Alternative: Depends on Level 2 being 'external'
            array(
                'id'           => 'external_url',
                'type'         => 'text',
                'title'        => 'External URL (Level 3)',
                'required'     => array(
                    array('show_link', '=', '1'),
                    array('link_type', '=', 'external'),
                ),

            ),
            // Level 4: Depends on external URL
            array(
                'id'           => 'open_in_new_tab',
                'type'         => 'switch',
                'title'        => 'Open in New Tab (Level 4)',
                'default'      => '1',
                'required'     => array(
                    array('show_link', '=', '1'),
                    array('link_type', '=', 'external'),
                ),

            ),
            // Level 3 Alternative: Depends on Level 2 being 'file'
            array(
                'id'           => 'file_upload',
                'type'         => 'media',
                'title'        => 'File to Download (Level 3)',
                'required'     => array(
                    array('show_link', '=', '1'),
                    array('link_type', '=', 'file'),
                ),

            ),
        ),
    ),
)

) ); `

blackandcode avatar Jul 29 '25 13:07 blackandcode

I created a pull request also. https://github.com/reduxframework/redux-framework/pull/4073/commits/85580837bf9d88ecdba6edf7436ec1a438d3aa9b

blackandcode avatar Jul 29 '25 14:07 blackandcode

The last change I made in redux-repeater.js fixed the issue visually, but unfortunately introduced significant performance problems. Because of this, the solution I proposed isn’t ideal, and it’s possible that this isn’t even the right area to address the root cause.

Someone with deeper knowledge of this plugin might be able to find a better or more efficient solution.

Here is another solution

NEW FIX

The issue was in the JavaScript initialization sequence and how Redux evaluates field dependencies on page load:

  1. The main Redux initialization calls checkRequired() on the container during document ready
  2. This triggers dependency checks based on the initial DOM state
  3. However, repeater fields might not be fully populated with their saved values at this point
  4. The dependency system relies on change events to trigger dependency evaluation, but these events aren't fired for saved values on page load
  5. Multi-level dependency issue: When dependency chains exist (A → B → C), if A doesn't trigger its change event, B stays hidden, so C is never evaluated
  6. As a result, dependent fields appear hidden because the dependency chain evaluation is never initiated with the saved values

Fix Implementation - Efficient Event-Based Solution

File Modified: /redux-core/inc/extensions/repeater/repeater/redux-repeater.js

Change 1: Added efficient dependency chain trigger function

// Helper function to trigger dependency evaluation efficiently
redux.field_objects.repeater.triggerDependencyChain = function( container ) {
    const maxRounds = 3; // Maximum rounds to handle deep nesting
    let round = 0;
    
    function triggerRound() {
        if ( round >= maxRounds ) {
            return;
        }
        
        round++;
        let triggeredAny = false;
        
        // Trigger change events on fields with values
        container.find( '.redux-field select, .redux-field input[type=radio]:checked, .redux-field input[type=checkbox], .redux-field input[type=hidden]' ).each( function() {
            const field = $( this );
            if ( field.hasClass( 'in-repeater' ) ) {
                const value = field.val();
                if ( value && value !== '' && value !== '0' && value !== 'false' ) {
                    field.trigger( 'change' );
                    triggeredAny = true;
                }
            }
        });
        
        // Handle switch fields specifically
        container.find( '.redux-field input[type=hidden]' ).each( function() {
            const hiddenField = $( this );
            if ( hiddenField.hasClass( 'in-repeater' ) && hiddenField.attr( 'name' ) && hiddenField.attr( 'name' ).indexOf( '[' ) > -1 ) {
                const value = hiddenField.val();
                if ( value === '1' || value === 'true' ) {
                    hiddenField.trigger( 'change' );
                    triggeredAny = true;
                }
            }
        });
        
        // If we triggered any changes, schedule another round to catch dependencies of newly shown fields
        if ( triggeredAny && round < maxRounds ) {
            setTimeout( triggerRound, 100 );
        }
    }
    
    triggerRound();
};

Change 2: Updated repeater initialization to use efficient chain evaluation

// Use efficient dependency chain evaluation instead of performance-heavy recursive checking
setTimeout( function() {
    redux.field_objects.repeater.triggerDependencyChain( el );
}, 150 );

Change 3: Enhanced accordion activate callback with efficient evaluation

// Use efficient dependency evaluation for newly activated panel
if ( ui.newPanel && ui.newPanel.length ) {
    setTimeout( function() {
        redux.field_objects.repeater.triggerDependencyChain( ui.newPanel );
    }, 100 );
}

How the Fix Works

  1. Event-Based Approach: Instead of directly manipulating field visibility, the fix triggers the native Redux change events on fields that have saved values.

  2. Multi-Round Evaluation: The triggerDependencyChain function runs up to 3 rounds of evaluation:

    • Round 1: Triggers change events on all fields with values
    • Round 2: Triggers change events on newly shown fields (dependencies of Round 1)
    • Round 3: Handles any final level dependencies
  3. Smart Value Detection: Only triggers change events on fields that have meaningful values (not empty, not '0', not 'false').

  4. Switch Field Handling: Special logic for hidden input fields used by switch components.

  5. Conditional Continuation: Only continues to the next round if the previous round actually triggered any changes.

  6. Performance Optimized:

    • Maximum 3 rounds prevents infinite loops
    • Only runs when fields have values
    • Uses native Redux event system instead of custom logic

Multi-Level Dependency Example

The enhanced fix now properly handles complex dependency chains like:

// Level 1: Main switch
'show_link' => switch field (triggers Round 1)

// Level 2: Depends on Level 1  
'link_type' => select field (becomes visible in Round 1, triggers in Round 2)

// Level 3: Depends on Level 2
'internal_link_type' => select field (becomes visible in Round 2, triggers in Round 3)

// Level 4: Depends on Level 3
'link_to_page' => page select (becomes visible in Round 3)

Performance Benefits

  • Highly Efficient: Uses native Redux event system instead of custom loops
  • Minimal Overhead: Maximum 3 rounds, only when needed
  • Smart Triggering: Only triggers events on fields with actual values
  • No Performance Issues: Eliminates the recursive approach that caused performance problems
  • Scalable: Works efficiently regardless of form complexity

This solution addresses the core issue efficiently by working with Redux's native dependency system rather than fighting against it, resulting in a clean, performant fix that handles multi-level dependencies properly.

blackandcode avatar Jul 29 '25 16:07 blackandcode

I want to check why this is still not patched. I'm using this patch for 5 months and Redux is working fine with this. Nothing is breaking . I would appriciate feedback on this.

blackandcode avatar Dec 11 '25 14:12 blackandcode

@blackandcode - Currently, my supervisors have Redux in a maintenance and bug fix only pattern. Your pull request would require several hours of testing to ensure that new issues are not introduced into the ecosystem and affect millions of users. It's not as simple as making sure the repeater works. Each field we offer must also be tested within, and then the same config applied to metaboxes and tabs, and also with multiple versions of PHP and WP.

Then, if I find issues, I either have to report them back to you or fix them myself, and my bosses would prefer I spend those resources on other projects.

kprovance avatar Dec 12 '25 07:12 kprovance