DEV Community

zchtodd
zchtodd

Posted on • Originally published at codetodd.com

Creating a resizable/draggable component in Angular2

Alt Text

This post covers how to create a resizable and draggable component in Angular2. The resizing is modeled after what you might expect to see in a desktop window environment. The resizable component is a rectangle with each corner containing a “hotspot” that listens for mouse down events. The component can also be dragged anywhere on the page.

To make this possible, we need to hook into three different browser events, on different DOM objects.

Event DOM Object
mousedown div The mousedown event signifies that we’re about to start dragging or resizing. This event is only relevant when received in the context of either the movable box or one of its corners, and so the listener is attached only to the div elements.
mousemove div, document This event indicates that the box is being dragged or resized. On each of these events, an offset is calculated from the last known cursor position and applied to the x, y, width, and height variables of the model.
mouseup document Moving or dragging has stopped when this event is fired. The listener is attached only to the document so that dragging or movement is stopped no matter where the cursor travels, as it will frequently escape the bounds of the box if the user flicks the cursor rapidly.

Let’s take a look at the template to see how the listeners and model variables are applied to the DOM.

@Component({
  selector: 'chat-window',
  template: `
    <style>
        #chat {
            position: fixed;
            background-color: black;
            opacity: 0.8;
        }   
        .chat-corner-resize {
            position: absolute;
            width: 10px;
            height: 10px;
        }   
        #chat-top-left-resize { top: 0px; left: 0px; }
        #chat-top-right-resize { top: 0px; right: 0px; }
        #chat-bottom-left-resize { bottom: 0px; left: 0px; }
        #chat-bottom-right-resize { bottom: 0px; right: 0px; }

    </style>
    <div id='chat' [style.top.px]='y'  
                   [style.left.px]='x' 
                   [style.width.px]='width'
                   [style.height.px]='height'
                   (mousedown)='onWindowPress($event)'
                   (mousemove)='onWindowDrag($event)'>
        <div (mousedown)='onCornerClick($event, topLeftResize)' id='chat-top-left-resize' class='chat-corner-resize'></div>
        <div (mousedown)='onCornerClick($event, topRightResize)' id='chat-top-right-resize' class='chat-corner-resize'></div>
        <div (mousedown)='onCornerClick($event, bottomLeftResize)' id='chat-bottom-left-resize' class='chat-corner-resize'></div>
        <div (mousedown)='onCornerClick($event, bottomRightResize)' id='chat-bottom-right-resize' class='chat-corner-resize'></div>
    </div>
  `
})

The mousedown listener also passes a function to the onCornerClick method. Each corner has its own function for determining how the x, y, width, and height variables should be manipulated during resizing.

The other point worth noting here is that the mouseup listener is missing. That’s because the component must respond to mouseup events occurring anywhere on the page, and not just inside the component. Since the listener can’t by definition be attached to anything in the template, the Angular2 HostListener decorator must be used to capture events outside the component.

I’ll show the full code below to display these concepts in action.

import { Component, HostListener, OnInit } from '@angular/core';

@Component({
  selector: 'chat-window',
  template: `
    <style>
        #chat {
            position: fixed;
            background-color: black;
            opacity: 0.8;
        }
        .chat-corner-resize {
            position: absolute;
            width: 10px;
            height: 10px;
        }
        #chat-top-left-resize { top: 0px; left: 0px; }
        #chat-top-right-resize { top: 0px; right: 0px; }
        #chat-bottom-left-resize { bottom: 0px; left: 0px; }
        #chat-bottom-right-resize { bottom: 0px; right: 0px; }

    </style>
    <div id='chat' [style.top.px]='y'
                   [style.left.px]='x'
                   [style.width.px]='width'
                   [style.height.px]='height'
                   (mousedown)='onWindowPress($event)'
                   (mousemove)='onWindowDrag($event)'>
        <div (mousedown)='onCornerClick($event, topLeftResize)' id='chat-top-left-resize' class='chat-corner-resize'></div>    
        <div (mousedown)='onCornerClick($event, topRightResize)' id='chat-top-right-resize' class='chat-corner-resize'></div>    
        <div (mousedown)='onCornerClick($event, bottomLeftResize)' id='chat-bottom-left-resize' class='chat-corner-resize'></div>    
        <div (mousedown)='onCornerClick($event, bottomRightResize)' id='chat-bottom-right-resize' class='chat-corner-resize'></div>    
    </div> 
  `
})
export class ChatWindowComponent implements OnInit {

  x: number;
  y: number;
  px: number;
  py: number;
  width: number;
  height: number;
  minArea: number;
  draggingCorner: boolean;
  draggingWindow: boolean;
  resizer: Function;

  constructor() { 
    this.x = 300;
    this.y = 100;
    this.px = 0;
    this.py = 0;
    this.width = 600;
    this.height = 300; 
    this.draggingCorner = false;
    this.draggingWindow = false;
    this.minArea = 20000
  }

  ngOnInit() {
  }

  area() {
    return this.width * this.height;
  }

  onWindowPress(event: MouseEvent) {
    this.draggingWindow = true;
    this.px = event.clientX;
    this.py = event.clientY;
  }

  onWindowDrag(event: MouseEvent) {
     if (!this.draggingWindow) {
        return;
    }
    let offsetX = event.clientX - this.px;
    let offsetY = event.clientY - this.py;

    this.x += offsetX;
    this.y += offsetY;
    this.px = event.clientX;
    this.py = event.clientY;
  }

  topLeftResize(offsetX: number, offsetY: number) {
    this.x += offsetX;
    this.y += offsetY;
    this.width -= offsetX;
    this.height -= offsetY;
  }

  topRightResize(offsetX: number, offsetY: number) {
    this.y += offsetY;
    this.width += offsetX;
    this.height -= offsetY;
  }

  bottomLeftResize(offsetX: number, offsetY: number) {
    this.x += offsetX;
    this.width -= offsetX;
    this.height += offsetY;
  }

  bottomRightResize(offsetX: number, offsetY: number) {
    this.width += offsetX;
    this.height += offsetY;
  }

  onCornerClick(event: MouseEvent, resizer?: Function) {
    this.draggingCorner = true;
    this.px = event.clientX;
    this.py = event.clientY;
    this.resizer = resizer;
    event.preventDefault();
    event.stopPropagation();
  }

  @HostListener('document:mousemove', ['$event'])
  onCornerMove(event: MouseEvent) {
    if (!this.draggingCorner) {
        return;
    }
    let offsetX = event.clientX - this.px;
    let offsetY = event.clientY - this.py;

    let lastX = this.x;
    let lastY = this.y;
    let pWidth = this.width;
    let pHeight = this.height;

    this.resizer(offsetX, offsetY);
    if (this.area() < this.minArea) {
        this.x = lastX;
        this.y = lastY;
        this.width = pWidth;
        this.height = pHeight;
    }
    this.px = event.clientX;
    this.py = event.clientY;
  }

  @HostListener('document:mouseup', ['$event'])
  onCornerRelease(event: MouseEvent) {
    this.draggingWindow = false;
    this.draggingCorner = false;
  }
}

Top comments (2)

Collapse
 
andixboya profile image
Andreyan Boyadzhiev

Works like a charm, thank you for the shared effort, best of luck in the future!

Collapse
 
hypertextcoffeepot profile image
Charles J

Awesome. Nice and simple. 🎉🔥