Simple Drag and Drop with Angular 2

Checkout Radzen Demos and download latest Radzen!

Updated on October 26, 2017 to fix the plunkr demo. 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',
  styles: [
    `
    .draggable {
      border: 1px solid #ccc;
      margin: 1rem;
      padding: 1rem;
      width: 6rem;
      cursor: move;
    }

    .drop-target {
      border: 1px dashed #ebebeb;
      margin: 1rem;
      padding: 1rem;
      width: 6rem;
    }
    `
  ],
  template: `
    <div>
      <div [myDraggable]="{data: 'Draggable A'}" class="draggable">Draggable A</div>
      <div [myDraggable]="{data: 'Draggable B'}" class="draggable">Draggable B</div>
      <div myDropTarget (myDrop)="onDrop($event)" class="drop-target">Accepts Draggable A or B</div>
      <div [myDropTarget]="{zone:'another'}" class="drop-target">Can't drop here</div>
    </div>
  `,
})
export class AppComponent {
  onDrop(data: any) {
    alert(`dropped: ${data}`);
  }
}

Live demo on Plunkr.

Cheers!

Leverage Radzen on LinkedIn

Yes, we are on LinkedIn and you should follow us!

Now, you have the opportunity to showcase your expertise by adding Radzen Blazor Studio and Radzen Blazor Components as skills to your LinkedIn profile. Present your skills to employers, collaborators, and clients.

All you have to do is go to our Products page on LinkedIn and click the + Add as skill button.

by Atanas Korchev

Testing Angular 2 apps with Webpack and mocha

Checkout Radzen Demos and download latest Radzen!
Read more