LIDOR SYSTEMS

Advanced User Interface Controls and Components

Drag Drop between Custom Angular Components

Created: 07 November 2017

Angular components by default don't have a drag drop functionality built-in. If you need to drag drop objects within the same component or different ones, you need to built it from scratch. IntegralUI DragDrop Service provides a way to create custom drag drop operations for different kinds of Angular components.

Drag Drop service is part of IntegralUI Web
a suite of UI Components for development of web apps

If you have any questions, don't hesitate to contact us at support@lidorsystems.com

In this example, there are three angular components: two lists and a block component. You can drag an item from the first list to the block or to the second list and vice versa. During this process, the IntegralUI DragDrop Service works as mediator between components. You can store and retrieve the dragged object using methods provided by the service. In more advanced scenarios, you can

How to Create Custom Angular Component

In this example, you need two components: a list and a block. Both components will have an add and remove methods used during drag drop operations and called from root application component. The main difference here is that the list has uses an array to store the objects that are dropped, while the block uses a variable. The components are defined as:

@Component({
    selector: 'iui-app',
    template: `
        <style>
            .main-block
            {
                padding: 10px;
            }
        </style>
        <div class="main-block">
            <custom-list [items]="list1"></custom-list>
            <custom-block></custom-block>
            <custom-list [items]="list3"></custom-list>
        </div>
        <br style="clear:both;"/>
    `,
    encapsulation: ViewEncapsulation.None
})
export class DragDropToCustomAngularComponentsSample {
    public list1: Array = [];
    public list2: Array = [];
    public list3: Array = [];

    constructor(){
        this.list1 = [
            { id: 1, text: "Item 1" },
            { id: 2, text: "Item 2" },
            { id: 3, text: "Item 3" },
            { id: 4, text: "Item 4" },
            { id: 5, text: "Item 5" },
            { id: 6, text: "Item 6" },
            { id: 7, text: "Item 7" }
        ];
    }
}

@Component({
    selector: 'custom-list',
    template: `
        <style>
            .list-block
            {
                float: left;
                background: white;
                border: thin solid gray;
                margin: 0 20px;
                width: 250px;
                height: 300px;
            }
            .list-block > ul 
            {
                margin: 0;
                padding: 0;
            }
            .list-block li
            {
                border-bottom: thin solid #f5f5f5;
                padding: 5px;
                list-style-type: none;
            }
        </style>
        <div class="list-block">
            <ul>
                <li *ngFor="let item of items" [ngStyle]="{ 'padding-bottom': item==targetItem ? itemPadding + 'px' : '5px' }">
                    <span>{{item.text}}</span>
                </li>
            </ul>
        </div>
    `,
    encapsulation: ViewEncapsulation.None
})
export class CustomListComponent {
    @Input() items: Array = [];

    public itemPadding: number = 30;
    public targetItem: any = null;

    constructor(){}

    addData(source: any, target?: any){
        if (target){
            let targetIndex: number = this.items.indexOf(target) + 1;
            targetIndex = Math.max(Math.min(targetIndex, this.items.length), 0);

            this.items.splice(targetIndex, 0, source);
        }
        else
            this.items.push(source);
    }

    removeData(source: any): boolean {
        let itemIndex: number = this.items.indexOf(source);
        if (itemIndex >= 0){
            this.items.splice(itemIndex, 1);

            return true;
        }

        return false;
    }
}


@Component({
    selector: 'custom-block',
    template: `
        <style>
            .block
            {
                float: left;
                background: white;
                border: thin solid blue;
                margin: 0 20px;
                width: 300px;
                height: 300px;
                line-height: 300px;
            }
        </style>
        <div class="block" align="center">
            <span>{{getData()}}</span>
        </div>

    `,
    encapsulation: ViewEncapsulation.None
})
export class CustomBlockComponent {
    @Input() data: any;

    constructor(){}

    getData(){
        return this.data ? this.data.text: 'Drop an Item Here';
    }

    addData(obj: any){
        this.data = obj;
    }

    removeData(obj: any): boolean {
        this.data = null;
        return true;
    }
}                            

In current state, these components don't have a drag drop functionality. They can only store objects and display them in their view.

How to Add Drag Drop functionality to Angular Components

In order to use the IntegralUI DragDrop Service, you need to add it as provider to the root application component. This makes sure that a single instance of this service is used across all child components. As a result, the data stored by the service in one component, is retrievable by another component.

@Component({
    selector: 'iui-app',

    . . .

    providers: [ IntegralUIDragDropService ]
})
export class DragDropToCustomAngularComponentsSample {

