动态添加行后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 } }); }); } });
问题根源
- 克隆行携带Tom Select实例残留:克隆已初始化Tom Select的模板行时,DOM元素上绑定的Tom Select内部属性被一并复制,导致新行的下拉框初始化时出现冲突。
- 填充逻辑冗余且判断混乱:克隆后手动添加占位符,后续填充函数又重复操作;同时提前尝试访问未初始化的
tomselect属性,引发未定义错误。 - 删除行未清理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
相关产品推荐
相关产品推荐

