モチベーション
- Vue のカスタムディレクティブ を作ったことがなかったので、挑戦してみる
- せっかくやるならば TypeScript で
- せっかくやるならばタイプセーフで(ただしベストエフォート
)
- せっかくやるならば npm に公開
作ったもの
Vue Intersect Directive (NPM / GitHub)
任意の要素(コンポーネント)がブラウザのビューポート内にあるかどうかを判別し、その情報を基にスタイルを適用したり、コールバック関数を呼び出します。スティッキー的な実装に役に立つかもしれません。
<div v-intersect="{ true: ['visible'] }">Hello</div>
Vue.directive インターフェースの確認
Vue.directive API を使い、Vue.directive('vue-intersect', IntersectDirective)
のように呼び出せる形を目指します。
ひとまず Vue.dierective API の型定義を確認してみます。
directive(
id: string,
definition?: DirectiveOptions | DirectiveFunction
): DirectiveOptions;
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 を使いました。
bind フック関数 にて、IntersectionObserver を生成し、要素の監視を開始するだけでも機能はしそうですが、unbind フック で IntersectionObserver による監視を終了する実装も用意しました。
おおよその枠組みは以下のようになります。
実装の詳細は割愛しますので、興味がある型は GitHub のソースをご覧ください。
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 を使い、Vue.use(VueIntersect)
でも使えるようにしたのですが、正直なところ、ここの型付けについてはこれで正しいのかイマイチ分からず..。
Vue ではなく _Vue を参照するとかなんとか書いてあるサイトもあったのですが、よく意図が分からなかったです。
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 で、UMD、ESM、ブラウザ用 (iife) の 3 つをコンパイルしました。
それぞれ 設定用ファイルを用意し、ブラウザ版パッケージにはミニファイ処理も追加しています。
TypeScript の型定義ファイルの書き出し
*.d.ts の書き出しは、tsconfig.json
にて、declaration
(と declarationDir
)プロパティを設定すれば良いはずなのですが、今回は Rollup によるバンドルの影響なのか、なぜかビルド処理では型定義ファイルが出力がされませんでした。
仕方ないので、package.json に型定義ファイルをビルドするための、コマンドを用意しました。
"build:dts": "tsc src/index.ts -d --emitDeclarationOnly --declarationDir dist/types"
以上になります。