    constructor(protected dragDropService: IntegralUIDragDropService){}

}

export class CustomListComponent {

    constructor(protected dragDropService: IntegralUIDragDropService){}

}

export class CustomBlockComponent {

    constructor(protected dragDropService: IntegralUIDragDropService){}

}                            

To handle drag drop operations, you can use standard HTML5 events. Each component should have handlers for dragstart and dragover events. When you drag an object initially, the dragstart event is fired . In your event handler, you need to call the setData method of the DragDrop Service to update the stored object. Also, you can store a reference to the component from which the drag operations starts. This is used later, when you need to remove the dragged object from the source component and add it to the target component, in other words move an object from one component to another.

Here are the handlers for dragstart event for list and block component. They are very similar:

@Component({
    selector: 'custom-list',
    template: `
        <div class="list-block">
            <ul>
                <li *ngFor="let item of items" [ngStyle]="{ 'padding-bottom': item==targetItem ? itemPadding + 'px' : '5px' }">
                    <span draggable="true" (dragstart)="itemDragged($event, item)">{{item.text}}</span>
                </li>
            </ul>
        </div>
    `,
    encapsulation: ViewEncapsulation.None
})
export class CustomListComponent {
    itemDragged(e: any, item: any){
        if (e.dataTransfer){
            e.dataTransfer.effectAllowed = 'move';

            this.dragDropService.setData({ source: item, sourceCtrl: this });
        }

        e.stopPropagation();
    }

    itemDragOver(e: any, item: any){
        e.preventDefault();

        this.targetItem = item;

        let dragDropData = this.dragDropService.getData();
        this.dragDropService.setData({
            action: 'move',
            source: dragDropData.source,
            sourceCtrl: dragDropData.sourceCtrl,
            target: item,
            targetCtrl: this,
            dropPos: 2
        });

        e.stopPropagation();
    }
}


@Component({
    selector: 'custom-block',
    template: `
        <div class="block" align="center">
            <span draggable="true" (dragstart)="ctrDragStart($event)">{{getData()}}</span>
        </div>
    `,
    encapsulation: ViewEncapsulation.None
})
export class CustomBlockComponent {

    ctrDragStart(e: any){
        if (e.dataTransfer){
            e.dataTransfer.effectAllowed = 'move';

            this.dragDropService.setData({ source: this.data, sourceCtrl: this });
        }

        e.stopPropagation();
    }
}                            

The handler for the dragover event is used to set the target component over which the dragged object is currently hovering. By setting the targetCtrl of the data stored in the DragDrop Service, you can later use it to add the object to the target component. This is all handled from the application root component.

@Component({
    selector: 'custom-list',
    template: `
        <div class="list-block" (dragenter)="ctrlDragEnter($event)" (dragleave)="ctrlDragLeave($event)" (dragover)="ctrlDragOver($event)" (drop)="ctrlDrop($event)">
            <ul>
                <li *ngFor="let item of items" (dragover)="itemDragOver($event, item)" [ngStyle]="{ 'padding-bottom': item==targetItem ? itemPadding + 'px' : '5px' }">
                    <span draggable="true" (dragstart)="itemDragged($event, item)">{{item.text}}</span>
                </li>
            </ul>
        </div>
    `,
    encapsulation: ViewEncapsulation.None
})
export class CustomListComponent {

    itemDragOver(e: any, item: any){
        e.preventDefault();

        this.targetItem = item;

        let dragDropData = this.dragDropService.getData();
        this.dragDropService.setData({
            action: 'move',
            source: dragDropData.source,
            sourceCtrl: dragDropData.sourceCtrl,
            target: item,
            targetCtrl: this,
            dropPos: 2
        });

        e.stopPropagation();
    }

    ctrlDragEnter(e: any){
        this.targetItem = null;
    }

    ctrlDragLeave(e: any){
        this.targetItem = null;
    }

    ctrlDragOver(e: any){
        e.preventDefault();

        let dragDropData = this.dragDropService.getData();
        this.dragDropService.setData({
            action: 'move',
            source: dragDropData.source,
            sourceCtrl: dragDropData.sourceCtrl,
            target: null,
            targetCtrl: this,
            dropPos: -1
        });

        e.stopPropagation();
    }

    ctrlDrop(e: any){
        this.targetItem = null;
    }
}


@Component({
    selector: 'custom-block',
    template: `
        <div class="block" (dragover)="ctrlDragOver($event)" align="center">
            <span draggable="true" (dragstart)="ctrDragStart($event)">{{getData()}}</span>
        </div>
    `,
    encapsulation: ViewEncapsulation.None
})
export class CustomBlockComponent {

