TreeViewDropdown — Inside a modal

Single selection commits immediately by default. Multiple selection and checked values use Apply and Cancel. The dropdown retains the underlying control's stable identities, payloads, checking, filtering, lazy loading, drag and drop, and change acknowledgments.

Behavior and integration

selection-presentation="tokens" changes how selected values are displayed; selection-mode="multiple" independently permits several selections. The modal example enables both. token-label-mode="path" disambiguates tree nodes by their ancestry; ListView tokens use item labels. Token removal uses existing selection/check events, commitment, form binding and change acknowledgments. remove-token-text="Remove {label}" localizes each removal button's accessible name.

Draft values are visible in the picker; getValue(), form fields and createSubmission() expose committed values. getDraftValue() inspects provisional choices. Apply commits a value; an application must still save its submission and acknowledge the server response. Cancel restores the chosen value without undoing renames, moves or loaded children.

Use the trigger with Enter, Space or arrows. Escape cancels the picker. Automatic presentation uses a native dialog on small screens and touch devices, without constructing a Bootstrap modal. Desktop popups stay within the viewport. Read only permits inspection; disabled prevents opening. Required validation uses the committed value.

Load Bootstrap's bundle, the corresponding TreeView or ListView stylesheet, and view/bs-view-dropdown.css. The TagHelper captures its auto module. Children remain bs-treeview-node or bs-listview-item. Start slots accept icons; pills remain at the near/far end.

Actual Razor

aspnet
@model ViewDropdownExampleModel
<bs-treeview-dropdown id="dropdown-demo" dropdown-label="Destination" dropdown-placeholder="Choose a destination…"
    source="dom" selection-mode="@(Model.InModal ? "multiple" : "single")" search="true" popup-height="400" required="true"
    selection-presentation="@(Model.InModal ? "tokens" : "text")" token-label-mode="path"
    commit-mode="@(Model.InModal ? "explicit" : "auto")" selection-bound-name="Destination"
    submission-bound-name="Changes" track-changes="true" event-committed="viewDropdownExample.committed">
    <bs-treeview-node node-id="operations" label="Operations" expanded="true">
        <bs-treeview-node node-id="accounting" label="Accounting" subtitle="General ledger and reporting" bound-value="ACCT">
            <bs-start-icon class="bi bi-journal" intent="decorative" />
            <bs-end-near-pill text="Ready" intent="informative" description="Accepting new entries" />
        </bs-treeview-node>
        <bs-treeview-node node-id="billing" label="Billing" subtitle="Invoices and aging" bound-value="BILL" />
    </bs-treeview-node>
    <bs-treeview-node node-id="administration" label="Administration" expanded="true">
        <bs-treeview-node node-id="configuration" label="Configuration" subtitle="Settings and grouping" bound-value="CONFIG" />
        <bs-treeview-node node-id="archived" label="Archived" disabled="true" />
    </bs-treeview-node>
</bs-treeview-dropdown>
Example wrapper, form and containing modal
aspnet
@model ViewDropdownExampleModel
<section id="dropdown-example" data-kind="@Model.Kind">
    @if (Model.InModal)
    {
        <button type="button" class="btn btn-primary mb-3" data-bs-toggle="modal" data-bs-target="#dropdown-owner">Open containing modal</button>
        <div id="dropdown-owner" class="modal fade" tabindex="-1" aria-labelledby="dropdown-owner-title">
            <div class="modal-dialog modal-dialog-scrollable"><div class="modal-content">
                <div class="modal-header"><h2 id="dropdown-owner-title" class="modal-title h5">Edit a notice</h2>
                    <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close editor"></button></div>
                <div class="modal-body">
                    <label class="form-label" for="owner-notes">Unsaved editor notes</label>
                    <input id="owner-notes" class="form-control mb-3" value="This state survives opening the picker." />
                    <form id="dropdown-example-form">
                        @await Html.PartialAsync("~/Views/Docs/_" + Model.Name + "GeneralControl.cshtml", Model)
                    </form>
                    <p class="small mt-3">On a phone, the picker opens in its own native dialog. Escape closes the picker first. This editor remains open.</p>
                </div>
                <div class="modal-footer"><button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close editor</button></div>
            </div></div>
        </div>
    }
    else
    {
        <form id="dropdown-example-form">
            @await Html.PartialAsync("~/Views/Docs/_" + Model.Name + "GeneralControl.cshtml", Model)
        </form>
    }
    <div class="d-flex flex-wrap gap-3 align-items-center my-3">
        <label for="dropdown-presentation">Presentation</label>
        <select id="dropdown-presentation" class="form-select w-auto">
            <option value="auto">Automatic</option><option value="dropdown">Anchored popup</option><option value="dialog">Native dialog</option>
        </select>
        <label for="dropdown-selection-presentation">Selected values</label>
        <select id="dropdown-selection-presentation" class="form-select w-auto">
            <option value="text" selected="@(!Model.InModal)">Text</option><option value="tokens" selected="@(Model.InModal)">Removable tokens</option>
        </select>
        <label><input type="checkbox" class="form-check-input" id="dropdown-multiple" checked="@(Model.InModal)" /> Multiple selection</label>
        <label><input type="checkbox" class="form-check-input" id="dropdown-readonly" /> Read only</label>
        <label><input type="checkbox" class="form-check-input" id="dropdown-disabled" /> Disabled</label>
    </div>
    <div class="d-flex flex-wrap gap-2 mb-3">
        <button id="dropdown-validate" type="button" class="btn btn-outline-primary">Validate selection</button>
        <button id="dropdown-form-values" type="button" class="btn btn-outline-secondary">Inspect form values</button>
        <button id="dropdown-reset" type="button" class="btn btn-outline-danger">Reset this example</button>
    </div>
    <p id="dropdown-status" role="status">Choose an option.</p>
    <p class="small">Choose removable tokens and multiple selection to edit several values. Click selects one row; Ctrl/⌘-click toggles additional rows, and Shift selects a range. Checkboxes appear only when checking is configured. Remove a token with its × button or Delete/Backspace; arrow keys, Home and End move between tokens. The field shows committed values, while selected rows in the open popup show the draft. The phone dialog also shows draft tokens. Apply commits; Cancel restores the previous values. Removing a closed token requests commitment immediately. Required validation can reject the last removal.</p>
    <details open><summary>Committed value and latest operation</summary><pre id="dropdown-result"></pre></details>
