Vue のカスタムディレクティブを TypeScript で開発してみる

# モチベーション

- [Vue のカスタムディレクティブ](https://jp.vuejs.org/v2/guide/custom-directive.html) を作ったことがなかったので、挑戦してみる
- せっかくやるならば TypeScript で
- せっかくやるならばタイプセーフで（ただしベストエフォート:grin:)
- せっかくやるならば npm に公開

# 作ったもの

**Vue Intersect Directive** ([NPM](https://www.npmjs.com/package/vue-intersect-directive) / [GitHub](https://github.com/megurock/vue-intersect-directive))

任意の要素（コンポーネント）がブラウザのビューポート内にあるかどうかを判別し、その情報を基にスタイルを適用したり、コールバック関数を呼び出します。スティッキー的な実装に役に立つかもしれません。

```html
<div v-intersect="{ true: ['visible'] }">Hello</div>
```

[デモ](https://megurock.github.io/vue-intersect-directive/dist/)

## Vue.directive インターフェースの確認

[Vue.directive API](https://jp.vuejs.org/v2/api/#Vue-directive) を使い、`Vue.directive('vue-intersect', IntersectDirective)` のように呼び出せる形を目指します。  
ひとまず Vue.dierective API の型定義を確認してみます。

```ts:node_modules/vue/types/vue.d.ts
directive(
  id: string,
  definition?: DirectiveOptions | DirectiveFunction
): DirectiveOptions;
```

```ts:node_modules/vue/types/options.d.ts
export interface DirectiveOptions {
  bind?: DirectiveFunction;
  inserted?: DirectiveFunction;
  update?: DirectiveFunction;
  componentUpdated?: DirectiveFunction;
  unbind?: DirectiveFunction;
}

export type DirectiveFunction = (
  el: HTMLElement,
  binding: DirectiveBinding,
  vnode: VNode,
  oldVnode: VNode
) => void;
```

## カスタムディレクティブの開発

今回は、要素がビューポートに存在しているかどうかを判別するのに、[IntersectionObserver](https://developer.mozilla.org/ja/docs/Web/API/Intersection_Observer_API) を使いました。

**bind フック関数** にて、IntersectionObserver を生成し、要素の監視を開始するだけでも機能はしそうですが、**unbind フック** で IntersectionObserver による監視を終了する実装も用意しました。

おおよその枠組みは以下のようになります。
実装の詳細は割愛しますので、興味がある型は GitHub のソースをご覧ください。

```ts:src/intersect-directive.ts
import { DirectiveOptions, DirectiveFunction, VNode } from 'vue'
import { DirectiveBinding } from 'vue/types/options'

/**
 *
 */
const bind: DirectiveFunction = (el: HTMLElement, binding: DirectiveBinding, vnode: VNode, oldVnode: VNode) => {
  // 具体的な実装
}

/**
 *
 */
const unbind: DirectiveFunction = (el: HTMLElement, binding: DirectiveBinding, vnode: VNode, oldVnode: VNode) => {
  // 具体的な実装
}

/**
 *
 */
const IntersectDirective: DirectiveOptions = {
  bind,
  unbind,
}

export default IntersectDirective
```

## プラグイン化

さらに [Vue.use API](https://jp.vuejs.org/v2/api/#Vue-use) を使い、`Vue.use(VueIntersect)` でも使えるようにしたのですが、正直なところ、ここの型付けについてはこれで正しいのかイマイチ分からず..。
Vue ではなく _Vue を参照するとかなんとか書いてある[サイト](https://www.mistergoodcat.com/post/vuejs-plugins-with-typescript)もあったのですが、よく意図が分からなかったです。:sweat:

```ts:src/index.ts
import IntersectDirective from './intersect-directive'
import Vue, { PluginObject, PluginFunction } from 'vue'

// window.Vue を TS に認識してもらってます。
declare global {
  interface Window {
    Vue: Vue | undefined
  }
}

const install: PluginFunction<never> = () => {
  Vue.directive('intersect', IntersectDirective)
}

const VueIntersect: PluginObject<never> = {
  install,
}

// import ではなく、<script> タグの読み込みの場合の処理です。
if (window.Vue) {
  Vue.use(VueIntersect.install)
}

export { IntersectDirective } // IntersectDirective をエクスポート 
export default VueIntersect   // PluginFunction をデフォルトエクスポート
```

## おまけ (NPM への公開まで）

カスタムディレクティブの開発よりも、むしろこちらの手順の方で調べることが多かったのでメモしておきます。

### モジュールのフォーマットについて

配布するモジュールは、[RollupJS](https://rollupjs.org) で、UMD、ESM、ブラウザ用 (iife) の 3 つをコンパイルしました。 
それぞれ [設定用ファイルを用意](https://github.com/megurock/vue-intersect-directive/tree/master/build)し、ブラウザ版パッケージにはミニファイ処理も追加しています。

### TypeScript の型定義ファイルの書き出し  
*.d.ts の書き出しは、`tsconfig.json` にて、`declaration` （と `declarationDir`）プロパティを設定すれば良いはずなのですが、今回は Rollup によるバンドルの影響なのか、なぜかビルド処理では型定義ファイルが出力がされませんでした。
仕方ないので、package.json に型定義ファイルをビルドするための、コマンドを用意しました。

```js
"build:dts": "tsc src/index.ts -d --emitDeclarationOnly --declarationDir dist/types"
```

以上になります。:sunglasses: