You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

动态添加行后Tom Select下拉框报错:Cannot read properties of undefined (reading 'trim')

动态添加表单行后Tom Select组件失效的解决方案

问题概述

表单下拉框使用Tom Select组件,支持动态添加新行。默认行的组件可正常工作,但点击添加按钮后,新行的Tom Select失效,抛出错误:

Error populating dropdown: TypeError: Cannot read properties of undefined (reading 'trim')

用户提供的原始代码:

$(document).ready(function () {
    // Initialize Tom Select for the default product dropdown
    initializeTomSelect('.product_id', true);

    // Add new row functionality
    $('#add_expense_row').click(function (e) {
        e.preventDefault();

        // Clone the row template
        const newRow = $('#aod_line').clone();
        newRow.removeAttr('id'); // Remove the ID to avoid duplication
        newRow.find('input[type="number"], input[type="text"]').val(''); // Clear input values
        newRow.find('.product_id').empty(); // Clear existing options
        newRow.find('.product_id').append('<option value="" disabled selected>Select a product</option>'); // Add placeholder
        newRow.find('#remove-line').prop('disabled', false); // Enable remove button

        // Append the new row
        $('#submit_div').before(newRow);

        // Initialize Tom Select for the newly added dropdown after populating options
        const productDropdown = newRow.find('.product_id');
        initializeTomSelect(productDropdown, false);
    });

    // Remove row functionality
    $(document).on('click', '#remove-line', function (e) {
        e.preventDefault();
        $(this).closest('.row').remove();
    });

    // Function to initialize Tom Select
    function initializeTomSelect(selector, isStatic = false) {
        $(selector).each(function () {
            const dropdown = $(this);

            // Populate options first before initializing Tom Select
            populateProductDropdown(dropdown).then(() => {
                console.log('Dropdown after population:', dropdown.html());
                
                if (!dropdown[0].tomselect) { // Check if Tom Select is already initialized
                    new TomSelect(dropdown[0], {
                        placeholder: 'Select a product',
                        allowEmptyOption: true, // Allow empty dropdowns to prevent errors
                    });
                }
            }).catch((error) => {
                console.error('Error populating dropdown:', error);
            });
        });
    }

    // Function to populate product dropdown with products
    function populateProductDropdown(dropdown) {
        return new Promise((resolve, reject) => {
            $.ajax({
                url: '../controller/operations_controller.php?status=get-products',
                method: 'GET',
                dataType: 'json',
                success: function (data) {
                    const select = dropdown[0].tomselect;

                    if (select) {
                        // If Tom Select is already initialized
                        select.clearOptions(); // Clear existing options
                        select.addOption({ value: '', text: 'Select a product', disabled: true }); // Add placeholder

                        data.forEach((product) => {
                            select.addOption({ value: product.product_id, text: product.product_name });
                        });
                        select.refreshOptions(false); // Refresh without opening
                    } else {
                        // If Tom Select is not initialized yet
                        dropdown.empty();
                        dropdown.append('<option value="" disabled selected>Select a product</option>'); // Add placeholder

                        data.forEach((product) => {
                            dropdown.append(`<option value="${product.product_id}">${product.product_name}</option>`);
                        });
                    }

                    resolve(); // Resolve the promise after population
                },
                error: function (xhr, status, error) {
                    console.error('AJAX error:', xhr.responseText || error);
                    alert('Failed to load products. Please try again later.');
                    reject(error); // Reject the promise on error
                }
            });
        });
    }
});

问题根源

  1. 克隆行携带Tom Select实例残留:克隆已初始化Tom Select的模板行时,DOM元素上绑定的Tom Select内部属性被一并复制,导致新行的下拉框初始化时出现冲突。
  2. 填充逻辑冗余且判断混乱:克隆后手动添加占位符,后续填充函数又重复操作;同时提前尝试访问未初始化的tomselect属性,引发未定义错误。
  3. 删除行未清理Tom Select实例:直接删除行会留下内存泄漏风险,也可能干扰其他组件状态。

修复后的代码

$(document).ready(function () {
    // 单独初始化默认行的Tom Select(模板行#aod_line保持未初始化状态)
    initializeTomSelect($('#aod_line .product_id'));

    // 添加新行功能
    $('#add_expense_row').click(function (e) {
        e.preventDefault();

        // 克隆纯HTML模板(避免携带Tom Select实例数据)
        const newRow = $('#aod_line').clone();
        newRow.removeAttr('id');
        // 清空输入框值
        newRow.find('input[type="number"], input[type="text"]').val('');
        // 清空下拉框,由填充函数统一处理内容
        newRow.find('.product_id').empty();
        newRow.find('#remove-line').prop('disabled', false);

        // 将新行插入页面
        $('#submit_div').before(newRow);

        // 初始化新行的Tom Select
        initializeTomSelect(newRow.find('.product_id'));
    });

    // 删除行功能(销毁对应Tom Select实例后再删除)
    $(document).on('click', '#remove-line', function (e) {
        e.preventDefault();
        const $row = $(this).closest('.row');
        const $selectElement = $row.find('.product_id')[0];
        
        // 销毁Tom Select实例,避免内存泄漏
        if ($selectElement.tomselect) {
            $selectElement.tomselect.destroy();
        }
        
        $row.remove();
    });

    // 初始化Tom Select的通用函数
    function initializeTomSelect($selectElement) {
        $selectElement.each(function () {
            const $dropdown = $(this);

            // 先填充选项,再初始化组件
            populateProductDropdown($dropdown).then(() => {
                // 确保未初始化过才创建实例
                if (!$dropdown[0].tomselect) {
                    new TomSelect($dropdown[0], {
                        placeholder: '选择产品',
                        allowEmptyOption: true,
                    });
                }
            }).catch((error) => {
                console.error('填充下拉框出错:', error);
            });
        });
    }

    // 填充产品下拉框的函数
    function populateProductDropdown($dropdown) {
        return new Promise((resolve, reject) => {
            $.ajax({
                url: '../controller/operations_controller.php?status=get-products',
                method: 'GET',
                dataType: 'json',
                success: function (data) {
                    // 清空下拉框并重新填充内容
                    $dropdown.empty();
                    $dropdown.append('<option value="" disabled selected>选择产品</option>');
                    
                    data.forEach((product) => {
                        $dropdown.append(`<option value="${product.product_id}">${product.product_name}</option>`);
                    });
                    
                    resolve();
                },
                error: function (xhr, status, error) {
                    console.error('AJAX请求出错:', xhr.responseText || error);
                    alert('加载产品失败,请稍后重试。');
                    reject(error);
                }
            });
        });
    }
});

关键修复说明

  • 模板行隔离:模板行#aod_line保持纯HTML状态,不初始化Tom Select,仅用于克隆,避免实例数据残留。
  • 简化填充逻辑:去掉对tomselect属性的提前判断,直接操作原生<select>元素填充选项,之后再初始化组件,避免未定义错误。
  • 清理实例:删除行前销毁对应的Tom Select实例,防止内存泄漏和状态混乱。
  • 冗余操作移除:删除克隆后手动添加占位符的代码,由填充函数统一处理,避免重复内容干扰组件初始化。

内容的提问来源于stack exchange,提问作者Harindu Madushanka

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 08:16:04