Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Bootstrap Duallistbox Laravel Package

istvan-ujjmeszaros/bootstrap-duallistbox

Bootstrap Dual Listbox is a responsive jQuery widget that turns two select boxes into a searchable dual-list transfer control for Bootstrap. It adds live filtering, move/remove-all buttons, keyboard support, and customizable styling for building accessible, mobile-friendly selection interfaces in we...

View on GitHub
Deep Wiki
Context7

Getting Started

This is not a Composer/PHP package — it's a jQuery plugin (Bootstrap 3–era, last tagged 3.0.7 in 2020) that progressively enhances a <select multiple> into a dual-listbox widget. There is no service provider, no vendor:publish, no Laravel integration to speak of. Assume you will wire it up manually.

Install the asset (pick one):

npm install bootstrap-duallistbox
{{-- or quick-and-dirty via CDN --}}
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/bootstrap-duallistbox.css">
<script src="https://unpkg.com/[email protected]/dist/jquery.bootstrap-duallistbox.min.js"></script>

Check the dist/ folder after install for exact filenames — the plugin JS is jquery.bootstrap-duallistbox.min.js / .js.

Where to look first:

  1. The README's "Options" table — that's 90% of the API surface (moveOnSelect, filterTextClear, infoText, nonselectedlistlabel, selectedlistlabel, preserveSelectionOnMove, showFilterInputs, etc.).
  2. The destroy / refresh methods — you will need them for any re-render.
  3. The generated DOM (two <select class="bootstrap-duallistbox-..."> elements + the original hidden <select>) — most debugging happens there.

First use case — roles/permissions on a user form:

{{-- resources/views/admin/users/_form.blade.php --}}
<select name="roles[]" id="roles" multiple class="form-control">
    @foreach ($roles as $id => $name)
        <option value="{{ $id }}" @selected(in_array($id, old('roles', $user->roles->pluck('id')->all() ?? [])))>
            {{ $name }}
        </option>
    @endforeach
</select>
// resources/js/app.js
import $ from 'jquery';
window.$ = window.jQuery = $;
import 'bootstrap-duallistbox/dist/jquery.bootstrap-duallistbox.min.js';
import 'bootstrap-duallistbox/dist/bootstrap-duallistbox.css';

$(function () {
    $('#roles').bootstrapDualListbox({
        nonselectedlistlabel: 'Available',
        selectedlistlabel: 'Assigned',
        filterPlaceHolder: 'Filter…',
        moveOnSelect: false,
        preserveSelectionOnMove: 'moved',
        infoText: 'Showing all {0}',
        infoTextFiltered: '<span class="badge bg-warning">{0}</span> / {1}',
        infoTextEmpty: 'Empty list',
    });
});

Server side, nothing changes: 'roles' => ['array'], 'roles.*' => ['integer', 'exists:roles,id'].

Implementation Patterns

Vite + jQuery ordering. jQuery must be on window before the plugin script evaluates. The snippet above (assign then import) works because ES module imports are hoisted — if you get $ is not defined inside the plugin, switch to a dynamic import or configure optimizeDeps.include / use the jquery global via a vite.config.js alias. Breeze/Vite no longer ships jQuery; add it explicitly and pin the plugin version.

Reusable Blade component. Wrap the select in an anonymous component so the id, name, and selected values are consistent — the plugin relies on the id for init.

{{-- resources/views/components/dual-listbox.blade.php --}}
@props(['name', 'options', 'selected' => [], 'label' => null])
<select name="{{ $name }}[]" {{ $attributes->merge(['class' => 'form-control duallistbox']) }} multiple>
    @foreach ($options as $value => $text)
        <option value="{{ $value }}" @selected(in_array($value, (array) $selected))>{{ $text }}</option>
    @endforeach
</select>
@push('scripts')
    <script>document.addEventListener('DOMContentLoaded', () => $('.{{ $attributes->get('class') }}').bootstrapDualListbox());</script>
@endpush

Multiple instances on a page. The plugin only enhances the first match per call. Loop with .each() and pass per-element options (labels differ per field):

$('.duallistbox').each(function () {
    $(this).bootstrapDualListbox({ moveOnSelect: false });
});

Pre-selection & ordering. The plugin reads selected attributes at init, and preserves order you add options to the selected side. For belongsToMany with pivot ordering, sort the incoming collection before rendering; the plugin will not sort for you.

Livewire / Alpine / Inertia re-renders. The plugin mutates DOM nodes your framework believes it owns. The reliable pattern is destroy → patch options → re-init:

function resetDualList(el) {
    if (el.dataset.dlInit === '1') {
        $(el).bootstrapDualListbox('destroy');
        el.dataset.dlInit = '0';
    }
    $(el).bootstrapDualListbox(/* options */);
    el.dataset.dlInit = '1';
}

