Angular Sortablejs directive
This package provides Angular binding for Storablejs which works with standard arrays as well as Angular FormArray objects.
Demo
Basic usage
View on GitHubThis examples shows various elements that can be sortable. They share the same model, so reordering elements in one container propagates to all other containers in the example.
Button groups
Cards
Navigation
The actual model
[
"Ankara",
"Moscow",
"Munich",
"Paris",
"Washington"
]<h3 class="h4">Button groups</h3> <div class="btn-group-vertical" [nxtSortablejs]="cities"> @for (city of cities; track city) { <button type="button" class="btn btn-secondary">{{ city }}</button> } </div> <br><br> <div class="btn-group" [nxtSortablejs]="cities"> @for (city of cities; track city) { <button type="button" class="btn btn-secondary">{{ city }}</button> } </div> <br><br> <h3 class="h4">Cards</h3> <div class="row" [nxtSortablejs]="cities"> @for (city of cities; track city) { <div class="col mb-4"> <div class="card"> <div class="card-header">{{ city }}</div> <div class="card-body">{{ city }} description</div> </div> </div> } </div> <h3 class="h4">Navigation</h3> <ul class="nav nav-pills mb-3" [nxtSortablejs]="cities"> @for (city of cities; track city) { <li class="nav-item"> <a class="nav-link active mr-1">{{ city }}</a> </li> } </ul> <nav aria-label="breadcrumb" role="navigation"> <ol class="breadcrumb" [nxtSortablejs]="cities"> @for (city of cities; track city) { <li class="breadcrumb-item active">{{ city }}</li> } </ol> </nav> <h3 class="h4">The actual model</h3> <app-code-block [code]="cities | json" [languages]="['json']" [copy]="false" />
import { JsonPipe } from '@angular/common' import { Component, ViewEncapsulation } from '@angular/core' import { SortablejsDirective } from 'nxt-sortablejs' import { CodeBlockComponent } from '../../../code-block/code-block.component' @Component({ selector: 'app-basic-example', templateUrl: './basic-example.component.html', styleUrls: ['./basic-example.component.css'], encapsulation: ViewEncapsulation.Emulated, imports: [ SortablejsDirective, CodeBlockComponent, JsonPipe ] }) export class BasicExampleComponent { cities = [ 'Ankara', 'Moscow', 'Munich', 'Paris', 'Washington' ] }
Sortable FormArray
View on GitHubMost of the time one wants to sort something more than just data. Complex form inputs with FormArray controls are also supported.
FormArray value
[
"Ankara",
"Moscow",
"Munich",
"Paris",
"Washington"
]<ul class="list-group" [nxtSortablejs]="citiesControls"> @for (city of citiesControls.controls; track city) { <li class="list-group-item"> <input class="form-control" [formControl]="city"> </li> } </ul> <br> <h3 class="h4">FormArray value</h3> <app-code-block [code]="citiesControls.value | json" [languages]="['json']" [copy]="false" />
import { JsonPipe } from '@angular/common' import { Component, ViewEncapsulation } from '@angular/core' import { FormArray, FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms' import { SortablejsDirective } from 'nxt-sortablejs' import { CodeBlockComponent } from '../../../code-block/code-block.component' @Component({ selector: 'app-form-array', templateUrl: './form-array.component.html', styleUrls: ['./form-array.component.css'], encapsulation: ViewEncapsulation.Emulated, imports: [ SortablejsDirective, CodeBlockComponent, FormsModule, ReactiveFormsModule, JsonPipe ] }) export class FormArrayComponent { citiesControls = new FormArray([ 'Ankara', 'Moscow', 'Munich', 'Paris', 'Washington' ].map(city => new FormControl(city))) }
Disabled options
View on GitHub- Element 1
- Element 2
- Element 3
- Element 4
- Element 5
The actual model
[
{
"draggable": true,
"text": "1"
},
{
"draggable": true,
"text": "2"
},
{
"draggable": false,
"text": "3"
},
{
"draggable": true,
"text": "4"
},
{
"draggable": true,
"text": "5"
}
]<ul class="list-group" [nxtSortablejs]="draggableItems" [config]="draggableOptions"> @for (item of draggableItems; track item.text) { <li class="list-group-item" [class.draggable]="item.draggable" [class.disabled]="!item.draggable"> Element {{ item.text }} </li> } </ul> <br> <h3 class="h4">The actual model</h3> <app-code-block [code]="draggableItems | json" [languages]="['json']" [copy]="false" />
import { JsonPipe } from '@angular/common' import { Component, ViewEncapsulation } from '@angular/core' import { SortablejsDirective } from 'nxt-sortablejs' import { Options } from 'sortablejs' import { CodeBlockComponent } from '../../../code-block/code-block.component' @Component({ selector: 'app-disabled-options', templateUrl: './disabled-options.component.html', styleUrls: ['./disabled-options.component.css'], encapsulation: ViewEncapsulation.Emulated, imports: [ SortablejsDirective, CodeBlockComponent, JsonPipe ] }) export class DisabledOptionsComponent { draggableItems = [ { draggable: true, text: '1' }, { draggable: true, text: '2' }, { draggable: false, text: '3' }, { draggable: true, text: '4' }, { draggable: true, text: '5' } ] draggableOptions: Options = { draggable: '.draggable' } }
Events
View on GitHubBinding to the events is easy. Any even could be attached as a property of Sortablejs. The example below binds to the onUpdate event. Drag the items and track the amount of updates.
Updated 0 times- Element 1
- Element 2
- Element 3
- Element 4
- Element 5
<app-code-block code="Updated {{ eventUpdateCounter }} times" [languages]="['text']" [copy]="false" /> <br> <ul class="list-group" [nxtSortablejs]="eventItems" [config]="eventOptions"> @for (item of eventItems; track item) { <li class="list-group-item"> Element {{ item }} </li> } </ul> <br> <div class="alert alert-info">Hint: for the <em>FormArray</em> using <em>onUpdate</em> is kinda overhead because the <em>FormArray.valueChanges</em> provides an event on every change.</div>
import { Component, ViewEncapsulation } from '@angular/core' import { SortablejsDirective } from 'nxt-sortablejs' import { Options } from 'sortablejs' import { CodeBlockComponent } from '../../../code-block/code-block.component' @Component({ selector: 'app-events', templateUrl: './events.component.html', styleUrls: ['./events.component.css'], encapsulation: ViewEncapsulation.Emulated, imports: [ SortablejsDirective, CodeBlockComponent ] }) export class EventsComponent { eventItems = [ '1', '2', '3', '4', '5' ] eventUpdateCounter = 0 eventOptions: Options = { onUpdate: () => this.eventUpdateCounter++ } }
Scrolling options
View on GitHubMost browsers automatically scroll content as you drag items to the edge of the screen. If the default behaviour doesn't work for you, Sortablejs provides an option to cofigure scrolling.
- Element 1
- Element 2
- Element 3
- Element 4
- Element 5
- Element 6
- Element 7
- Element 8
- Element 9
- Element 10
- Element 11
- Element 12
- Element 13
- Element 14
- Element 15
- Element 16
- Element 17
- Element 18
- Element 19
- Element 20
- Element 21
- Element 22
- Element 23
- Element 24
- Element 25
- Element 26
- Element 27
- Element 28
- Element 29
- Element 30
<ul class="list-group" [nxtSortablejs]="scrollableItems" [config]="scrollableOptions"> @for (item of scrollableItems; track item) { <li class="list-group-item"> Element {{ item }} </li> } </ul>
import { ChangeDetectorRef, Component, inject, ViewEncapsulation } from '@angular/core' import { SortablejsDirective } from 'nxt-sortablejs' import { Options } from 'sortablejs' @Component({ selector: 'app-scrolling-options', templateUrl: './scrolling-options.component.html', styleUrls: ['./scrolling-options.component.css'], encapsulation: ViewEncapsulation.Emulated, imports: [ SortablejsDirective ] }) export class ScrollingOptionsComponent { private readonly cdRef = inject(ChangeDetectorRef) scrollableItems = Array.from({ length: 30 }) .map((_, i) => i + 1) scrollableOptions: Options = { scroll: true, scrollSensitivity: 100, // Most browsers handle scrolling well, this forces custom scrolling which uses `scrollSensitivity` forceAutoScrollFallback: true, onUpdate: () => { // Detect changes only when neccesary this.cdRef.detectChanges() } } }
Transfer between lists
View on GitHubThese lists are connected together. You can drag / drop elements across the lists.
- Element 1
- Element 2
- Element 3
- Element 4
- Element 5
- Element 6
- Element 7
- Element 8
- Element 9
- Element 10
List states
[
"1",
"2",
"3",
"4",
"5"
][
"6",
"7",
"8",
"9",
"10"
]<div class="row"> <div class="col"> <ul class="list-group sortable" [nxtSortablejs]="normalList1" [config]="normalOptions"> @for (item of normalList1; track item) { <li class="list-group-item">Element {{ item }}</li> } </ul> </div> <div class="col"> <ul class="list-group sortable" [nxtSortablejs]="normalList2" [config]="normalOptions"> @for (item of normalList2; track item) { <li class="list-group-item">Element {{ item }}</li> } </ul> </div> </div> <br> <h3 class="h4">List states</h3> <div class="row"> <div class="col"> <app-code-block [code]="normalList1 | json" [languages]="['json']" [copy]="false" /> </div> <div class="col"> <app-code-block [code]="normalList2 | json" [languages]="['json']" [copy]="false" /> </div> </div>
import { JsonPipe } from '@angular/common' import { Component, ViewEncapsulation } from '@angular/core' import { SortablejsDirective } from 'nxt-sortablejs' import { Options } from 'sortablejs' import { CodeBlockComponent } from '../../../code-block/code-block.component' @Component({ selector: 'app-transfer-lists', templateUrl: './transfer-lists.component.html', styleUrls: ['./transfer-lists.component.css'], encapsulation: ViewEncapsulation.Emulated, imports: [ SortablejsDirective, CodeBlockComponent, JsonPipe ] }) export class TransferListsComponent { normalList1 = [ '1', '2', '3', '4', '5' ] normalList2 = [ '6', '7', '8', '9', '10' ] normalOptions: Options = { group: 'normal-group' } }
- Element 1
- Element 2
- Element 3
- Element 4
- Element 5
- Element 6
- Element 7
- Element 8
- Element 9
- Element 10
List states
[
"1",
"2",
"3",
"4",
"5"
][
"6",
"7",
"8",
"9",
"10"
]<div class="row"> <div class="col"> <ul class="list-group sortable" [nxtSortablejs]="cloneList1" [config]="clone1Options"> @for (item of cloneList1; track item) { <li class="list-group-item">Element {{ item }}</li> } </ul> </div> <div class="col"> <ul class="list-group sortable" [nxtSortablejs]="cloneList2" [config]="clone2Options"> @for (item of cloneList2; track item) { <li class="list-group-item">Element {{ item }}</li> } </ul> </div> </div> <br> <h3 class="h4">List states</h3> <div class="row"> <div class="col"> <app-code-block [code]="cloneList1 | json" [languages]="['json']" [copy]="false" /> </div> <div class="col"> <app-code-block [code]="cloneList2 | json" [languages]="['json']" [copy]="false" /> </div> </div>
import { JsonPipe } from '@angular/common' import { Component, ViewEncapsulation } from '@angular/core' import { SortablejsDirective } from 'nxt-sortablejs' import { Options } from 'sortablejs' import { CodeBlockComponent } from '../../../code-block/code-block.component' @Component({ selector: 'app-item-clone', templateUrl: './item-clone.component.html', styleUrls: ['./item-clone.component.css'], encapsulation: ViewEncapsulation.Emulated, imports: [ SortablejsDirective, CodeBlockComponent, JsonPipe ] }) export class ItemCloneComponent { cloneList1 = [ '1', '2', '3', '4', '5' ] cloneList2 = [ '6', '7', '8', '9', '10' ] clone1Options: Options = { group: { name: 'clone-group', pull: 'clone', put: false } } clone2Options: Options = { group: 'clone-group' } }
- Element 1
- Element 2
- Element 3
- Element 4
- Element 5
- Element 6
- Element 7
- Element 8
- Element 9
- Element 10
- Element 11
- Element 12
- Element 13
List states
[
"1",
"2",
"3",
"4",
"5"
][
"6",
"7",
"8",
"9",
"10"
][
"11",
"12"
][
"13"
]<div class="row"> <div class="col mb-4"> <strong>1. This list cannot accept items</strong> <ul class="mt-2 list-group sortable" [nxtSortablejs]="list1" [config]="list1Options"> @for (item of list1; track item) { <li class="list-group-item">Element {{ item }}</li> } </ul> </div> <div class="col mb-4"> <strong>2. This is a <em>normal</em> list</strong> <ul class="mt-2 list-group sortable" [nxtSortablejs]="list2" [config]="list2Options"> @for (item of list2; track item) { <li class="list-group-item">Element {{ item }}</li> } </ul> </div> <div class="col mb-4"> <strong>3. This list clones its children</strong> <ul class="mt-2 list-group sortable" [nxtSortablejs]="list3" [config]="list3Options"> @for (item of list3; track item) { <li class="list-group-item">Element {{ item }}</li> } </ul> </div> <div class="col mb-4"> <strong>4. Only items from #1 can be put here</strong> <ul class="mt-2 list-group sortable" [nxtSortablejs]="list4" [config]="list4Options"> @for (item of list4; track item) { <li class="list-group-item">Element {{ item }}</li> } </ul> </div> </div> <h3 class="h4">List states</h3> <div class="row"> <div class="col mb-4"> <app-code-block [code]="list1 | json" [languages]="['json']" [copy]="false" /> </div> <div class="col mb-4"> <app-code-block [code]="list2 | json" [languages]="['json']" [copy]="false" /> </div> <div class="col mb-4"> <app-code-block [code]="list3 | json" [languages]="['json']" [copy]="false" /> </div> <div class="col mb-4"> <app-code-block [code]="list4 | json" [languages]="['json']" [copy]="false" /> </div> </div>
import { JsonPipe } from '@angular/common' import { Component, ViewEncapsulation } from '@angular/core' import { SortablejsDirective } from 'nxt-sortablejs' import { Options } from 'sortablejs' import { CodeBlockComponent } from '../../../code-block/code-block.component' @Component({ selector: 'app-complex-example', templateUrl: './complex-example.component.html', styleUrls: ['./complex-example.component.css'], encapsulation: ViewEncapsulation.Emulated, imports: [ SortablejsDirective, CodeBlockComponent, JsonPipe ] }) export class ComplexExampleComponent { list1 = [ '1', '2', '3', '4', '5' ] list2 = [ '6', '7', '8', '9', '10' ] list3 = [ '11', '12' ] list4 = [ '13' ] list1Options: Options = { group: { name: 'group1', put: false } } list2Options: Options = { group: { name: 'group2', put: ['group1', 'group2'] } } list3Options: Options = { group: { name: 'group2', pull: 'clone', put: ['group1', 'group2'], revertClone: true } } list4Options: Options = { group: { name: 'group2', put: ['group1'] } } }
Union example
View on GitHubThe list items can be of different types (the element type can be a union type).
The actual model
[
1,
2,
[
3,
4
]
]<div class="btn-group-vertical" [nxtSortablejs]="elems"> @for (elem of elems; track elem) { <button type="button" class="btn btn-secondary">Element {{ isArray(elem) ? (elem | json) : elem }}</button> } </div> <br><br> <h3 class="h4">The actual model</h3> <app-code-block [code]="elems | json" [languages]="['json']" [copy]="false" />
import { JsonPipe } from '@angular/common' import { Component, ViewEncapsulation } from '@angular/core' import { SortablejsDirective } from 'nxt-sortablejs' import { CodeBlockComponent } from '../../../code-block/code-block.component' @Component({ selector: 'app-union-example', templateUrl: './union-example.component.html', styleUrls: ['./union-example.component.css'], encapsulation: ViewEncapsulation.Emulated, imports: [ SortablejsDirective, CodeBlockComponent, JsonPipe ] }) export class UnionExampleComponent { readonly elems = [ 1, 2, [3, 4] ] readonly isArray = Array.isArray }
Table items
View on GitHubTable rows are nested inside tbody, and require the container selector to be set accordingly.
| City | Description |
|---|---|
| Ankara | Ankara description |
| Moscow | Moscow description |
| Munich | Munich description |
| Paris | Paris description |
| Washington | Washington description |
<table cdk-table [dataSource]="cities" [nxtSortablejs]="cities" [sortablejsContainer]="'tbody'" class="table"> <tr cdk-header-row *cdkHeaderRowDef="['city', 'description']"></tr> <tr cdk-row *cdkRowDef="let row; columns: ['city', 'description']"></tr> <ng-container cdkColumnDef="city"> <th cdk-header-cell *cdkHeaderCellDef>City</th> <td cdk-cell *cdkCellDef="let element">{{ element }}</td> </ng-container> <ng-container cdkColumnDef="description"> <th cdk-header-cell *cdkHeaderCellDef>Description</th> <td cdk-cell *cdkCellDef="let element">{{ element }} description</td> </ng-container> </table>
import { CdkTableModule } from '@angular/cdk/table' import { Component, ViewEncapsulation } from '@angular/core' import { SortablejsDirective } from 'nxt-sortablejs' @Component({ selector: 'app-table', templateUrl: './table.component.html', styleUrls: ['./table.component.css'], encapsulation: ViewEncapsulation.Emulated, imports: [ SortablejsDirective, CdkTableModule ] }) export class TableComponent { cities = [ 'Ankara', 'Moscow', 'Munich', 'Paris', 'Washington' ] }
nxt-components