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:
- We prefix the attribute selector of the directive in order to avoid clashes with other directives or existing standard HTML attributes.
@HostBinding('draggable')
sets thedraggable
HTML attribute which enables HTML5 Drag and Drop.- We expose an
@Input
property named after the directive selector. It allows us to configure the directive:<div [myDraggable]="{zone:'dropzone1'}"></div>
. onDragStart
handles thedragstart
event handler of the host element. We store the data associated with the draggable directive viadataTransfer.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:
- Again we expose an
@Input
property that allows us to specify the drop zone:<div [myDropTarget]="{zone:'dropzone1'}"></div>
. - 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. onDragOver
handles thedragover
event and prevents the default behavior when the element being dragged. is from the drop same zone. To do so it asks theDragService
. It also handlesdragenter
for Internet Explorer compatibility.onDrop
handles thedrop
event and emits amyDrop
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!