document.addEventListener('livewire:navigated', () => {
    document.querySelectorAll('select.duallistbox').forEach(resetDualList);
});
// Livewire 3: Livewire.hook('morph.updated', ({ el }) => { ...if (el.contains(el.closest('select.duallistbox'))) resetDualList(el.closest('select.duallistbox')); });

For an Inertia + Vue/React page, prefer mounting the <select> inside a ref div, init in onMounted, destroy in onBeforeUnmount. If the component re-renders often, this widget adds friction — consider a non-jQuery alternative.

Dynamic option lists (dependent dropdowns). There is a refresh command, but filter inputs and info text are only reliably rebuilt on destroy/re-init:

const $el = $('#permissions');
$el.bootstrapDualListbox('destroy');
$el.empty();
$.each(newOptions, (v, t) => $el.append(new Option(t, v)));
$el.bootstrapDualListbox({ moveOnSelect: false });

Form UX conventions that age well:

  • moveOnSelect: false + preserveSelectionOnMove: 'moved' — lets users ctrl-click a batch, then move; feels native for permission pickers.
  • Keep showFilterInputs: true whenever > ~20 options; the filter is this plugin's main advantage over a plain multi-select.
  • Pass translated labels via __() in the Blade partial, not hard-coded strings.
  • Store one canonical id per select and reuse the component — don't re-implement options inline in every view.

Integration with server-side validation and old input. Because the original <select multiple> is merely hidden (not removed), old('roles', ...) in Blade and Laravel's array validation work untouched. Never build the options in JS only — always render server-side first for graceful degradation when JS fails.

Gotchas and Tips

  • No Composer package / no Laravel support. If you were expecting composer require istvan-ujjmeszaros/..., that doesn't exist. This is a frontend asset only; budget for Vite/CDN wiring and your own Blade wrapper.
  • Maintenance risk. Last release April 2020 (v3.0.7). Bootstrap 5 is only partially compatible — the plugin emits Bootstrap 3 classes (.btn-default, .list-group markup assumptions) and there is no official BS5 build. Expect CSS touch-ups: .bootstrap-duallistbox-container .btn overrides, filter input form-control styling, and box heights. For greenfield projects without a jQuery requirement, Tom Select / Choices.js / Slim Select are better-maintained.
  • jQuery is required. Laravel 10/11 with Breeze/Inertia does not ship jQuery. Add it, set window.jQuery/window.$, and pin the plugin to an exact version ("bootstrap-duallistbox": "3.0.7") — the package is deprecated enough that ^3 may resolve to something unexpected later.
  • The visible widgets are clones. After init your DOM contains select.bootstrap-duallistbox-nonselected-list_ and select.bootstrap-duallistbox-selected-list_ plus the original hidden select. Read form state from the original ($('#roles').val()), never from the clones. Any JS that grabs select[name="roles[]"] and iterates options may see duplicates.
  • Change events. In some versions the plugin does not fire change on the original select when options move; if you have dependent UI (e.g. "count selected"), bind to .on('change', ...) on the original and test it — otherwise attach a manual trigger inside a setInterval-free mutation observer, or wrap your own move handlers via on... callbacks if exposed.
  • Destroy before teardown. Forgetting ('destroy') before a Livewire/Alpine/Turbo re-render leads to orphaned widget DOM, duplicated filter inputs, and doubled name="roles[]" submissions. Always track an data-dl-init flag.
  • Filter + large lists. The filter is client-side only; a 5,000-option select will lag on every keystroke. Keep lists under a few hundred, or paginate server-side and switch to an autocomplete component instead.
  • Accessibility & keyboard. The dual-listbox is mouse-first; screen readers see the hidden native select rather than the two enhanced boxes. If accessibility is a hard requirement, treat this as decorative and provide a fallback.
  • Debugging checklist. (1) Is jQuery loaded before the plugin? (2) Does the select have multiple? (3) Is the ID unique on the page — the plugin's internal selectors are class-based, and duplicate IDs break init? (4) Did you re-init after replacing options? (5) Does the hidden select still show the right val()? If yes, the problem is CSS, not the plugin.
  • Extension points are thin. There's no event bus, no plugin API beyond destroy/refresh, and options are strings for i18n. For anything richer than a multi-select (async search, create-on-the-fly, grouping), you're fighting the widget — swap packages rather than patching it.
  • Licensing. NOASSERTION on the registry side is a red flag for procurement; the GitHub repo carries a license file, so verify it matches the (WTFPL/MIT-ish) upstream before including in a client deliverable.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core