npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

ng-hub-ui-sortable

v22.1.1

Published

Angular bindings for Sortable.js (ng-hub-ui fork of @worktile/ngx-sortablejs)

Readme

ng-hub-ui-sortable

Español | English

NPM Version

Documentation and Live Examples

This package is part of Hub UI, a collection of Angular component libraries for standalone apps.

  • Docs: https://hubui.dev/sortable/overview/
  • Live examples: https://hubui.dev/sortable/examples/
  • Hub UI: https://hubui.dev/

🧩 Library Family ng-hub-ui

This library is part of the ng-hub-ui ecosystem:

Description

ng-hub-ui-sortable provides a modern Angular integration for SortableJS, making it easy to build drag-and-drop interfaces with a directive-first API. You can turn any list into a sortable experience by adding a single attribute to your template.

The library supports simple list reordering as well as advanced scenarios such as nested lists, cross-list transfers, item cloning, Angular Reactive Forms (FormArray) integration, Angular Signals (WritableSignal) support, and deep customization through SortableJS options and events. Each drag-and-drop interaction stays synchronized with your data model so the UI remains reactive and predictable.

This package is a fork of @worktile/ngx-sortablejs, keeping the proven API while updating the branding, metadata, and Angular integration to match the ng-hub-ui family.

Features

  • Directive-based - Simple directive API for adding sortable functionality to any container
  • Array binding - Automatically syncs drag-and-drop operations with your data array
  • Manual control mode - Opt-in manual array management for full control (similar to Angular CDK)
  • Signal support - Native integration with Angular writable signals for reactive state management
  • FormArray support - Native integration with Angular Reactive Forms FormArray
  • Full SortableJS API - Access to all SortableJS options and events
  • Zone integration - Events are properly proxied into Angular's zone for predictable change detection
  • Clone mode - Support for cloning items with custom clone functions
  • Multi-list support - Drag items between multiple connected lists
  • TypeScript support - Full type safety with proper typings

Installation

# Install the component and SortableJS
npm install ng-hub-ui-sortable sortablejs

# Install types for development
npm install -D @types/sortablejs

Or using yarn:

yarn add ng-hub-ui-sortable sortablejs
yarn add -D @types/sortablejs

Peer Requirements

  • Angular >=18.0.0
  • SortableJS >=1.7.0

Quick Start

Here's a quick example to get you started with ng-hub-ui-sortable. The directive is standalone, so you import it directly — no module required.

1. Import the standalone directive

import { Component } from "@angular/core";
import { SortableDirective } from "ng-hub-ui-sortable";

@Component({
  selector: "app-sortable-demo",
  standalone: true,
  imports: [SortableDirective],
  template: `
    <div [hubSortable]="items" [options]="{ animation: 150 }">
      @for (item of items; track item) {
        <div class="sortable-item">{{ item }}</div>
      }
    </div>
  `,
})
export class SortableDemoComponent {
  items = ["Item 1", "Item 2", "Item 3", "Item 4", "Item 5"];
}

2. (Optional) Share global options

To apply the same SortableJS options to every directive in the app, register them once with the standalone provideSortable() provider:

import { bootstrapApplication } from "@angular/platform-browser";
import { provideSortable } from "ng-hub-ui-sortable";

bootstrapApplication(AppComponent, {
  providers: [provideSortable({ animation: 150, ghostClass: "sortable-ghost" })],
});

Usage

Standalone directive (recommended)

import { Component } from "@angular/core";
import { SortableDirective } from "ng-hub-ui-sortable";

@Component({
  selector: "app-sortable-list",
  standalone: true,
  imports: [SortableDirective],
  template: `
    <div [hubSortable]="items" [options]="{ animation: 150 }">
      @for (item of items; track item) {
        <div class="sortable-item">{{ item }}</div>
      }
    </div>
  `,
})
export class SortableListComponent {
  items = ["Item 1", "Item 2", "Item 3"];
}

Global options with provideSortable() (recommended)

import { provideSortable } from "ng-hub-ui-sortable";

bootstrapApplication(AppComponent, {
  providers: [provideSortable({ animation: 150 })],
});

NgModule (deprecated, backwards compatibility only)

Deprecated. SortableModule and SortableModule.forRoot() are kept only for legacy NgModule applications. Prefer the standalone SortableDirective and provideSortable().

import { NgModule } from "@angular/core";
import { SortableModule } from "ng-hub-ui-sortable";

@NgModule({
  imports: [SortableModule.forRoot({ animation: 150 })],
})
export class AppModule {}

