Angular Sortablejs directive

GitHub license  npm  scope

This package provides Angular binding for Storablejs which works with standard arrays as well as Angular FormArray objects.


Demo


Basic usage

View on GitHub

This 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

Ankara
Ankara description
Moscow
Moscow description
Munich
Munich description
Paris
Paris description
Washington
Washington description

Navigation

The actual model

[
  "Ankara",
  "Moscow",
  "Munich",
  "Paris",
  "Washington"
]
<h3 class="h4">Button groups</h3>
<div class="btn-group-vertical"
[nxtSortablejs]="cities">
<button type="button"
class="btn btn-secondary"
*ngFor="let city of cities">{{ city }}</button>
</div>
<br><br>
<div class="btn-group"
[nxtSortablejs]="cities">
<button type="button"
class="btn btn-secondary"
*ngFor="let city of cities">{{ city }}</button>
</div>
<br><br>
<h3 class="h4">Cards</h3>
<div class="row"
[nxtSortablejs]="cities">
<div class="col mb-4"
*ngFor="let city of cities">
<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">
<li class="nav-item"
*ngFor="let city of cities">
<a class="nav-link active mr-1">{{ city }}</a>
</li>
</ul>
<nav aria-label="breadcrumb"
role="navigation">
<ol class="breadcrumb"
[nxtSortablejs]="cities">
<li class="breadcrumb-item active"
*ngFor="let city of cities">{{ city }}</li>
</ol>
</nav>
<h3 class="h4">The actual model</h3>
<app-code-block [code]="cities | json"
[languages]="['json']"
[copy]="false"></app-code-block>
import { Component, ViewEncapsulation } from '@angular/core'
@Component({
selector: 'app-basic-example',
templateUrl: './basic-example.component.html',
styleUrls: ['./basic-example.component.css'],
encapsulation: ViewEncapsulation.Emulated
})
export class BasicExampleComponent {
cities = [
'Ankara',
'Moscow',
'Munich',
'Paris',
'Washington'
]
}

Sortable FormArray

View on GitHub

