Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,40 +1,41 @@
<h1 class="text-center">Tree-select with Drag-drop</h1>

<p>
Combine <code>bs-tree-select</code> (in <code>multiple</code> mode) with Angular CDK's
<code>cdkDropList</code> + <code>cdkDrag</code> to let users reorder the selected items by
dragging. The tree-select renders its chips inside its own shadow DOM, so the draggable
list lives <em>alongside</em> it: pick items in the dropdown, then drag to reorder. The new
order is pushed back into <code>[value]</code>, so the chips re-render in the chosen order.
Add the <code>reorderable</code> attribute to a <code>multiple</code> /
<code>checkbox</code> <code>bs-tree-select</code> to let users reorder the selected chips
<strong>directly inside the component</strong> — drag a chip with the pointer, or focus it and
press <kbd>M</kbd> then the arrow keys. The new order is emitted through
<code>valueChange</code>, so the form value stays in sync.
</p>

<p>
Reordering is <em>opt-in</em>: import <code>BsTreeSelectReorderDirective</code> from
<code>&#64;mintplayer/ng-bootstrap/tree-select/reorder</code> and add it to your component's
<code>imports</code>. Without that import the drag-drop code is tree-shaken out and
<code>reorderable</code> stays inert.
</p>

<h2>Basic usage</h2>

<bs-form>
<bs-tree-select
mode="multiple"
reorderable
[provider]="provider"
[value]="selected()"
(valueChange)="onValueChange($event)"
placeholder="Pick technologies">
</bs-tree-select>
</bs-form>

