システムから発行したIDやパスワードなどを、ユーザーが画面上で簡単にクリップボードにコピーできるようにしたいなと思って実現方法を調べていたところvue-clipboard2というモジュールが見つかったので使ってみることにしました。普通に使えばドキュメント通りに実装すれば動くと思うのですが、Vuetifyのv-dialog
の中で使おうとするとかなり手こずったので、動かす方法をまとめます。
環境
- Vue.js 2.6.11
- Vuetify 2.2.11
- TypeScript 3.9.3
- vue/cli
- クラスベース記法
vue-clipboard2とは
Vueでクリップボードへテキストをコピーできるモジュールです。ボタンをクリックするとテキストをクリップボードにコピーする、といったことが(通常は)簡単にできます。
セットアップは以下の通り。
$ npm install vue-clipboard2
import VueClipboard from 'vue-clipboard2';
Vue.use(VueClipboard);
問題
ドキュメントにbootstrapのmodal内だと細工しないと動かないよと書いてありますが、Vuetifyのv-dialog
でも当然のように動きませんでした。対処法は探すといくつか見つかりはしますが、TypeScriptを使っている事例がなかなかなく、かなり試行錯誤が必要でした。
解決方法
2つある使い方のうちv-clipboard:copy
を使う方法だとどうやっても駄目でした。
$copyText
を使う場合、以下のように実装することで動かすことができました。
<template>
<v-dialog>
<v-card>
<v-card-text>
<v-text-field :value="textToCopy" readonly/>
<v-btn icon ref="copyBtn" @click="onCopy"><v-icon>mdi-content-copy</v-icon></v-btn>
</v-card-text>
</v-card>
</v-dialog>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
@Component
export default class SampleDialog extends Vue {
textToCopy = 'some text to copy';
private onCopy(): void {
const isVue = (x: unknown): x is Vue => x instanceof Vue;
const { copyBtn } = this.$refs;
if (!isVue(copyBtn)) {
return;
}
this.$copyText(this.textToCopy, copyBtn.$el);
}
}
</script>
ハマりどころは$copyText
の第2引数でした。ドキュメントにはcontainer
を渡せと雑に書いてはあるものの、実際何型の何を渡せばいいのやら、それらしい可能性を片っ端から試していって上記のコードに辿り着きました。
ボタンにコピー機能を仕込む場合はそのボタンをコンテナとして指定すればいいようです。型はVeturのコード補完ではobject | HTMLElement
と出てきますがElement
型のオブジェクトを渡すとうまくいきました。
ボタンをElement
として取得する方法も(Vue慣れてる人だと簡単かもしれませんが)結構悩みました。
v-btn
タグにref
属性を付けるとthis.$refs
経由で参照できるようになりますが、その際の型はVue | Element | Vue[] | Element[]
です。上記のコードの場合copyBtn
で実際に参照できるオブジェクトの型はVue
ですが(v-btn
もコンポーネントである以上Vue
を継承している形になっている)、TypeScript的には使う前にタイプガードで型をVue
に絞ってあげる必要がありました。(x: unknown): x is Vue => x instanceof Vue
のところですね。このチェックを通すことで型が保証され、以降はVue
型として扱えるようになります。あとはVue
の$el
プロパティがElement
なので、それを$copyText
の第2引数に渡せばTHE ENDにゃん。
ちなみにVueClipboard.config.autoSetContainer = true;
という方法も紹介されていますが、こちらはうまくいきませんでした。
もうひとつちなみに、ドキュメントには非同期関数内で使わないでねと書いてありますが、上記コードのonCopy
にasync
付けても動きました。$copyText
はPromise
を返すので、処理結果までちゃんと見たい場合はconst result = await this.$copyText(...);
のように書くことができます。もちろんthis.$copyText(...).then(...).catch(...);
で書いてもOKです。