Angularでカスタムディレクティブを作ってみた（ドラッグスクロール）

業務でドラッグスクロールしたいという要望がありライブラリを探してみたんですが、色々あって独自で作ることになりました。

せっかくなので、いろんな場面で使いまわせるようにディレクティブで作ってみたので、その際のメモを残しておきます。



# ディレクティブとは

ディレクティブには2種類あります

- [構造ディレクティブ](https://angular.jp/guide/structural-directives)
 - `*ngIf`や`*ngFor`みたいなやつ
- [属性ディレクティブ](https://angular.jp/guide/attribute-directives)
 - `<p appHighlight>Highlight me!</p>`のように属性で指定する

今回は独自の属性ディレクティブを作って行きます




# カスタム属性ディレクティブを作る

### ディレクティブクラスの作成

- `@Directive`アノテーションを付与したディレクティブクラスを作成
- `selector`に指定した名称がhtmlで指定する属性になります
    - `[]`で囲うのを忘れないように！

```drag-scroll.directive.ts
import { Directive, HostListener, ElementRef } from '@angular/core';

@Directive({
    selector: '[dragScroll]'
})
export class DragScrollDirective {
    private position: { top: number, left: number, x: number, y: number };

    constructor(private el: ElementRef) { }

    get element() { return this.el.nativeElement as HTMLElement; }

    @HostListener('mousedown', ['$event']) onMouseDown(event: MouseEvent) {
        if (!this.element) { return; }

        // クリック時の位置を保持
        this.position = {
            top: this.element.scrollTop,
            left: this.element.scrollLeft,
            x: event.clientX,
            y: event.clientY,
        };

        return false;
    }

    @HostListener('mousemove', ['$event']) onMouseMove(event: MouseEvent) {
        if (this.position) {
            if (event.clientX && event.clientY) {
                // マウスの移動距離を算出
                const moveX = event.clientX - this.position.x;
                const moveY = event.clientY - this.position.y;
                // 移動距離だけスクロール
                this.element.scrollLeft = this.position.left - moveX;
                this.element.scrollTop = this.position.top - moveY;

                return false;
            }
        }
        return true;
    }
    @HostListener('mouseup') onMouseUp() { this.position = null; }
    @HostListener('mouseleave') onMouseLeave() { this.position = null; }
}
```

中身の処理は以下をやってるだけです。

- クリック時にクリック位置を保持
    - 通常の`mousedown`イベントを伝搬しないようにfalseを返してます
        - これをしないとマウスを動かしたときに範囲選択されてしまって挙動がおかしくなることがあったので
- クリックしたままマウスを動かしたら、動かした分だけスクロール
    - これもスクロールする場合はクリック時と同様にfalseを返してイベント伝搬しないようにしています
- クリック終了時およびカーソルがDOMの外に出た場合にクリック位置を破棄

#### モジュール化

- いろんな画面で使いまわせるようにモジュール化します
    - ※別にモジュール化しなくても使えます
        - 宣言は1か所でしかできないため、複数画面で使いたい場合はモジュール化が必須です。

```drag-scroll.module.ts
import { NgModule } from '@angular/core';
import { DragScrollDirective } from './drag-scroll.directive';

@NgModule({
    declarations: [DragScrollDirective],
    exports: [DragScrollDirective],
})
export class DragScrollModule { }
```


### カスタムディレクティブのインポート

- 作成したモジュールを使いたい場所のモジュールでインポート
    - モジュール化していない場合は`declarations`で直接宣言


```app.module.ts
・・・
import { DragScrollModule } from './shared/directives/drag-scroll/drag-scroll.module';

@NgModule({
  ・・・
  imports: [
    ・・・
    DragScrollModule,
  ],
  ・・・
})
export class AppModule { }
```

### HTMLへの組み込み

- htmlで対象のタグにカスタムディレクティブを指定

```app.component.html
<div dragScroll style="max-width: 300px; max-height: 300px; overflow: auto;">
    <div style="width: 500px; height: 500px;">
        test
    </div>
</div>
```

# ついでにドラッグスクロール要素内にドラッグ可能な要素を置いてみる

以下のように`draggable="true"`の要素を置くだけだと、ドラッグスクロールが勝ってしまいます

```app.component.html
<div dragScroll style="max-width: 300px; max-height: 300px; overflow: auto;">
    <div style="width: 500px; height: 500px;">
        test
        <div draggable="true" style="width: 100px; height: 100px; background-color: red">
            draggable
        </div>
    </div>
</div>
```

そのため、draggable要素のmousedownで一工夫してあげましょう

```app.component.html
<div dragScroll style="max-width: 300px; max-height: 300px; overflow: auto;">
    <div style="width: 500px; height: 500px;">
        test
        <div draggable="true" (mousedown)="onMouseDown($event)" style="width: 100px; height: 100px; background-color: red">
            draggable
        </div>
    </div>
</div>
```

```app.component.ts
    onMouseDown(event: MouseEvent) {
        // 親要素へのイベント伝搬を止める
        event.stopImmediatePropagation();
    }
```

このようにdraggable要素のmousedownにて`stopImmediatePropagation`を呼ぶことで親要素へイベントが伝搬されないため、dragScrollのmousedownはハンドリングされなくなり、ちゃんとドラッグ出来るようになります。


# 最後に

割と簡単にカスタムディレクティブが作れました！
なお、今回作ったものは[GitHub](https://github.com/teracy55/angular-drag-scroll)にアップしてます。