Directive API

Primary Inputs

| Input | Type | Description | | ----------------- | ------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- | | items | any[], FormArray, or WritableSignal<any[]> | Model binding that stays in sync with drag-and-drop operations (used with alias [hubSortable]) | | container | string | Optional CSS selector for the real sortable container when the host is wrapped by another component | | options | Options | Native SortableJS options object. Provide a new object reference to trigger option updates | | cloneFunction | (item: any) => any | Custom clone function for clone mode. Allows you to customize how items are cloned | | autoUpdateArray | boolean | Controls automatic array updates. When true (default), arrays update automatically. When false, you have full control (similar to Angular CDK) |

SortableJS Option Inputs

All SortableJS options can be passed either through the [options] input or as individual inputs:

| Input | Type | Description | | ----------------------- | -------------------- | --------------------------------------------------------------------------------- | | group | string \| object | Group name or options for dragging between lists | | sort | boolean | Enable/disable sorting within the list | | delay | number | Time in milliseconds to define when sorting should start | | disabled | boolean | Disable the sortable if set to true | | draggable | string | CSS selector for draggable items within the container | | handle | string | CSS selector for drag handle within list items | | animation | number | Animation speed in milliseconds when sorting | | ghostClass | string | CSS class applied to the ghost element during drag | | chosenClass | string | CSS class applied to the chosen element | | dragClass | string | CSS class applied to the dragging element | | fallbackOnBody | boolean | Append ghost element to document body | | fallbackTolerance | number | Number of pixels a point should move before triggering drag | | fallbackClass | string | CSS class applied when using forceFallback | | fallbackOffset | object | Fallback offset configuration | | forceFallback | boolean | Force the fallback to activate | | filter | string \| function | CSS selector or function to filter items that should not be draggable | | preventOnFilter | boolean | Call preventDefault on filter event | | direction | string | Direction of Sortable ('vertical' or 'horizontal', auto-detected if not provided) | | swapThreshold | number | Threshold of swap zone (0-1) | | invertSwap | boolean | Inverts swap threshold direction | | invertedSwapThreshold | number | Threshold when swapping direction is inverted | | removeCloneOnHide | boolean | Remove clone element when not showing | | ignore | string | CSS selector for elements to ignore | | touchStartThreshold | number | Number of pixels a point should move before cancelling a delayed drag event | | emptyInsertThreshold | number | Distance mouse must be from empty sortable to insert drag element into it | | dropBubble | boolean | Enable drop bubble | | dragoverBubble | boolean | Enable dragover bubble | | dataIdAttr | string | HTML attribute that defines the data id | | delayOnTouchOnly | boolean | Only delay on touch devices | | easing | string | Easing for animation (e.g., 'cubic-bezier(1, 0, 0, 1)') | | setData | function | Function to set data for dragover/drop events | | store | object | Store module for saving and restoring the sort order |

Outputs

All outputs emit events that are proxied through Angular's zone for proper change detection:

| Output | Type | Description | | ------------- | ---------------------------------------------------------- | ------------------------------------------------------------- | | init | EventEmitter<Sortable> | Emits the instantiated Sortable instance on initialization | | start | EventEmitter<SortableEvent> | Fired when dragging starts | | end | EventEmitter<SortableEvent> | Fired when dragging ends | | add | EventEmitter<SortableEvent> | Element is added from another list | | remove | EventEmitter<SortableEvent> | Element is removed to another list | | update | EventEmitter<SortableEvent> | Element position is updated within the same list | | sortEvent | EventEmitter<SortableEvent> | Called when the list is sorted (any change in order) | | filterEvent | EventEmitter<SortableEvent> | Called when an attempt is made to drag a filtered element | | change | EventEmitter<SortableEvent> | Called when list changes by adding or removing an item | | choose | EventEmitter<SortableEvent> | Element is chosen (mouse down on draggable element) | | unchoose | EventEmitter<SortableEvent> | Element is unchosen (mouse up without drag) | | clone | EventEmitter<SortableEvent> | Element is cloned when dragging between lists with clone mode | | move | EventEmitter<{ event: MoveEvent; originalEvent: Event }> | Called during drag move with move event details |

SortableJS Options

All SortableJS options can be passed via options. They are proxied into Angular's zone to keep change detection predictable.

Common Options

import { Options } from "ng-hub-ui-sortable";