Most 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">
<li class="list-group-item"
*ngFor="let city of citiesControls.controls">
<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"></app-code-block>
import { Component, ViewEncapsulation } from '@angular/core'
import { FormArray, FormControl } from '@angular/forms'
@Component({
selector: 'app-form-array',
templateUrl: './form-array.component.html',
styleUrls: ['./form-array.component.css'],
encapsulation: ViewEncapsulation.Emulated
})
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">
<li class="list-group-item"
*ngFor="let item of draggableItems"
[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"></app-code-block>
import { Component, ViewEncapsulation } from '@angular/core'
import { Options } from 'sortablejs'
@Component({
selector: 'app-disabled-options',
templateUrl: './disabled-options.component.html',
styleUrls: ['./disabled-options.component.css'],
encapsulation: ViewEncapsulation.Emulated
})
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 GitHub

Binding 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

Hint: for the FormArray using onUpdate is kinda overhead because the FormArray.valueChanges provides an event on every change.
<app-code-block code="Updated {{ eventUpdateCounter }} times"
[languages]="['text']"
[copy]="false"></app-code-block>
<br>
<ul class="list-group"
[nxtSortablejs]="eventItems"
[config]="eventOptions">
<li class="list-group-item"
*ngFor="let item of eventItems">
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 { Options } from 'sortablejs'
@Component({
selector: 'app-events',
templateUrl: './events.component.html',
styleUrls: ['./events.component.css'],
encapsulation: ViewEncapsulation.Emulated
})
export class EventsComponent {
eventItems = [
'1',
'2',
'3',
'4',
'5'
]
eventUpdateCounter = 0
eventOptions: Options = {
onUpdate: () => this.eventUpdateCounter++
}
}

Scrolling options

View on GitHub

Most 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">
<li class="list-group-item"
*ngFor="let item of scrollableItems">
Element {{ item }}
</li>
</ul>
import { Component, ViewEncapsulation } from '@angular/core'
import { Options } from 'sortablejs'
@Component({
selector: 'app-scrolling-options',
templateUrl: './scrolling-options.component.html',
styleUrls: ['./scrolling-options.component.css'],
encapsulation: ViewEncapsulation.Emulated
})
export class ScrollingOptionsComponent {
scrollableItems = Array.from({ length: 30 })
.map((_, i) => i + 1)
scrollableOptions: Options = {
scroll: true,
scrollSensitivity: 100,
// @ts-ignore Most browsers handle scrolling well, this forces custom scrolling which uses `scrollSensitivity`
forceAutoScrollFallback: true
}
}

Transfer between lists

View on GitHub

These 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">
<li class="list-group-item"
*ngFor="let item of normalList1">Element {{ item }}</li>
</ul>
</div>
<div class="col">
<ul class="list-group sortable"
[nxtSortablejs]="normalList2"
[config]="normalOptions">
<li class="list-group-item"
*ngFor="let item of normalList2">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"></app-code-block>
</div>
<div class="col">
<app-code-block [code]="normalList2 | json"
[languages]="['json']"
[copy]="false"></app-code-block>
</div>
</div>
import { Component, ViewEncapsulation } from '@angular/core'
import { Options } from 'sortablejs'
@Component({
selector: 'app-transfer-lists',
templateUrl: './transfer-lists.component.html',
styleUrls: ['./transfer-lists.component.css'],
encapsulation: ViewEncapsulation.Emulated
})
export class TransferListsComponent {
normalList1 = [
'1',
'2',
'3',
'4',
'5'
]
normalList2 = [
'6',
'7',
'8',
'9',
'10'
]
normalOptions: Options = {
group: 'normal-group'
}
}

Clone items

View on GitHub

The first list is a clone factory, and the second list can be its target.

  • 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">
<li class="list-group-item"
*ngFor="let item of cloneList1">Element {{ item }}</li>
</ul>
</div>
<div class="col">
<ul class="list-group sortable"
[nxtSortablejs]="cloneList2"
[config]="clone2Options">
<li class="list-group-item"
*ngFor="let item of cloneList2">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"></app-code-block>
</div>
<div class="col">
<app-code-block [code]="cloneList2 | json"
[languages]="['json']"
[copy]="false"></app-code-block>
</div>
</div>
import { Component, ViewEncapsulation } from '@angular/core'
import { Options } from 'sortablejs'
@Component({
selector: 'app-item-clone',
templateUrl: './item-clone.component.html',
styleUrls: ['./item-clone.component.css'],
encapsulation: ViewEncapsulation.Emulated
})
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'
}
}

Complex example

View on GitHub

Each list can have different options and restrictions.

1. This list cannot accept items
  • Element 1
  • Element 2
  • Element 3
  • Element 4
  • Element 5
2. This is a normal list
  • Element 6
  • Element 7
  • Element 8
  • Element 9
  • Element 10
3. This list clones its children
  • Element 11
  • Element 12
4. Only items from #1 can be put here
  • 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">
<li class="list-group-item"
*ngFor="let item of list1">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">
<li class="list-group-item"
*ngFor="let item of list2">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">
<li class="list-group-item"
*ngFor="let item of list3">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">
<li class="list-group-item"
*ngFor="let item of list4">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"></app-code-block>
</div>
<div class="col mb-4">
<app-code-block [code]="list2 | json"
[languages]="['json']"
[copy]="false"></app-code-block>
</div>
<div class="col mb-4">
<app-code-block [code]="list3 | json"
[languages]="['json']"
[copy]="false"></app-code-block>
</div>
<div class="col mb-4">
<app-code-block [code]="list4 | json"
[languages]="['json']"
[copy]="false"></app-code-block>
</div>
</div>
import { Component, ViewEncapsulation } from '@angular/core'
import { Options } from 'sortablejs'
@Component({
selector: 'app-complex-example',
templateUrl: './complex-example.component.html',
styleUrls: ['./complex-example.component.css'],
encapsulation: ViewEncapsulation.Emulated
})
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']
}
}
}