    ctrlDragOver(e: any){
        e.preventDefault();

        let dragDropData = this.dragDropService.getData();
        this.dragDropService.setData({
            action: 'move',
            source: dragDropData.source,
            sourceCtrl: dragDropData.sourceCtrl,
            target: null,
            targetCtrl: this,
            dropPos: -1
        });

        e.stopPropagation();
    }
}                            

The difference here between the list and block components is that the list also can have as a target an item and also the list itself, while the block act as a drop target in whole.

In addition, during hovering over the list, it is good to visually display the position at which the dragged object can drop. For this purpose, a simple change to item padding is used. To make the example simple, the object can drop only below the target item.

How to Handle Drop Operation from App Root Component

Until this point you can start drag and drop and handle the target over which object hovers. Now, you need to process the drop of the dragged objects. For this purpose, the best is to handle it from the application root component, and call the add/remove methods for the source and target components.

To handle drops over target component, you need to add a handler to the standard HTML5 drop event. You don't have to add an event handler to each child component, you can handle it from the root component. Any drop events in a child also bubble-up to the parent component.

@Component({
    selector: 'iui-app',
    template: `
        <div class="main-block" (drop)="ctrlDrop($event)">
            <custom-list [items]="list1"></custom-list>
            <custom-block></custom-block>
            <custom-list [items]="list3"></custom-list>
        </div>
        <br style="clear:both;"/>
    `,
    providers: [ IntegralUIDragDropService ],
    encapsulation: ViewEncapsulation.None
})
export class DragDropToCustomAngularComponentsSample {

    ctrlDrop(e: any){
        if (e.dataTransfer && e.dataTransfer.effectAllowed != 'none'){
            let data = this.dragDropService.getData();
            if (data && data.sourceCtrl){
                let isDataRemoved = data.sourceCtrl.removeData(data.source);
                if (isDataRemoved && data.targetCtrl)
                    data.targetCtrl.addData(data.source, data.target);
            }
        }
    }
}                            

During drag drop operation using the DragDrop Service, you have stored the object that is dragged but also the source component from which the object is dragged and the target component over which the object is dropped. You can use this references to call the add/remove methods and complete the move operation.

As it is shown in above code, the operation is completed only when item is removed at first from the source, and then add it to the target component.

Note Each list or block component can act as source and target.

Put all Together

Here is the complete source code of this sample:

import { Component, enableProdMode, Input, ViewContainerRef, ViewChild, ViewChildren, ViewEncapsulation } from '@angular/core';
import { IntegralUIDragDropService } from './integralui/services/integralui.dragdrop.service';

enableProdMode();

@Component({
    selector: 'iui-app',
    template: `
        <style>
            .main-block
            {
                padding: 10px;
            }
        </style>
        <div class="main-block" (drop)="ctrlDrop($event)">
            <custom-list [items]="list1"></custom-list>
            <custom-block></custom-block>
            <custom-list [items]="list3"></custom-list>
        </div>
        <br style="clear:both;"/>
    `,
    providers: [ IntegralUIDragDropService ],
    encapsulation: ViewEncapsulation.None
})
export class DragDropToCustomAngularComponentsSample {
    public list1: Array = [];
    public list2: Array = [];
    public list3: Array = [];

    constructor(protected dragDropService: IntegralUIDragDropService){
        this.list1 = [
            { id: 1, text: "Item 1" },
            { id: 2, text: "Item 2" },
            { id: 3, text: "Item 3" },
            { id: 4, text: "Item 4" },
            { id: 5, text: "Item 5" },
            { id: 6, text: "Item 6" },
            { id: 7, text: "Item 7" }
        ];
    }

    ctrlDrop(e: any){
        if (e.dataTransfer && e.dataTransfer.effectAllowed != 'none'){
            let data = this.dragDropService.getData();
            if (data && data.sourceCtrl){
                let isDataRemoved = data.sourceCtrl.removeData(data.source);
                if (isDataRemoved && data.targetCtrl)
                    data.targetCtrl.addData(data.source, data.target);
            }
        }
    }
}

