Radzen blog

Rapid application development for Angular

Simple Drag and Drop with Angular 2

Published on by

Updated on March 11, 2017 to address a bug in Internet Explorer which affected the initial implementation.

Drag and drop is a common UI gesture in which the user grabs an object and moves it to a different location or over another object.

In this blog post I will show you how to roll out simple and reusable drag and drop directives that leverage the HTML 5 Drag and Drop API.

If you need a full blown Angular 2 Drag and Drop solution check ng2-dragula or ng2-dnd.

Directive vs. Component

We will use attribute directives instead of components this time.

<!-- draggable component -->
<my-draggable>
  <div>You can drag me!</div>
</my-draggable>

<!-- draggable directive -->
<div myDraggable>You can drag me!</div>

The advantage is obvious - one can easily make any HTML element or Angular component draggable by setting an attribute. It is cumbersome to use a component in this case because it would have required wrapping of elements.

Drag service

The service will track the current drop zone. Drop zone allow drop targets to accept only certain draggables.

@Injectable()
export class DragService {
  private zone: string;

  startDrag(zone: string) {
    this.zone = zone;
  }

  accepts(zone: string): boolean {
    return zone == this.zone;
  }
}

Draggable directive

Now let’s implement the DraggableDirective. It allows the user to drag the target element or component.

@Directive({
  selector: '[myDraggable]'
})
export class DraggableDirective {
  constructor(private dragService: DragService) {
  }

  @HostBinding('draggable')
  get draggable() {
    return true;
  }

  @Input()
  set myDraggable(options: DraggableOptions) {
    if (options) {
      this.options = options;
    }
  }

  private options: DraggableOptions = {};

  @HostListener('dragstart', ['$event'])
  onDragStart(event) {
    const { zone = 'zone', data = {} } = this.options;

    this.dragService.startDrag(zone);

    event.dataTransfer.setData('Text', JSON.stringify(data));
  }
}
export interface DraggableOptions {
  zone?: string;
  data?: any;
}

Implementation notes:

  1. We prefix the attribute selector of the directive in order to avoid clashes with other directives or existing standard HTML attributes.
  2. @HostBinding('draggable') sets the draggable HTML attribute which enables HTML5 Drag and Drop.
  3. We expose an @Input property named after the directive selector. It allows us to configure the directive: <div [myDraggable]="{zone:'dropzone1'}"></div>.
  4. onDragStart handles the dragstart event handler of the host element. We store the data associated with the draggable directive via dataTransfer.setData.

DropTarget directive

The DropTargetDirective allows an element or component to accept draggable elements from the same drop zone.

@Directive({
  selector: '[myDropTarget]'
})
export class DropTargetDirective {
  constructor(private dragService: DragService) {
  }

  @Input()
  set myDropTarget(options: DropTargetOptions) {
    if (options) {
      this.options = options;
    }
  }

  @Output('myDrop') drop = new EventEmitter();

  private options: DropTargetOptions = {};

  @HostListener('dragenter', ['$event'])
  @HostListener('dragover', ['$event'])
  onDragOver(event) {
    const { zone = 'zone' } = this.options;

    if (this.dragService.accepts(zone)) {
       event.preventDefault();
    }
  }

  @HostListener('drop', ['$event'])
  onDrop(event) {
    const data =  JSON.parse(event.dataTransfer.getData('Text'));

    this.drop.next(data);
  }
}
export interface DropTargetOptions {
  zone?: string;
}

Implementation notes:

  1. Again we expose an @Input property that allows us to specify the drop zone: <div [myDropTarget]="{zone:'dropzone1'}"></div>.
  2. We create an @Output property which will emit whenever the user drops something. Notice that it is exposed with a prefix to the outside world. This avoids naming conflicts with existing DOM or directive events.
  3. onDragOver handles the dragover event and prevents the default behavior when the element being dragged. is from the drop same zone. To do so it asks the DragService. It also handles dragenter for Internet Explorer compatibility.
  4. onDrop handles the drop event and emits a myDrop event with the data associated with the drag operation.

Usage

To use the draggable directive decorate an element with the myDraggable attribute. Optionally specify data and drop zone.

To use the drop target directive decorate an element with the myDropTarget attribute and subscribe to the myDrop event. Optionally specify the drop zone.

@Component({
  selector: 'my-app',
  template: `
    <div>
      <div [myDraggable]="{data: 'Draggable 1'}">Drag me</div>
      <div [myDraggable]="{data: 'Draggable 2'}">Drag me</div>
      <div myDropTarget (myDrop)="onDrop($event)">Drop here</div>
      <div [myDropTarget]="{zone:'another'}">Drop here</div>
    </div>
  `,
})
export class App {
  onDrop(data: any) {
    console.log('dropped:', data)
  }
}

Live demo on Plunker.

Cheers!

radzen.png

Radzen is a rapid application development solution for Angular. Create modern web apps without writing HTML, CSS or JavaScript. Connect them to your REST API, OData service or MS SQL Server.

Try it for free now!