interface SortableOptions extends Options {
  animation?: number; // Animation speed in ms
  handle?: string; // CSS selector for drag handle
  filter?: string; // CSS selector for elements to ignore
  draggable?: string; // CSS selector for draggable items
  ghostClass?: string; // Class for the drop placeholder
  chosenClass?: string; // Class for the chosen item
  dragClass?: string; // Class for the dragging item
  group?: string | object; // Group name for multi-list drag
  sort?: boolean; // Enable sorting within list
  disabled?: boolean; // Disable the sortable
  // ... and many more
}

Event Callbacks

{
  onStart: (event) => { /* Drag started */ },
  onEnd: (event) => { /* Drag ended */ },
  onAdd: (event) => { /* Item added from another list */ },
  onRemove: (event) => { /* Item removed to another list */ },
  onUpdate: (event) => { /* Item order changed within list */ },
  onSort: (event) => { /* Any sorting change */ },
  onChange: (event) => { /* Item moved within or between lists */ },
  onChoose: (event) => { /* Item chosen */ },
  onUnchoose: (event) => { /* Item unchosen */ },
  onFilter: (event) => { /* Filtered element clicked */ },
  onClone: (event) => { /* Clone created */ }
}

Examples

Simple Sortable List

<ul [hubSortable]="items">
  @for (item of items; track item) {
  <li>{{ item }}</li>
  }
</ul>

With Animation and Handle

<div [hubSortable]="items" [options]="{ animation: 150, handle: '.drag-handle' }">
  @for (item of items; track item.id) {
  <div class="item">
    <span class="drag-handle">&#9776;</span>
    {{ item.name }}
  </div>
  }
</div>

Multiple Connected Lists

@Component({
  selector: "app-multi-list",
  standalone: true,
  imports: [SortableDirective],
  template: `
    <div class="list" [hubSortable]="list1" [options]="options">
      @for (item of list1; track item) {
        <div>{{ item }}</div>
      }
    </div>
    <div class="list" [hubSortable]="list2" [options]="options">
      @for (item of list2; track item) {
        <div>{{ item }}</div>
      }
    </div>
  `,
})
export class MultiListComponent {
  list1 = ["Item 1", "Item 2", "Item 3"];
  list2 = ["Item 4", "Item 5", "Item 6"];

  options = {
    group: "shared",
    animation: 150,
  };
}

With FormArray

@Component({
  selector: "app-form-array",
  standalone: true,
  imports: [ReactiveFormsModule, SortableDirective],
  template: `
    <form [formGroup]="form">
      <div [hubSortable]="formArray" [options]="{ animation: 150 }">
        @for (control of formArray.controls; track control; let i = $index) {
          <div>
            <input [formControlName]="i" />
          </div>
        }
      </div>
    </form>
  `,
})
export class FormArrayComponent {
  form = new FormGroup({
    items: new FormArray([new FormControl("Item 1"), new FormControl("Item 2"), new FormControl("Item 3")]),
  });

  get formArray() {
    return this.form.get("items") as FormArray;
  }
}

With Angular Signals

import { Component, signal, computed } from "@angular/core";
import { SortableDirective } from "ng-hub-ui-sortable";

@Component({
  selector: "app-signal-sortable",
  standalone: true,
  imports: [SortableDirective],
  template: `
    <div [hubSortable]="items" [options]="{ animation: 150 }">
      @for (item of items(); track item.id) {
        <div class="item">{{ item.name }}</div>
      }
    </div>
    <p>Current items: {{ itemNames() }}</p>
  `,
})
export class SignalSortableComponent {
  items = signal([
    { id: 1, name: "Item 1" },
    { id: 2, name: "Item 2" },
    { id: 3, name: "Item 3" },
  ]);

  // Computed signal that derives from sortable signal
  itemNames = computed(() =>
    this.items()
      .map((item) => item.name)
      .join(", "),
  );
}

Clone Mode

@Component({
  selector: "app-clone",
  standalone: true,
  imports: [SortableDirective],
  template: `
    <div [hubSortable]="items" [options]="cloneOptions" [cloneFunction]="cloneItem">
      @for (item of items; track item.name) {
        <div>{{ item.name }}</div>
      }
    </div>
  `,
})
export class CloneComponent {
  items = [{ name: "Item 1" }, { name: "Item 2" }];

  cloneOptions = {
    group: {
      name: "clone-group",
      pull: "clone",
      put: false,
    },
  };

  cloneItem = (item: any) => ({ ...item, name: `${item.name} (copy)` });
}

Custom Container

For cases where the directive host is wrapped by another component (e.g., Angular Material):

