Custom Field Types

I’m looking for some help on a custom field type. I created a multi select dropdown with search and editor options. This seems to be working just fine on my local environment. But when I push to our hosting site it’s completely broken. If I add several of the fields to the form only one field actually looks like it’s suppose to the other looks like the core multiselect. Here is my classes below

class GF_Multiselect_Service {





    /** Register the field type with Gravity Forms */

    public function register_field() {

        // Only proceed when GF is loaded

        if (! class_exists('\GFForms') || ! class_exists('\GF_Fields')) return;




        // Lazy-require the field class here to avoid load-order issues

        require_once plugin_dir_path(__FILE__) . '/GF/class-gf-field-multiselect-dropdown.php';




        \GF_Fields::register(

            new GF_Field_MultiSelect_Dropdown()

        );

    }




    public function gppa_is_supported_field($is_supported, $field) {

        if (is_object($field) && isset($field->type) && $field->type === 'ms_dropdown') {

            return true;

        }

        return $is_supported;

    }




    public function gppa_input_choices($choices, $field) {

        if (!is_object($field) || $field->type !== 'ms_dropdown') {

            return $choices;

        }




        // Normalize

        $out = [];

        foreach ((array) $choices as $c) {

            if (is_array($c)) {

                $text = isset($c['text']) ? (string) $c['text'] : (string) reset($c);

                $val  = array_key_exists('value', $c) && $c['value'] !== '' ? (string) $c['value'] : $text;

            } else {

                $text = (string) $c;

                $val  = $text;

            }

            $out[] = ['text' => $text, 'value' => $val];

        }




        return $out;

    }




    /** Front-end assets only when a form contains our field */

    public function enqueue_assets($form, $is_ajax) {

        $has = false;

        foreach ((array) rgar($form, 'fields') as $field) {

            if (is_object($field) && $field->type === 'ms_dropdown') {

                $has = true;

                break;

            }

            if (is_array($field)  && rgar($field, 'type') === 'ms_dropdown') {

                $has = true;

                break;

            }

        }




        if (!$has) return;




        $ver = '1.0.0';

        wp_enqueue_style(

            'deck-gf-msd',

            DECKLOPEDIA_PLUGIN_URL . 'public/css/gf-multisel.css',

            [],

            $ver

        );

        wp_enqueue_script(

            'deck-gf-msd',

            DECKLOPEDIA_PLUGIN_URL . 'public/js/gf-multisel.js',

            ['jquery'],

            $ver,

            true

        );

        wp_add_inline_script(

            'deck-gf-msd',

            <<<JS

        try {

            gform.addFilter('gppa_is_supported_field', function(isSupported, field){

                return field && field.type === 'ms_dropdown' ? true : isSupported;

            });

        } catch(e) { console.warn('[ms_dropdown] GPPA frontend not present:', e); }




        jQuery(document).on('gppa_updated_batch_fields', function(){

        // If your init needs re-binding, call it here (otherwise safe to no-op)

        // e.g., jQuery('.sp-msd').each(function(){ /* re-init if needed */ });

        });




        console.log('[ms_dropdown] GPPA frontend shim loaded');

        JS,

            'after'

        ); // ensure it runs after the main handle

    }




    public function enqueue_gf_editor_assets($hook) {

        // Only on GF form editor page

        if (empty($_GET['page']) || $_GET['page'] !== 'gf_edit_forms') return;




        $ver = '1.0.0';




        wp_enqueue_style(

            'deck-gf-msd',

            DECKLOPEDIA_PLUGIN_URL . 'public/css/gf-multisel.css',

            [],

            $ver

        );




        wp_enqueue_script(

            'deck-gf-msd',

            DECKLOPEDIA_PLUGIN_URL . 'public/js/gf-multisel.js',

            ['jquery'],

            $ver,

            true

        );

    }




