The Ignite UI for Angular Drag and Drop directives enable dragging of elements around the page. The supported features include free dragging, using a drag handle, drag ghost, animations and multiple drop strategies.
Getting Started with Ignite UI for Angular Drag and Drop
To get started with the Ignite UI for Angular Drag and Drop directives, first you need to install Ignite UI for Angular. In an existing Angular application, type the following command:
ng add igniteui-angular
cmd
For a complete introduction to the Ignite UI for Angular, read the getting started topic.
The next step is to import the IgxDragDropModule in your app.module.ts file.
Alternatively, as of 16.0.0 you can import the IgxDragDirective and IgxDropDirective as standalone dependencies, or use the IGX_DRAG_DROP_DIRECTIVES token to import the component and all of its supporting components and directives.
Now that you have the Ignite UI for Angular Drag and Drop module or directives imported, you can start using the igxDrag and igxDrop directives.
Using the Angular Drag Directive
When an element inside your Angular application needs to be dragged from one place to another on the page, the igxDrag directive is designed to help achieve this behavior. In combination with the igxDrop directive, the placing of the dragged element can be done as well, so you can have fully interactive application.
Dragging Fundamentals
A drag operation starts when the end user swipes at least 5px in any direction. This is customizable and can be changed using the dragTolerance input. Otherwise the interaction is considered as a click and a dragClick event is triggered.
When the dragging starts, the dragStart event is triggered. To prevent any actual movement to occur, the event can be canceled by setting the cancel property to true.
Before any actual movement is about to be performed, the dragMove event is also triggered, containing the last and next position of the pointer. It is triggered every time a movement is detected while dragging an element around.
After the user releases the mouse/touch the drag ghost element is removed from the DOM and the dragEnd event will be emitted.
Due to the nature of the dragMove event, it can be triggered many times in a short period of time, which may cause performance issues for complex operations done when triggered.
Dragging With Ghost
The igxDrag directive can be applied on any DOM element by just adding it to its template.
<divigxDrag>Drag me</div>html
The default behavior of igxDrag directive is to leave the base element unmodified and to create a ghost element when drag operation is performed by the end user.
Before the ghost is rendered on the page, a ghostCreate event is triggered containing information of the ghost element that is about to be added. The event is triggered right after the dragStart event. If the dragStart is canceled, no ghost will be created and the ghostCreate event will not trigger accordingly.
Right before the ghost is about to be removed, the ghostDestroy event will be triggered.
Customizing The Ghost
The ghost element by default is a copy of the base element the igxDrag is used on. It can be customized by providing a template reference to the ghostTemplate input directly.
If you would like to move the base element, to which the igxDrag directive is applied, you can do that by setting the ghost input to false. That way there will be no extra ghost element rendered and if you need to apply custom styling when dragging and element, you can apply it directly to the base element.
<divigxDrag [ghost]="false">Drag me</div>html
Dragging Using a Handle
You can specify an element that is a child of the igxDrag by which to drag, since by default the whole element is used to perform that action. It can be done using the igxDragHandle directive and can be applied to multiple elements inside the igxDrag.
When an element is being dragged, there are no animations applied by default.
You can apply transition animation to the igxDrag at any time, but it is advised to use it when dragging ends or the element is not currently dragged. This can be achieved by using the transitionToOrigin and the transitionTo methods.
The transitionToOrigin method, as the name suggests, animates the currently dragged element or its ghost to the start position, where the dragging began. The transitionTo method animates the element to a specific location relative to the page (i.e. pageX and pageY) or to the position of a specified element. If the element is not being currently dragged, it will animate anyway or create ghost and animate it to the desired position.
Both functions have arguments that you can set to customize the transition animation and set duration, timing function or delay. If specific start location is set it will animate the element starting from there.
When the transition animation ends, if a ghost is created, it will be removed and the igxDrag directive will return to its initial state. If no ghost is created, it will keep its position. In both cases, then the transitioned event will be triggered, depending on how long the animation lasts. If no animation is applied, it will be triggered instantly.
You can have other types of animations that involve element transformations. That can be done like any other element either using the Angular Animations or straight CSS Animations to either the base igxDrag element or its ghost. If you want to apply them to the ghost, you would need to define a custom ghost and apply animations to its element.
Reorder items in the list using the drag handle. While dragging a list item other list items will re-order with animation.
EXAMPLE
TS
HTML
SCSS
import {
Component,
ElementRef,
QueryList,
ViewChild,
ViewChildren
} from'@angular/core';
import { IDragBaseEventArgs, IDragMoveEventArgs, IgxDragDirective, IgxDragLocation, IgxListComponent, IgxListItemComponent, IgxDropDirective, IgxListLineTitleDirective, IgxListLineSubTitleDirective, IgxIconComponent, IgxDragHandleDirective, IgxListActionDirective } from'igniteui-angular';
@Component({
selector: 'app-list-reorder-sample',
templateUrl: './list-reorder-sample.component.html',
styleUrls: ['./list-reorder-sample.component.scss'],
imports: [IgxListComponent, IgxListItemComponent, IgxDropDirective, IgxDragDirective, IgxListLineTitleDirective, IgxListLineSubTitleDirective, IgxIconComponent, IgxDragHandleDirective, IgxListActionDirective]
})
exportclassListReorderSampleComponent{
@ViewChildren('dragDirRef', { read: IgxDragDirective })
public dragDirs: QueryList<IgxDragDirective>;
@ViewChild('listContainer', { read: ElementRef })
public listContainer: ElementRef;
public employees = [
{ id: 0, name: 'Ivan Cornejo', title: 'Senior Product Owner' },
{ id: 1, name: 'Amish Shiravadakar', title: 'Business Tools Director' },
{ id: 2, name: 'Elsi Hansdottir', title: 'Financial Director' },
{ id: 3, name: 'Benito Noboa', title: 'Marketing Specialist' },
{ id: 4, name: 'Beth Murphy', title: 'Platform Lead for Web' }
];
public newIndex = null;
public animationDuration = 0.3;
private listItemHeight = 55;
public getDragDirectiveRef(id: number): IgxDragDirective {
returnthis.dragDirs.find((item) => item.data.id === id);
}
publiconDragStart(event: IDragBaseEventArgs, dragIndex: number) {
// Record the current index as basis for moving up/down.this.newIndex = dragIndex;
// Sets specific class when dragging.
event.owner.data.dragged = true;
}
publiconDragEnd(event: IDragBaseEventArgs, itemIndex: number) {
if (this.newIndex !== null) {
// When we have moved the dragged element up/down, animate it to its new location.const moveDown = this.newIndex > itemIndex;
// If the new position is below add the height moved down, otherwise subtract it.const prefix = moveDown ? 1 : -1;
// The height that the new position differs from the current. We know that each item is 55px height.const movedHeight = prefix * Math.abs(this.newIndex - itemIndex) * this.listItemHeight;
const originLocation = event.owner.originLocation;
event.owner.transitionTo(
new IgxDragLocation(originLocation.pageX, originLocation.pageY + movedHeight),
{ duration: this.animationDuration }
);
} else {
// Otherwise animate it to its original position, since it is unchanged.
event.owner.transitionToOrigin({ duration: this.animationDuration });
}
}
publiconTransitioned(event: IDragBaseEventArgs, itemIndex: number) {
// We can have other items transitioned when they move to free up space where the dragged element would be.if (event.owner.data.dragged && this.newIndex != null && this.newIndex !== itemIndex) {
// If the element finished transitioning is the one were dragging,// We can update all elements their new position in the list.this.shiftElements(itemIndex, this.newIndex);
event.owner.setLocation(event.owner.originLocation);
this.newIndex = null;
}
// Disables the specific class when dragging.
event.owner.data.dragged = false;
}
publiconDragMove(event: IDragMoveEventArgs, itemIndex: number) {
const containerPosY = this.listContainer.nativeElement.getBoundingClientRect().top;
// Relative position of the dragged element to the list container.const relativePosY = event.nextPageY - containerPosY;
let newIndex = Math.floor(relativePosY / this.listItemHeight);
newIndex = newIndex < 0 ? 0 : (newIndex >= this.employees.length ? this.employees.length - 1 : newIndex);
if (newIndex === this.newIndex) {
// If the current new index is unchanged do nothing.return;
}
const movingDown = newIndex > itemIndex;
if (movingDown && newIndex > this.newIndex ||
(!movingDown && newIndex < this.newIndex && newIndex !== itemIndex)) {
// If we are moving the dragged element down and the new index is bigger than the current// this means that the element we are stepping into is not shifted up and should be shifted.// Same if we moving the dragged element up and the new index is smaller than the current.const elementToMove = this.getDragDirectiveRef(this.employees[newIndex].id);
const currentLocation = elementToMove.location;
const prefix = movingDown ? -1 : 1;
elementToMove.transitionTo(
new IgxDragLocation(currentLocation.pageX, currentLocation.pageY + prefix * this.listItemHeight),
{ duration: this.animationDuration }
);
} else {
// Otherwise if are moving up but the new index is still bigger than the current, this means that// the item we are stepping into is already shifted and should be returned to its original position.// Same if we are moving down and the new index is still smaller than the current.const elementToMove = this.getDragDirectiveRef(this.employees[this.newIndex].id);
elementToMove.transitionToOrigin({ duration: this.animationDuration });
}
this.newIndex = newIndex;
}
privateshiftElements(draggedIndex: number, targetIndex: number) {
// Move the dragged element in DOM to the new position.const movedElem = this.employees.splice(draggedIndex, 1);
this.employees.splice(targetIndex, 0, movedElem[0]);
this.dragDirs.forEach((dir) => {
if (this.employees[targetIndex].id !== dir.data.id) {
// Reset each element its location since it will be repositioned in the DOM except the element we drag.
dir.setLocation(dir.originLocation);
dir.data.shifted = false;
}
});
}
}
ts
If the user wants to have interactable children of the main element which have igxDrag instanced, he can set the igxDragIgnore directive in order to make them be ignored by the igxDrag and not perform any dragging action. This will leave these elements be fully interactable and receive all mouse events.
When an element that is being dragged using the igxDrag directive needs to be placed in an area, the igxDrop can be used to achieve this behavior. It provides events that you can use to determine if an element is entering the boundaries of the element it is applied to and if it is being released inside it.
The igxDrop directive can be applied to any DOM element just like the igxDrag directive.
By default, the igxDrop directive doesn't apply any logic for modifying the dragged element position in the DOM. That's why you need to specify a dropStrategy or apply custom logic. Drop strategies are discussed in the next section.
Drop Strategies
The igxDrop comes with 4 drop strategies which are: Default, Prepend, Insert and Append:
Default - does not perform any action when an element is dropped onto an igxDrop element and is implemented as a class named IgxDefaultDropStrategy.
Append - always inserts the dropped element as a last child and is implemented as a class named IgxAppendDropStrategy.
Prepend - always inserts the dropped element as first child and is implemented as a class named IgxPrependDropStrategy.
Insert - inserts the dragged element at last position. If there is a child under the element when it was dropped though, the igxDrag instanced element will be inserted at that child's position and the other children will be shifted. It is implemented as a class named IgxInsertDropStrategy.
The way a strategy can be applied is by setting the dropStrategy input to one of the listed classes above. The value provided has to be a type and not an instance, since the igxDrop needs to create and manage the instance itself.
public appendStrategy = IgxAppendDropStrategy;
typescript
When using a specific drop strategy, its behavior can be canceled in the dropped events by setting the cancel property to true. The dropped event is specific to the igxDrop. If you does not have drop strategy applied to the igxDrop canceling the event would have no side effects.
If you would like to implement your own drop logic, we advise binding to the dropped event and execute your logic there or extend the IgxDefaultDropStrategy class.
Linking Drag to Drop Element
Using the dragChannel and dropChannel input on respectively igxDrag and igxDrop directives, you can link different elements to interact only between each other. For example, if an igxDrag element needs to be constrained so it can be dropped on specific igxDrop element and not all available, this can easily be achieved by assigning them the same channel.
<divigxDrag [dragChannel]="['Mammals', 'Land']"> Human </div><divigxDrag [dragChannel]="['Mammals', 'Water']"> Dolphin </div><divigxDrag [dragChannel]="['Insects', 'Air']"> Butterfly </div><divigxDrag [dragChannel]="['Insects', 'Land']"> Ant </div><divigxDrop [dropChannel]="['Mammals']"> Mammals </div><divigxDrop [dropChannel]="['Insects']"> Insects </div><divigxDrop [dropChannel]="['Land']"> Land </div>html
Drag e-mails on the right into the folders on the left.
Since both igxDrag and igxDrop combined can be used in many different and complex application scenarios, the following example demonstrates how they can be used in an Kanban board.
The user could reorder the cards in each column. It is done by setting each card also a drop area, so we can detect when another card has entered its area and switch them around at runtime, to provide better user experience.
It won't be Kanban board without also the ability to switch cards between columns. A card can be directly moved from one column to another column at a specific position. It is achieved here with a dummy object, so it would create a visual area where the card will be position if released. The dummy object is removed once the dragging of a card ends or exits another column.
Drag items around the kanban board.
EXAMPLE
TS
HTML
SCSS
/* eslint-disable no-shadow *//* eslint-disable @typescript-eslint/naming-convention */import { ChangeDetectorRef, Component, ElementRef, OnInit, Renderer2, ViewChild } from'@angular/core';
import { IDropBaseEventArgs, IDropDroppedEventArgs, IgxDropDirective, IgxChipComponent, IgxCardComponent, IgxDragDirective, IgxCardHeaderComponent, IgxCardHeaderTitleDirective, IgxCardContentDirective } from'igniteui-angular';
enum state {
toDo = 'toDo',
inProgress = 'inProgress',
done = 'done'
}
interface IListItem {
id: string;
text: string;
state: state;
hide?: boolean;
}
@Component({
selector: 'app-kanban-sample',
templateUrl: './kanban-sample.component.html',
styleUrls: ['./kanban-sample.component.scss'],
imports: [IgxDropDirective, IgxChipComponent, IgxCardComponent, IgxDragDirective, IgxCardHeaderComponent, IgxCardHeaderTitleDirective, IgxCardContentDirective]
})
exportclassKanbanSampleComponentimplementsOnInit{
@ViewChild('toDo', { read: ElementRef }) public toDo: ElementRef;
@ViewChild('inProgress', { read: ElementRef }) public inProgress: ElementRef;
@ViewChild('done', { read: ElementRef }) public done: ElementRef;
public toDoList: IListItem[];
public inProgressList: IListItem[];
public doneList: IListItem[];
private dragObj;
private dummyObj;
private lastDragEnterList: string;
private currentList: string;
constructor(private renderer: Renderer2, private cdr: ChangeDetectorRef) { }
public ngOnInit(): void {
this.toDoList = [
{ id: 'STR-000132', text: 'Implement chat bubble', state: state.toDo },
{ id: 'STR-000097', text: 'Implement sticky header', state: state.toDo },
{ id: 'STR-000191', text: 'Change trial days to credit', state: state.toDo }
];
this.inProgressList = [
{ id: 'STR-000124', text: 'Implement fback widget', state: state.inProgress },
{ id: 'STR-000121', text: 'Add analytics', state: state.inProgress }
];
this.doneList = [
{ id: 'STR-000129', text: 'Add SSL to account pages', state: state.done }
];
this.dragObj = null;
this.dummyObj = null;
this.lastDragEnterList = '';
this.currentList = '';
}
publiconStateContainerEnter(event: IDropBaseEventArgs) {
// If we have entered another list container, we have to remove the 'dummy' object from the previous oneif (this.currentList !== event.owner.element.nativeElement.id) {
this[this.currentList] = this[this.currentList].filter((item) => item.id !== 'dummy');
this.cdr.detectChanges();
this.currentList = event.owner.element.nativeElement.id;
this.dummyObj = null;
}
// Add the blue container hightlight when an item starts being draggedthis.renderer.addClass(event.owner.element.nativeElement, 'active');
}
publiconStateContainerLeave(event: IDropBaseEventArgs) {
// This event also gets raised when the user drags a task over another task tile.// That means we have to re-apply the 'active' class in the `onItemEnter` event handlerthis.renderer.removeClass(event.owner.element.nativeElement, 'active');
}
publicdragStartHandler(event) {
// We have to save the dragStartList so we could remove the dragged item from it later, when it gets droppedthis.currentList = event.owner.element.nativeElement.dataset.state + 'List';
this.lastDragEnterList = this.currentList;
this.dragObj = this[this.currentList].filter((elem) => elem.id === event.owner.element.nativeElement.id)[0];
}
publicdragEndHandler(event) {
this.toDoList = this.toDoList.filter((x) => x.id !== 'dummy');
this.inProgressList = this.inProgressList.filter((x) => x.id !== 'dummy');
this.doneList = this.doneList.filter((x) => x.id !== 'dummy');
if (this.dragObj) {
this.dragObj.hide = false;
}
}
publiconItemEnter(event: IDropBaseEventArgs) {
// Applying the container highlighting againconst listContainer = event.owner.element.nativeElement.dataset.state;
this.renderer.addClass(this[listContainer].nativeElement, 'active');
const currentList = event.owner.element.nativeElement.dataset.state + 'List';
const currentItemIndex = this[currentList].findIndex((item) => item.id === event.owner.element.nativeElement.id);
// Checking if items in the same list are being reorderedif (this.lastDragEnterList === currentList) {
const draggedItemIndex = this[currentList].findIndex((item) => item.id === this.dragObj.id);
this.swapTiles(draggedItemIndex, currentItemIndex, currentList);
} else {
// We need a hidden dummy object that would make an empty space for the dragged element in the listif (!this.dummyObj) {
this.dummyObj = {id: 'dummy', text: '', state: event.owner.element.nativeElement.dataset.state};
const newCurrentList = [
...this[currentList].slice(0, currentItemIndex),
this.dummyObj,
...this[currentList].slice(currentItemIndex)
];
this[currentList] = newCurrentList;
this.cdr.detectChanges();
} else {
const dummyObjIndex = this[currentList].findIndex((item) => item.id === 'dummy');
if (dummyObjIndex !== -1) {
this.swapTiles(dummyObjIndex, currentItemIndex, currentList);
}
}
}
}
publiconItemLeave(event: IDropBaseEventArgs) {
const listContainer = event.owner.element.nativeElement.dataset.state;
this.renderer.removeClass(this[listContainer].nativeElement, 'active');
}
publiconItemDropped(event: IDropDroppedEventArgs) {
const dropListState = event.owner.element.nativeElement.id;
const dragListState = event.drag.element.nativeElement.dataset.state + 'List';
const dummyItemIndex = this[dropListState].findIndex((item) => item.id === 'dummy');
if (dropListState !== dragListState) {
// The state of the dragged object should be updated before inserting it in the dropped listthis.dragObj.state = dropListState.substring(0, dropListState.length - 4);
this[dragListState] = this[dragListState].filter((item) => item.id !== this.dragObj.id);
// Check if there is a dummy item and replace it with the dragged oneif (dummyItemIndex !== -1) {
this[dropListState].splice(dummyItemIndex, 1, this.dragObj);
} else {
this[dropListState].push(this.dragObj);
}
}
this.dragObj.hide = false;
this.dragObj = null;
// The default browser drag behavior should be cancelled
event.cancel = true;
}
private swapTiles(currentIndex: number, targetIndex: number, itemList: string): void {
const tempObj = this[itemList][currentIndex];
this[itemList].splice(currentIndex, 1);
this[itemList].splice(targetIndex, 0, tempObj);
this.cdr.detectChanges();
}
}
ts