Laravel Livewire+Alpine.js环境下Tom Select动态添加多行选择器渲染异常问题排查及Select2替代可行性咨询
Hey there, let's work through this problem together! The core issue here is that Tom Select (and Select2, for that matter) only initializes on elements that exist in the DOM when your page first loads. When Livewire dynamically adds new rows, those new <select> elements never get the Tom Select initialization treatment—so they stay as plain old dropdowns.
Fixing Tom Select with Livewire Dynamic Rows
Here's how to adjust your code to make Tom Select work with dynamic rows:
1. Refactor Your Tom Select Initialization Code
First, wrap the initialization logic in a reusable function, then add listeners for Livewire events to re-run initialization on new elements:
import TomSelect from "tom-select"; (function (cash) { "use strict"; // Reusable function to initialize Tom Select function initTomSelect(selector) { cash(selector).each(function () { // Skip if already initialized to avoid duplicates if (this.tomselect) return; let options = { plugins: { dropdown_input: {}, }, }; if (cash(this).data("placeholder")) { options.placeholder = cash(this).data("placeholder"); } if (cash(this).attr("multiple") !== undefined) { options = { ...options, plugins: { ...options.plugins, remove_button: { title: "Remove this item" }, }, persist: false, create: true, onDelete: function (values) { return confirm( values.length > 1 ? `Are you sure you want to remove these ${values.length} items?` : `Are you sure you want to remove "${values[0]}"?` ); }, }; } if (cash(this).data("header")) { options = { ...options, plugins: { ...options.plugins, dropdown_header: { title: cash(this).data("header") }, }, }; } new TomSelect(this, options); // Mark as initialized to skip future runs cash(this).attr("data-tomselect-ready", "true"); }); } // Initialize existing elements on page load initTomSelect(".tom-select"); // Listen for Livewire's row add event document.addEventListener('livewire:init', function () { Livewire.on('addFormRow', function () { // Wait for Livewire to finish updating the DOM setTimeout(() => { initTomSelect(".tom-select:not([data-tomselect-ready])"); }, 100); }); // Optional: Clean up Tom Select instances when rows are removed Livewire.on('removeFormRow', function (rowKey) { cash(`.tom-select[data-row-key="${rowKey}"]`).each(function () { if (this.tomselect) { this.tomselect.destroy(); } }); }); }); })(cash);
2. Update Your Blade View
Add a data-row-key attribute to your selects so you can target them when removing rows:
@foreach ($inputs as $key => $value) <tr> <!-- ... existing cells ... --> <td class="px-6 py-2 whitespace-nowrap text-sm font-medium text-gray-900"> <select data-row-key="{{ $key }}" data-placeholder="Select your favorite actors" class="tom-select w-full" > <option value="1" selected>Leonardo DiCaprio</option> <option value="2">Johnny Deep</option> <option value="3" selected>Robert Downey, Jr</option> <option value="4">Samuel L. Jackson</option> <option value="5">Morgan Freeman</option> </select> </td> <td class="px-6 py-2 whitespace-nowrap text-sm text-gray-500"> <select data-row-key="{{ $key }}" data-placeholder="Select your favorite actors" class="tom-select w-full" multiple > <!-- ... options ... --> </select> </td> <!-- ... remove button ... --> </tr> @endforeach
3. Update Your Livewire Component
Emit an event when removing a row to trigger cleanup:
public function remove($i) { $this->emit('removeFormRow', $i); unset($this->inputs[$i]); }
Switching to Select2 (If Needed)
If you prefer Select2, the exact same logic applies—you just swap the initialization code:
(function (cash) { "use strict"; function initSelect2(selector) { cash(selector).each(function () { if (cash(this).data('select2')) return; let options = { placeholder: cash(this).data("placeholder"), allowClear: true }; if (cash(this).attr("multiple") !== undefined) { options.multiple = true; } cash(this).select2(options); cash(this).attr("data-select2-ready", "true"); }); } // Initialize on page load initSelect2(".select2"); // Livewire event listeners document.addEventListener('livewire:init', function () { Livewire.on('addFormRow', function () { setTimeout(() => { initSelect2(".select2:not([data-select2-ready])"); }, 100); }); Livewire.on('removeFormRow', function (rowKey) { cash(`.select2[data-row-key="${rowKey}"]`).each(function () { cash(this).select2('destroy'); }); }); }); })(cash);
Then just update your Blade selects to use the select2 class instead of tom-select.
Key Takeaway
Any third-party UI plugin that modifies DOM elements needs to be re-initialized on dynamically added content. Livewire emits events when it updates the DOM, so we can hook into those events to run our initialization code after new rows are added.
内容的提问来源于stack exchange,提问作者user4543412