<mat-list [hubSortable]="items" container=".mat-list-inner">
  @for (item of items; track item) {
  <mat-list-item>{{ item }}</mat-list-item>
  }
</mat-list>

Manual Control Mode

By default, ng-hub-ui-sortable automatically updates your arrays when items are dragged and dropped. However, you can opt into manual control mode for complete control over when and how arrays are updated, similar to Angular CDK's drag-and-drop approach.

When to Use Manual Control

Use manual control mode ([autoUpdateArray]="false") when you need to:

  • Validate changes before updating the data model
  • Make API calls to persist changes to a backend
  • Implement undo/redo functionality
  • Use immutable data patterns for optimal change detection
  • Add custom business logic to drag operations
  • Handle errors gracefully before committing changes

How It Works

When autoUpdateArray is set to false, the directive only emits events without modifying your arrays. You're responsible for updating the data using the provided event information.

import { Component } from "@angular/core";
import { SortableDirective, moveItemInArray, transferArrayItem, SortableEvent } from "ng-hub-ui-sortable";

@Component({
  selector: "app-tasks",
  standalone: true,
  imports: [SortableDirective],
  template: `
    <div [hubSortable]="tasks" [autoUpdateArray]="false" (update)="onUpdate($event)">
      @for (task of tasks; track task.id) {
        <div class="task">{{ task.title }}</div>
      }
    </div>
  `,
})
export class TasksComponent {
  tasks = [
    { id: 1, title: "Design mockups" },
    { id: 2, title: "Implement feature" },
    { id: 3, title: "Write tests" },
  ];

  onUpdate(event: SortableEvent): void {
    if (event.oldIndex === undefined || event.newIndex === undefined) {
      return;
    }

    // Validate the drag operation
    if (this.isValidMove(event.oldIndex, event.newIndex)) {
      // Manually update the array
      moveItemInArray(this.tasks, event.oldIndex, event.newIndex);

      // Persist to backend
      this.apiService.updateTaskOrder(this.tasks).subscribe();
    }
  }

  isValidMove(oldIndex: number, newIndex: number): boolean {
    // Add your validation logic here
    return true;
  }
}

Helper Functions

The library provides three helper functions for manual array manipulation:

moveItemInArray

Moves an item within the same array:

import { moveItemInArray } from "ng-hub-ui-sortable";

const items = ["A", "B", "C", "D"];
moveItemInArray(items, 1, 3);
// Result: ['A', 'C', 'D', 'B']
transferArrayItem

Transfers an item from one array to another:

import { transferArrayItem } from "ng-hub-ui-sortable";

const source = ["A", "B", "C"];
const target = ["1", "2", "3"];
transferArrayItem(source, target, 1, 2);
// source: ['A', 'C']
// target: ['1', '2', 'B', '3']
copyArrayItem

Copies an item to another array without removing it from the source:

import { copyArrayItem } from "ng-hub-ui-sortable";

const source = ["A", "B", "C"];
const target = ["1", "2", "3"];
copyArrayItem(source, target, 1, 2);
// source: ['A', 'B', 'C'] (unchanged)
// target: ['1', '2', 'B', '3']

Multiple Lists Example

import { Component } from "@angular/core";
import { SortableDirective, transferArrayItem, SortableEvent } from "ng-hub-ui-sortable";

@Component({
  selector: "app-kanban",
  standalone: true,
  imports: [SortableDirective],
  template: `
    <div class="board">
      <div class="column">
        <h3>To Do</h3>
        <ul [hubSortable]="todoList" [autoUpdateArray]="false" [options]="{ group: 'tasks' }" (add)="onListAdd($event, todoList)" (update)="onListUpdate($event, todoList)" data-list-id="todo">
          @for (item of todoList; track item.id) {
            <li>{{ item.title }}</li>
          }
        </ul>
      </div>

      <div class="column">
        <h3>In Progress</h3>
        <ul [hubSortable]="inProgressList" [autoUpdateArray]="false" [options]="{ group: 'tasks' }" (add)="onListAdd($event, inProgressList)" (update)="onListUpdate($event, inProgressList)" data-list-id="inProgress">
          @for (item of inProgressList; track item.id) {
            <li>{{ item.title }}</li>
          }
        </ul>
      </div>

      <div class="column">
        <h3>Done</h3>
        <ul [hubSortable]="doneList" [autoUpdateArray]="false" [options]="{ group: 'tasks' }" (add)="onListAdd($event, doneList)" (update)="onListUpdate($event, doneList)" data-list-id="done">
          @for (item of doneList; track item.id) {
            <li>{{ item.title }}</li>
          }
        </ul>
      </div>
    </div>
  `,
})
export class KanbanComponent {
  todoList = [{ id: 1, title: "Task 1" }];
  inProgressList = [{ id: 2, title: "Task 2" }];
  doneList = [{ id: 3, title: "Task 3" }];