<div
cdkDropList
cdkDropListOrientation="mixed"
(cdkDropListDropped)="onDrop($event)"
class="dnd-list mt-3">
@for (item of selected(); track item.id) {
<span cdkDrag class="dnd-chip">
<span class="drag-handle">&#9776;</span>
{{ item.label }}
<button type="button" class="dnd-remove" (click)="remove(item)" aria-label="Remove">&times;</button>
</span>
} @empty {
<span class="text-muted">Nothing selected yet — pick a few items above.</span>
<p class="mt-3">
Current order:
@if (selected().length) {
<strong>{{ orderLabels() }}</strong>
} @else {
<span class="text-muted">nothing selected yet; pick a few items above.</span>
}
</div>
</p>

<bs-code-snippet [codeToCopy]="snippetBasicHtml" [language]="'html'"></bs-code-snippet>
<bs-code-snippet [codeToCopy]="snippetBasicTs" [language]="'ts'"></bs-code-snippet>
Original file line number Diff line number Diff line change
@@ -1,46 +1,10 @@
.dnd-list {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
min-height: 2.5rem;
padding: 0.5rem;
border: 1px dashed var(--bs-border-color, #dee2e6);
border-radius: var(--bs-border-radius, 0.375rem);
:host {
display: block;
}

.dnd-chip {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.25rem 0.5rem;
kbd {
padding: 0.1rem 0.35rem;
font-size: 0.85em;
background: var(--bs-secondary-bg, #e9ecef);
border-radius: var(--bs-border-radius, 0.375rem);
cursor: grab;
}

.dnd-chip.cdk-drag-preview {
cursor: grabbing;
box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.2);
}

.dnd-chip.cdk-drag-placeholder {
opacity: 0.4;
}

.drag-handle {
color: var(--bs-secondary-color, #6c757d);
}

.dnd-remove {
border: 0;
background: transparent;
color: inherit;
cursor: pointer;
line-height: 1;
padding: 0;
opacity: 0.7;
}

.dnd-remove:hover {
opacity: 1;
border-radius: var(--bs-border-radius-sm, 0.25rem);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
import { CdkDragDrop, DragDropModule, moveItemInArray } from '@angular/cdk/drag-drop';
import { Component, ChangeDetectionStrategy, computed, signal } from '@angular/core';
import { BsCodeSnippetComponent } from '@mintplayer/ng-bootstrap/code-snippet';
import { BsFormComponent } from '@mintplayer/ng-bootstrap/form';
import {
Expand All @@ -8,6 +7,7 @@ import {
type TreeNode,
type TreeSelectProvider,
} from '@mintplayer/ng-bootstrap/tree-select';
import { BsTreeSelectReorderDirective } from '@mintplayer/ng-bootstrap/tree-select/reorder';
import { dedent } from 'ts-dedent';

const SAMPLE_TREE: TreeNode[] = [
Expand All @@ -34,82 +34,60 @@ const SAMPLE_TREE: TreeNode[] = [
selector: 'demo-tree-select-drag-drop',
templateUrl: './tree-select-drag-drop.component.html',
styleUrls: ['./tree-select-drag-drop.component.scss'],
imports: [BsCodeSnippetComponent, BsFormComponent, BsTreeSelectComponent, DragDropModule],
imports: [BsCodeSnippetComponent, BsFormComponent, BsTreeSelectComponent, BsTreeSelectReorderDirective],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TreeSelectDragDropComponent {

protected readonly provider: TreeSelectProvider = new InMemoryTreeSelectProvider(SAMPLE_TREE);
protected readonly selected = signal<TreeNode[]>([]);
protected readonly orderLabels = computed(() => this.selected().map((n) => n.label).join(' → '));

// value-change fires for selection AND reorder, so this stays in sync with the
// chip order shown inside the component.
onValueChange(value: TreeNode | TreeNode[] | null) {
this.selected.set(Array.isArray(value) ? value : value ? [value] : []);
}

onDrop(event: CdkDragDrop<TreeNode[]>) {
const items = [...this.selected()];
moveItemInArray(items, event.previousIndex, event.currentIndex);
// Push the new order back — the tree-select chips re-render in this order.
this.selected.set(items);
}

remove(node: TreeNode) {
this.selected.set(this.selected().filter((n) => n.id !== node.id));
}

protected readonly snippetBasicHtml = dedent`
<bs-form>
<bs-tree-select
mode="multiple"
reorderable
[provider]="provider"
[value]="selected()"
(valueChange)="onValueChange($event)"
placeholder="Pick technologies">
</bs-tree-select>
</bs-form>

<!-- Reorder the selection with CDK drag-drop; the new order flows back. -->
<div cdkDropList cdkDropListOrientation="mixed"
(cdkDropListDropped)="onDrop($event)" class="dnd-list">
@for (item of selected(); track item.id) {
<span cdkDrag class="dnd-chip">
<span class="drag-handle">&#9776;</span>
{{ item.label }}
<button type="button" (click)="remove(item)" aria-label="Remove">&times;</button>
</span>
}
</div>
`;

protected readonly snippetBasicTs = dedent`
import { Component, signal } from '@angular/core';
import { CdkDragDrop, DragDropModule, moveItemInArray } from '@angular/cdk/drag-drop';
import {
BsTreeSelectComponent,
InMemoryTreeSelectProvider,
type TreeNode,
type TreeSelectProvider,
} from '@mintplayer/ng-bootstrap/tree-select';
// Opt-in: importing this directive pulls in the drag-drop code and makes
// \`reorderable\` live. Omit it and the reorder code is tree-shaken away.
import { BsTreeSelectReorderDirective } from '@mintplayer/ng-bootstrap/tree-select/reorder';
import { BsFormComponent } from '@mintplayer/ng-bootstrap/form';

@Component({
selector: 'my-tree-select-drag-drop-demo',
templateUrl: './my-tree-select-drag-drop-demo.component.html',
imports: [BsFormComponent, BsTreeSelectComponent, DragDropModule],
imports: [BsFormComponent, BsTreeSelectComponent, BsTreeSelectReorderDirective],
})
export class MyTreeSelectDragDropDemoComponent {
protected readonly provider: TreeSelectProvider = new InMemoryTreeSelectProvider(MY_TREE);
protected readonly selected = signal<TreeNode[]>([]);

// Reordering chips emits value-change with the new order, so the form
// value (and [(ngModel)] / formControl) updates automatically.
onValueChange(value: TreeNode | TreeNode[] | null) {
this.selected.set(Array.isArray(value) ? value : value ? [value] : []);
}

onDrop(event: CdkDragDrop<TreeNode[]>) {
const items = [...this.selected()];
moveItemInArray(items, event.previousIndex, event.currentIndex);
this.selected.set(items);
}
}
`;
}
Loading
Loading