Creating a resizable/draggable component in Angular2
Tags: webdev, angular, typescript
Published: 2019-10-07 22:31:42

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/w0jpdd3o9bdnyp9ww4zi.gif)

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.

```html
@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.

```typescript
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;
  }
}
```