  onListAdd(event: SortableEvent, targetList: any[]): void {
    if (event.oldIndex === undefined || event.newIndex === undefined) {
      return;
    }

    const sourceList = this.getListByElement(event.from);
    if (sourceList) {
      transferArrayItem(sourceList, targetList, event.oldIndex, event.newIndex);

      // Update backend
      this.apiService.moveTask(/* ... */).subscribe();
    }
  }

  onListUpdate(event: SortableEvent, list: any[]): void {
    if (event.oldIndex === undefined || event.newIndex === undefined) {
      return;
    }

    moveItemInArray(list, event.oldIndex, event.newIndex);
  }

  private getListByElement(element: HTMLElement): any[] | null {
    const listId = element.getAttribute("data-list-id");
    switch (listId) {
      case "todo":
        return this.todoList;
      case "inProgress":
        return this.inProgressList;
      case "done":
        return this.doneList;
      default:
        return null;
    }
  }
}

Comparison: Automatic vs Manual

| Aspect | Automatic Mode (default) | Manual Mode | | ------------------- | -------------------------- | ------------------------------ | | Array updates | Automatic | You control when and how | | Code required | Minimal | Event handlers needed | | Validation | Not possible before update | Full validation support | | API integration | Update after the fact | Update before committing | | Immutability | Mutates arrays in place | Can create new arrays | | Best for | Simple lists, prototypes | Production apps, complex logic |

Note on Event Handling: SortableJS internally fires multiple events for a single drag operation (e.g., both update and sort). ng-hub-ui-sortable handles this automatically, ensuring you only receive one event per operation in manual mode. For more details on the internal event flow, see the Events Guide.

Real-world Use Cases

The ng-hub-ui-sortable component is versatile and can be used in various real-world applications:

  • Task Lists - Reorder tasks by priority with drag-and-drop
  • Playlist Managers - Arrange media items in custom order
  • Form Builders - Drag-and-drop form field ordering
  • Dashboard Widgets - User-customizable widget layouts
  • Photo Galleries - Rearrange images in albums
  • Menu Editors - CMS navigation menu ordering
  • Kanban Boards - (For advanced boards, see ng-hub-ui-board)

Troubleshooting

Here are some common issues and how to resolve them:

Drag and drop not working

  • Check imports: Ensure the standalone SortableDirective is properly imported
  • Verify binding: Make sure [hubSortable] is bound to an array or FormArray
  • Check container: Items must be direct children of the sortable container

Array not updating

  • Reference check: SortableJS modifies the array in place; ensure change detection picks it up
  • Zone issues: If using OnPush, you may need to trigger change detection manually

Events not firing

  • Zone proxying: Events are proxied through Angular's zone automatically
  • Option reference: Provide a new object reference to options when updating options

Multi-list issues

  • Group name: Ensure all lists share the same group name in options
  • Directive import: SortableDirective must be imported by both components hosting the connected lists

FormArray sync issues

  • Direct binding: Bind the FormArray directly, not the parent FormGroup
  • Control access: Access controls via formArray.controls in your template

If problems persist, open an issue at: https://github.com/carlos-morcillo/ng-hub-ui-sortable/issues

Accessibility

SortableJS reordering is pointer-only: there is no built-in keyboard or screen-reader path for drag-and-drop, and this wrapper does not add one yet. Where reordering is an essential capability, pair the sortable list with an alternative affordance operating on the same array — for example per-item "move up / move down" buttons — so keyboard and assistive-technology users can achieve the same result. An aria-live announcer story for drag operations is tracked as future work.

Changelog

See CHANGELOG.md for the full version history.

Contributing

Contributions are welcome! Here's how you can help:

  1. Fork the repository
  2. Create your feature branch: git checkout -b feature/my-new-feature
  3. Commit your changes: git commit -am 'Add some feature'
  4. Push to the branch: git push origin feature/my-new-feature
  5. Submit a pull request

Support the Project

If you find this project helpful and would like to support its development, you can buy me a coffee:

"Buy Me A Coffee"

Your support is greatly appreciated and helps maintain and improve this project!

License

This project is licensed under the MIT License.

For full license details, see the LICENSE file.


Made with love by Carlos Morcillo Fernandez