Search code examples
phpcsswoocommercecheckoutcustom-fields

Add multiple custom checkout fields grouped in two columns on WooCommerce


I added custom code to generate custom checkout fields based on whether a custom has one or two products in their cart. For each product, 4 custom checkout fields are needed and added. They need to be displayed in two columns, side-by-side.

The desired end result is:

col1 col2
A1 A2
B1 B2
C1 C2
D1 D2

Currently, the script below generates this:

col1 col2
A1 B1
C1 D1
A2 B2
C2 D2
function add_custom_checkout_fields_for_product_1234() {
    global $woocommerce;
    
    // Get the current cart.
    $cart = WC()->cart;

    // Product ID to check for.
    $product_id_to_check = 5027;

    // Calculate the total quantity of the specified product in the cart.
    $total_quantity = 0;

    foreach ( $cart->get_cart() as $cart_item_key => $cart_item ) {
        if ( $cart_item['product_id'] == $product_id_to_check ) {
            $total_quantity += $cart_item['quantity'];
        }
    }

    // If the product with ID 1234 is in the cart, add the custom fields based on quantity.
    if ( $total_quantity > 0 ) {
        echo '<div id="custom_checkout_fields">
            <h3>' . __('Custom Fields', 'woocommerce') . '</h3>
            <p>' . __('Enter your custom information:', 'woocommerce') . '</p>
            <div class="custom-fields-container">';

        // Loop to display custom fields based on quantity.
        $custom_field_labels = array('A1', 'B1', 'C1', 'D1');
        $second_batch_labels = array('A2', 'B2', 'C2', 'D2');
        
        for ( $i = 1; $i <= $total_quantity; $i++ ) {
            // Determine which set of labels to use based on even or odd iteration.
            $labels = ($i % 2 == 0) ? $second_batch_labels : $custom_field_labels;
            
            foreach ($labels as $label) {
                echo '<div class="custom-field">
                    <label for="custom_field_' . $i . $label . '">' . __('Custom Field', 'woocommerce') . ' ' . $label . ' ' . $i . '</label>
                    <input type="text" class="input-text" name="custom_field_' . $i . $label . '" id="custom_field_' . $i . $label . '" />
                </div>';
            }
        }

        echo '</div></div>';
    }
}
add_action('woocommerce_before_checkout_form', 'add_custom_checkout_fields_for_product_1234');

// Validate and save the custom field data.
function validate_and_save_custom_checkout_fields($posted_data) {
    // Loop through the posted data to validate custom fields.
    for ( $i = 1; $i <= $total_quantity; $i++ ) {
        foreach ($custom_field_labels as $label) {
            $field_name = 'custom_field_' . $i . $label;
            
            if ( empty( $posted_data[$field_name] ) ) {
                wc_add_notice( __('Please fill in all custom fields.', 'woocommerce'), 'error' );
            }
        }
    }

    // Return the posted data.
    return $posted_data;
}
add_filter('woocommerce_checkout_posted_data', 'validate_and_save_custom_checkout_fields');


The additional CSS used for this table:

.custom-fields-container {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
}

/* Style custom field columns */
.custom-field {
    flex-basis: calc(50% - 10px); /* Adjust width as needed */
    margin-bottom: 10px;
    box-sizing: border-box;
}