    public function enqueue_gf_editor_script($hook) {

        if (empty($_GET['page']) || $_GET['page'] !== 'gf_edit_forms') return;




        // Load after GPPA’s script. Using an empty handle with inline JS ensures a real <script>.

        wp_register_script('deck-gf-editor', '', ['jquery'], null, true);

        wp_enqueue_script('deck-gf-editor');




        wp_add_inline_script(

            'deck-gf-editor',

            <<<JS

                (function($){

                // Make GPPA treat our custom field as supported in the editor

                try {

                    gform.addFilter('gppa_is_supported_field', function(isSupported, field){

                    return field && field.type === 'ms_dropdown' ? true : isSupported;

                    });

                } catch(e) {

                    console.warn('[ms_dropdown] GPPA editor not present:', e);

                }




                $(document).on('gform_load_field_settings', function (event, field, form) {




                    // Display mode

                    $('#sp_msd_display_mode').val(field.sp_msd_display_mode || 'chips');




                    // Checkboxes

                    $('#sp_msd_enable_search').prop('checked', String(field.sp_msd_enable_search) === '1' || field.sp_msd_enable_search === true);

                    $('#sp_msd_enable_select_all').prop('checked', String(field.sp_msd_enable_select_all) === '1' || field.sp_msd_enable_select_all === true);




                    // Max selections

                    $('#sp_msd_max_selections').val(field.sp_msd_max_selections || '');

                });




                // Tell GF which settings to unhide for ms_dropdown

                window.fieldSettings = window.fieldSettings || {};

                fieldSettings.ms_dropdown = (fieldSettings.ms_dropdown || '')

                    + ', choices_setting, choice_values_setting'

                    + ', sp_msd_enable_search, sp_msd_enable_select_all, sp_msd_max_selections, sp_msd_display_mode';




                // Optional console pings

                console.log('[ms_dropdown] GF editor shim loaded (admin)');

                })(jQuery);

                JS

        );

    }




    public function dl_msd_fix_gppa_props($form, $form_id = null) {

        if (empty($form['fields'])) return $form;




        foreach ($form['fields'] as &$field) {

            if (!is_object($field) || $field->type !== 'ms_dropdown') {

                continue;

            }




            // GPPA + GF expect these for choice-like inputs

            $field->inputType = 'multiselect'; // keep internal type choice-friendly

            $field->allowsPrepopulate = true;




            // IMPORTANT: ensure choices is always an array (GPPA will replace it later)

            if (!is_array($field->choices)) {

                $field->choices = [];

            }




            $enabled = !empty($field->{'gppa-choices-enabled'});




            // GPPA uses inputName to locate its population target sometimes

            if ($enabled) {

                // Use the normal GF input name format

                $field->inputName = sprintf('input_%d', (int) $field->id);

            }

        }

        unset($field);




        return $form;

    }





    /** Admin field editor: extra settings blocks (optional) */

    public function editor_settings_markup($position, $form_id) {

        if ($position != 50) return;

?>

        <li class="sp_msd_enable_search field_setting" style="margin-top:8px;">

            <input type="checkbox"

                id="sp_msd_enable_search"

                onclick="SetFieldProperty('sp_msd_enable_search', this.checked ? 1 : 0)">

            <label for="sp_msd_enable_search" class="inline"><?php esc_html_e('Enable search', 'decklopedia'); ?></label>

        </li>

        <li class="sp_msd_enable_select_all field_setting">

            <input type="checkbox"

                id="sp_msd_enable_select_all"

                onclick="SetFieldProperty('sp_msd_enable_select_all', this.checked ? 1 : 0)">

                <label for="sp_msd_enable_select_all" class="inline"><?php esc_html_e('Enable “Select all”', 'decklopedia'); ?></label>

        </li>

        <li class="sp_msd_max_selections field_setting">

            <label for="sp_msd_max_selections"><?php esc_html_e('Max selections (optional)', 'decklopedia'); ?></label>

            <input type="number" id="sp_msd_max_selections" min="0" oninput="SetFieldProperty('sp_msd_max_selections', this.value)">

        </li>

        <li class="sp_msd_display_mode field_setting">

            <label for="sp_msd_display_mode"><?php esc_html_e('Display mode', 'decklopedia'); ?></label>

            <select id="sp_msd_display_mode" onchange="SetFieldProperty('sp_msd_display_mode', this.value)">

                <option value="chips"><?php esc_html_e('Chips', 'decklopedia'); ?></option>

                <option value="count"><?php esc_html_e('Count only', 'decklopedia'); ?></option>

            </select>

        </li>

    <?php

    }




    public function editor_defaults() { ?>

        case 'ms_dropdown':

        field.placeholder = 'Select...';

        field.sp_msd_display_mode = 'chips';

        field.sp_msd_enable_search = false;

        field.sp_msd_enable_select_all = false;

        field.sp_msd_max_selections = '';

        break;

<?php }

}
class GF_Field_MultiSelect_Dropdown extends GF_Field {




    public $type = 'ms_dropdown';




    // Tell GF this field has choices (so the Choices UI shows up)

    public function has_choices() {

        return true;

    }




    public function get_input_type() {

        return $this->type; // "ms_dropdown"

    }




    // Submit as an array (you already had this, keep it)

    public function is_value_submission_array() {

        return true;

    }




    public function get_form_editor_field_title() {

        $decklopedia = new Decklopedia();




        return esc_html__('Multi-Select Dropdown', $decklopedia->get_plugin_name());

    }




    public function get_form_editor_field_settings() {

        return [

            'label_setting',

            'description_setting',

            'required_setting',

            'css_class_setting',

            'choices_setting',              // <— provides add/remove options UI

            'choice_values_setting',        // toggle “Enable values”

            // (optional) if you want the built-in “Enhanced UI” toggle on Advanced tab:

            // 'enable_enhanced_ui_setting',




            'placeholder_setting',

            'conditional_logic_field_setting',

            // your custom settings (search, select-all, max, display mode)

            'sp_msd_enable_search',

            'sp_msd_enable_select_all',

            'sp_msd_max_selections',

            'sp_msd_display_mode',

        ];

    }




    public function get_form_editor_button() {

        return [

            'group' => 'advanced_fields',

            'text'  => $this->get_form_editor_field_title(),

            'icon'  => 'gform-icon--select' // pick any

        ];

    }




    public function allowsPrepopulate() {

        return true;

    }