</section>
<script type="module" src="/js/view-dropdown-example.js"></script>
JavaScript: values, validation and reset
javascript
import { TreeViewDropdown } from '/_content/CopelandSyst.BootstrapComponents/lib/Bootstrap/dist/treeview/bs-treeview-dropdown.js';
import { ListViewDropdown } from '/_content/CopelandSyst.BootstrapComponents/lib/Bootstrap/dist/listview/bs-listview-dropdown.js';

const example = document.getElementById('dropdown-example');
const status = document.getElementById('dropdown-status');
const result = document.getElementById('dropdown-result');
let view;
window.viewDropdownExample = {
    committed(event) {
        status.textContent = event.detail.value.ids.length ? 'Value committed. No server save is configured on this General page.' : 'Selection cleared.';
        result.textContent = JSON.stringify(event.detail.value, null, 2);
    }
};
view = (example.dataset.kind === 'treeview' ? TreeViewDropdown : ListViewDropdown).getOrCreateInstance(document.getElementById('dropdown-demo'));
const initialOptions = view.getDropdownOptions();
const initialMultiple = document.getElementById('dropdown-multiple').checked;
document.getElementById('dropdown-presentation').addEventListener('change', event => view.setDropdownOptions({ presentation: event.target.value }));
document.getElementById('dropdown-selection-presentation').addEventListener('change', event => view.setDropdownOptions({ selectionPresentation: event.target.value }));
document.getElementById('dropdown-multiple').addEventListener('change', async event => {
    if (view.isOpen && !await view.cancel('selection-mode')) { event.target.checked = !event.target.checked; return; }
    view.setSelectionMode(event.target.checked ? 'multiple' : 'single');
});
document.getElementById('dropdown-readonly').addEventListener('change', event => view.setReadonly(event.target.checked));
document.getElementById('dropdown-disabled').addEventListener('change', event => view.setDisabled(event.target.checked));
document.getElementById('dropdown-validate').addEventListener('click', () => {
    status.textContent = view.reportValidity() ? 'The committed selection is valid.' : 'A committed selection is required.';
});
document.getElementById('dropdown-form-values').addEventListener('click', () => {
    result.textContent = JSON.stringify([...new FormData(document.getElementById('dropdown-example-form'))]
        .map(([name, value]) => ({ name, value: name === 'Changes' ? JSON.parse(value) : value })), null, 2);
    status.textContent = 'Form values captured. Draft choices are excluded.';
});
document.getElementById('dropdown-reset').addEventListener('click', () => {
    view.setDisabled(false); view.setReadonly(false); view.setSelectionMode(initialMultiple ? 'multiple' : 'single');
    view.resetChanges(); view.setDropdownOptions(initialOptions);
    document.getElementById('dropdown-disabled').checked = false;
    document.getElementById('dropdown-readonly').checked = false;
    document.getElementById('dropdown-presentation').value = 'auto';
    document.getElementById('dropdown-selection-presentation').value = initialOptions.selectionPresentation;
    document.getElementById('dropdown-multiple').checked = initialMultiple;
    const notes = document.getElementById('owner-notes');
    if (notes) notes.value = notes.defaultValue;
    status.textContent = 'Example reset.'; result.textContent = JSON.stringify(view.getValue(), null, 2);
});
result.textContent = JSON.stringify(view.getValue(), null, 2);

The Ajax examples under TreeViewDropdown demonstrate committed selection, checking, reordering, renaming and recovery. Reset affects that example only.