Solution

  • First, you can't use woocommerce_before_checkout_form hook to add custom checkout fields, as the fields will be outside the checkout form (the hook name is explicit).

    Instead, you can use woocommerce_checkout_before_customer_details hook to add checkout fields.

    Note: The code, doesn't need any CSS (styling) as I use WooCommerce checkout form fields.

    The fields will be displayed In 2 columns, just like in your desired result:

    enter image description here

    The fields will be validated and saved to the database on submit.

    // Utility function: Get specific product total cart item quantity
    function get_specific_cart_item_quantity() {
        $targeted_id = 5027; // <== Product ID to check for.
        $item_qty    = 0; // Initializing
    
        // Loop through cart items
        foreach ( WC()->cart->get_cart() as $item ) {
            if ( $item['product_id'] == $targeted_id ) {
                $item_qty += $item['quantity'];
            }
        }
        return $item_qty;
    }
    
    // Add custom checkout fields
    add_action('woocommerce_checkout_before_customer_details', 'add_custom_checkout_fields');
    function add_custom_checkout_fields() {
        $item_qty = get_specific_cart_item_quantity();
        
        if ( $item_qty ) {
            $domain  = 'woocommerce';
    
            echo '<div id="custom_checkout_fields">
                <h3>' . __('Custom Fields', $domain) . '</h3>
                <p>' . __('Enter your custom information:', $domain) . '</p>
                <div class="custom-fields-container">';
            
            $qty1 = $item_qty % 2 == 0 ? $item_qty / 2 : ( $item_qty == 1 ? 1 : ($item_qty + 1) / 2 );
            $qty2 = $item_qty == 1 ? 1 : 2;
    
            // Multi-loop to display custom fields based on quantity in 2 columns
            for ( $k = 1; $k <= $qty1; $k++ ) {
                $qty2 = $item_qty % 2 != 0 && $k == $qty1 ? 1 : 2;
    
                // 2nd Loop (letters)
                foreach ( array('A', 'B', 'C', 'D') as $letter ) {
                    $key = strtolower($letter);
    
                    // 3rd Loop (Columns)
                    for ( $h = 1; $h <= $qty2; $h++ ) {
                        $class = $item_qty > 1 ? (($h % 2 == 0) ? 'last' : 'first' ) : 'wide';
                        $class = $item_qty % 2 != 0 && $k == $qty1 ? 'wide' : $class;
    
                        $index = $item_qty == 1 ? 1 : 2;
                        $index = in_array($class, ['first', 'wide']) ? ($k*2)-1 : $k*2;
    
                        $field = "custom_field_{$key}{$index}";
                        $label = sprintf('%s %s%d', __('Custom Field', $domain), $letter, $index);
                    
                        woocommerce_form_field( $field, array(
                            'type'          => 'text',
                            'label'         => $label,
                            'placeholder'   => '',
                            'class'         => array('custom-field form-row-'.$class),
                            'required'      => true, // or false
                        ), WC()->checkout->get_value( $field ) );
                    }
                }
            }
            echo '</div></div>';
        }
    }
    
    // Validate custom fields
    add_action( 'woocommerce_after_checkout_validation', 'validate_custom_checkout_fields', 10, 2 );
    function validate_custom_checkout_fields( $data, $errors ) {
        if ( did_action('woocommerce_checkout_process') >= 2 ) return;
    
        $item_qty = get_specific_cart_item_quantity();
    
        if ( $item_qty ) {
            $domain   = 'woocommerce';
            $break    = false;
    
            // 1st Loop (Letters)
            foreach ( array('A', 'B', 'C', 'D') as $letter ) {
                $key   = strtolower($letter);
    
                // 2nd Loop (Numbers)
                for ( $i = 1; $i <= $item_qty; $i++ ) {
                    $field = "custom_field_{$key}{$i}";
                    
                    if ( isset($_POST[$field]) && empty($_POST[$field]) ) {
                        $errors->add( 'validation', __('Please fill in all custom fields.', $domain), 'error' );
                        $break = true;
                        break;
                    }
                }
                if ( $break ) break;
            }
        }
    }
    
    // Save custom field data as custom order meta data
    add_action( 'woocommerce_checkout_create_order', 'save_custom_checkout_fields', 10, 2 );
    function save_custom_checkout_fields( $order, $data ) {
        $item_qty = get_specific_cart_item_quantity();
    
        if ( $item_qty ) {
            // 1st Loop (Letters)
            foreach ( array('A', 'B', 'C', 'D') as $letter ) {
                $key   = strtolower($letter);
    
                // 2nd Loop (Numbers)
                for ( $i = 1; $i <= $item_qty; $i++ ) {
                    $field = "custom_field_{$key}{$i}";
                    
                    if ( isset($_POST[$field]) && ! empty($_POST[$field]) ) {
                        $order->update_meta_data( $field, sanitize_text_field($_POST[$field]) );
                    }
                }
            }
        }
    }
    

    Code goes in functions.php file of your child theme (or in a plugin). Tested and works.