    public function get_field_input($form, $value = '', $entry = null) {

        $form_id  = absint($form['id']);

        $field_id = intval($this->id);




        //error_log("Rendering Form: " . print_r($form, true));




        $tabindex = $this->get_tabindex();

        $required = $this->isRequired ? 'aria-required="true"' : '';

        $placeholder = esc_attr($this->placeholder);




        // Normalize $value to array of selected values

        $current = is_array($value) ? $value : ($value !== '' ? (array) $value : []);

        // Support dynamic prepop as comma-separated string

        if (count($current) === 1 && is_string($current[0]) && strpos($current[0], ',') !== false) {

            $current = array_map('trim', explode(',', $current[0]));

        }




        $choices = (array) $this->choices;

        $input_name = "input_{$field_id}[]"; // array submission

        $enable_search     = filter_var($this->sp_msd_enable_search ?? false, FILTER_VALIDATE_BOOLEAN);

        $enable_select_all = filter_var($this->sp_msd_enable_select_all ?? false, FILTER_VALIDATE_BOOLEAN);




        $data = [

            'enableSearch'    => $enable_search ? '1' : '0',

            'enableSelectAll' => $enable_select_all ? '1' : '0',

            'maxSelections'   => (isset($this->sp_msd_max_selections) && $this->sp_msd_max_selections !== '') ? (int) $this->sp_msd_max_selections : '',

            'displayMode'     => $this->sp_msd_display_mode ?: 'chips',

            'inputName'       => $input_name,

        ];




        $data_attrs = '';

        foreach ($data as $k => $v) {

            $data_attrs .= ' data-' . esc_attr($k) . '="' . esc_attr($v) . '"';

        }




        $screen = function_exists('get_current_screen') ? get_current_screen() : null;




        // Gravity Forms → Form Editor

        $is_gf_editor =

            (isset($_GET['page']) && $_GET['page'] === 'gf_edit_forms')  // GF 2.7+

            || ($screen && strpos($screen->id ?? '', 'gf_edit_forms') !== false);




        ob_start(); ?>




        <div class="sp-msd gform-theme__no-reset--children" id="sp-msd-<?php echo $form_id . '-' . $field_id; ?>" <?php echo $data_attrs; ?>>

            <button type="button"

                class="sp-msd__button"

                aria-haspopup="listbox"

                aria-expanded="false"

                <?php echo $tabindex . ' ' . $required; ?>>

                <span class="sp-msd__placeholder"><?php echo $placeholder ?: esc_html__('Select...', 'decklopedia'); ?></span>

                <span class="sp-msd__chips" aria-hidden="true">

                    <?php if ($is_gf_editor) {

                        echo '<span class="sp-msd__chip skeleton" aria-hidden="true"></span>';

                    }

                    ?>

                </span>

                <span class="sp-msd__count" aria-hidden="true"></span>

                <span class="sp-msd__chevron" aria-hidden="true">▾</span>

            </button>




            <div class="sp-msd__panel" role="group" aria-label="<?php echo esc_attr($this->label); ?>">

                <?php

                $current = is_array($value) ? $value : ($value !== '' ? (array) $value : []);

                if (count($current) === 1 && is_string($current[0]) && strpos($current[0], ',') !== false) {

                    $current = array_map('trim', explode(',', $current[0]));

                }




                // Ask GF to prepare choices (this is where GPPA typically injects)

                if (method_exists($this, 'get_choices')) {

                    $maybe = $this->get_choices($value);

                    if (is_array($maybe)) {

                        $this->choices = $maybe;

                    }

                }




                $raw_choices = is_array($this->choices) ? $this->choices : [];




                $choices = [];

                foreach ($raw_choices as $c) {

                    if (is_array($c)) {

                        $text = isset($c['text']) ? (string) $c['text'] : (string) reset($c);

                        $val  = isset($c['value']) && $c['value'] !== '' ? (string) $c['value'] : $text;

                    } else {

                        $text = (string) $c;

                        $val  = $text;

                    }

                    $choices[] = ['text' => $text, 'value' => $val];

                }




                // Optional: if no choices yet (freshly added field), render an empty list gracefully

                if (empty($choices)) {

                    $choices = []; // or seed with some defaults if you prefer

                }




                

                if ($enable_search) : ?>

                    <div class="sp-msd__search">

                        <input type="text" class="sp-msd__search-input" placeholder="<?php esc_attr_e('Search...', 'decklopedia'); ?>">

                    </div>

                <?php endif; ?>




                <?php                

                if ($enable_select_all) : ?>

                    <div class="sp-msd__selectall">

                        <label><input type="checkbox" class="sp-msd__toggle-all"> <?php esc_html_e('Select all', 'decklopedia'); ?></label>

                    </div>

                <?php endif; ?>




                <ul class="sp-msd__list" role="listbox" aria-multiselectable="true">

                    <?php foreach ($choices as $i => $choice):

                        $val     = $choice['value'];

                        $text    = $choice['text'];

                        $checked = in_array((string) $val, array_map('strval', $current), true);

                        $item_id = "spmsd-{$form_id}-{$field_id}-{$i}";

                    ?>

                        <li class="sp-msd__item" role="option" aria-selected="<?php echo $checked ? 'true' : 'false'; ?>">

                            <label for="<?php echo esc_attr($item_id); ?>">

                                <input id="<?php echo esc_attr($item_id); ?>"

                                    type="checkbox"

                                    name="<?php echo esc_attr($input_name); ?>"

                                    class="sp-msd__cb"

                                    value="<?php echo esc_attr($val); ?>"

                                    <?php checked($checked); ?>>

                                <span class="sp-msd__label"><?php echo esc_html($text); ?></span>

                            </label>

                        </li>

                    <?php endforeach; ?>

                </ul>




                <?php if ($data['maxSelections'] !== '') : ?>

                    <div class="sp-msd__note" aria-live="polite"></div>

                <?php endif; ?>

            </div>

        </div>




<?php

        return ob_get_clean();

    }




    /** Admin field setting bindings so values persist */

    public function get_field_content($value, $force_frontend_label, $form) {

        // Use default renderer; our settings bind via JS on admin.

        return parent::get_field_content($value, $force_frontend_label, $form);

    }




    /** Sanitize on submission */

    public function sanitize_entry_value($value, $form_id) {

        if (is_array($value)) {

            $value = array_values(array_filter(array_map('sanitize_text_field', $value), 'strlen'));

        } else {

            $value = sanitize_text_field($value);

        }

        return $value;

    }




    /** Entry list display */

    public function get_value_entry_list($value, $entry, $field_id, $columns, $form) {

        $vals = is_array($value) ? $value : ($value ? [$value] : []);

        return esc_html(implode(', ', $vals));

    }

}

Interesting! I was able to make it work in live site. I’ll play with your code this weekend and let’s see if I can find something.

1 Like