@Component({
    selector: 'custom-list',
    template: `
        <style>
            .list-block
            {
                float: left;
                background: white;
                border: thin solid gray;
                margin: 0 20px;
                width: 250px;
                height: 300px;
            }
            .list-block > ul 
            {
                margin: 0;
                padding: 0;
            }
            .list-block li
            {
                border-bottom: thin solid #f5f5f5;
                padding: 5px;
                list-style-type: none;
            }
        </style>
        <div class="list-block" (dragenter)="ctrlDragEnter($event)" (dragleave)="ctrlDragLeave($event)" (dragover)="ctrlDragOver($event)" (drop)="ctrlDrop($event)">
            <ul>
                <li *ngFor="let item of items" (dragover)="itemDragOver($event, item)" [ngStyle]="{ 'padding-bottom': item==targetItem ? itemPadding + 'px' : '5px' }">
                    <span draggable="true" (dragstart)="itemDragged($event, item)">{{item.text}}</span>
                </li>
            </ul>
        </div>
    `,
    encapsulation: ViewEncapsulation.None
})
export class CustomListComponent {
    @Input() items: Array = [];

    public itemPadding: number = 30;
    public targetItem: any = null;

    constructor(protected dragDropService: IntegralUIDragDropService){}

    addData(source: any, target?: any){
        if (target){
            let targetIndex: number = this.items.indexOf(target) + 1;
            targetIndex = Math.max(Math.min(targetIndex, this.items.length), 0);

            this.items.splice(targetIndex, 0, source);
        }
        else
            this.items.push(source);
    }

    removeData(source: any): boolean {
        let itemIndex: number = this.items.indexOf(source);
        if (itemIndex >= 0){
            this.items.splice(itemIndex, 1);

            return true;
        }

        return false;
    }

    itemDragged(e: any, item: any){
        if (e.dataTransfer){
            e.dataTransfer.effectAllowed = 'move';

            this.dragDropService.setData({ source: item, sourceCtrl: this });
        }

        e.stopPropagation();
    }

    itemDragOver(e: any, item: any){
        e.preventDefault();

        this.targetItem = item;

        let dragDropData = this.dragDropService.getData();
        this.dragDropService.setData({
            action: 'move',
            source: dragDropData.source,
            sourceCtrl: dragDropData.sourceCtrl,
            target: item,
            targetCtrl: this,
            dropPos: 2
        });

        e.stopPropagation();
    }

    ctrlDragEnter(e: any){
        this.targetItem = null;
    }

    ctrlDragLeave(e: any){
        this.targetItem = null;
    }

    ctrlDragOver(e: any){
        e.preventDefault();

        let dragDropData = this.dragDropService.getData();
        this.dragDropService.setData({
            action: 'move',
            source: dragDropData.source,
            sourceCtrl: dragDropData.sourceCtrl,
            target: null,
            targetCtrl: this,
            dropPos: -1
        });

        e.stopPropagation();
    }

    ctrlDrop(e: any){
        this.targetItem = null;
    }
}

@Component({
    selector: 'custom-block',
    template: `
        <style>
            .block
            {
                float: left;
                background: white;
                border: thin solid blue;
                margin: 0 20px;
                width: 300px;
                height: 300px;
                line-height: 300px;
            }
        </style>
        <div class="block" (dragover)="ctrlDragOver($event)" align="center">
            <span draggable="true" (dragstart)="ctrDragStart($event)">{{getData()}}</span>
        </div>
    `,
    encapsulation: ViewEncapsulation.None
})
export class CustomBlockComponent {
    @Input() data: any;

    constructor(protected dragDropService: IntegralUIDragDropService){}

    getData(){
        return this.data ? this.data.text: 'Drop an Item Here';
    }

    addData(obj: any){
        this.data = obj;
    }

    removeData(obj: any): boolean {
        this.data = null;
        return true;
    }

    ctrDragStart(e: any){
        if (e.dataTransfer){
            e.dataTransfer.effectAllowed = 'move';

            this.dragDropService.setData({ source: this.data, sourceCtrl: this });
        }

        e.stopPropagation();
    }

    ctrlDragOver(e: any){
        e.preventDefault();

        let dragDropData = this.dragDropService.getData();
        this.dragDropService.setData({
            action: 'move',
            source: dragDropData.source,
            sourceCtrl: dragDropData.sourceCtrl,
            target: null,
            targetCtrl: this,
            dropPos: -1
        });

        e.stopPropagation();
    }
}                            

Conclusion

Drag Drop functionality by default is not present in Angular components. If you need to add a drag drop to your components, you can use the IntegralUI DragDrop Service to handle the data. By setting this service as a provider on global level to the application root component, you can add drag drop functionality to any child component. Each component during drag drop, can act as a source or a target , which is determined by the data stored in the service.

The Drag Drop service is part of IntegralUI Web.

Did you Like this Article?


Enter your e-mail address below and you will receive latest articles as well as news on upcoming events and special offers.