2020-03-31
【Vue.js】 $nextTick() で動的に描写される DOM 要素の位置や高さを取り扱う
やりたいこと
- Vue.js で、動的に描写される要素の画面上部からの高さを取得したい。
- ロード中はロード画面が表示される。(
loading: bool
で状態管理) - 絞り込みを行った場合は検索条件ボックスが表示されるため、上からの高さが動的に変化する。
- ロード中はロード画面が表示される。(
算出プロパティでの取得はできない
おなじみの vm.$refs
で特定の DOM 要素にアクセスできるので、computed() で取得しようとしたところ…
<div ref="targetEl"></div>
computed: {
targetOffsetTop() {
// Error: Cannot read property 'getBoundingClientRect' of undefined
return (
this.$refs.targetEl.getBoundingClientRect().top + window.pageYOffset;
);
}
}
computed は マウントよりも早いタイミングで実行されるため、computed の中で vm.$refs
にアクセスすることはできません。
$refs はリアクティブでない
では $refs
の変化に応じて computed()
させればいいのでは?と考え、以下のように記述。
computed: {
targetOffsetTop() {
if (this.$refs.length === 0) {
return 0;
}
return (
this.$refs.targetEl.getBoundingClientRect().top + window.pageYOffset;
);
}
}
しかし、マウントが完了して $refs.targetEl
が見れるようになっても、 targetOffsetTop=0
のまま値が変わらない。
これは公式ドキュメントの注意書きにもありますが、$refs
がリアクティブでないためです。
$refsはコンポーネントの描画後にデータが反映されるだけで、リアクティブではありません。子コンポーネントへの直接操作のための、退避用ハッチのような意味合いです(テンプレート内または算出プロパティから$refsにアクセスするのは避けるべきです)。
(そもそもここに算出プロパティで $refs
にアクセスするなと書いてありました)
描画が完了してから処理を行うには、this.$nextTick() を使おう
そもそも今回やりたいことは、this.loading=false
と状態が変化し DOM が更新されてから $refs.targetEl
の位置を取得したいというものです。
このような処理には Vue.nextTick(callback)
を利用します。
データの変更後に Vue.js の DOM 更新の完了を待つには、データが変更された直後に Vue.nextTick(callback) を使用することができます。そのコールバックは DOM が更新された後に呼ばれます。
(リアクティブの探求…かっこいい)
したがって処理を整理すると、
this.loading = false
として状態を変化this.nextTick()
で状態変化によって DOM が更新されるのを待つ- DOM の描写が完了した状態で、適切に
this.$refs.targetEl
を取得
というようになります。正しく動作するコードは以下の通りです。
data: () => ({
items: []
loading: true,
targetOffsetTop: 0,
}),
async mounted(): {
// api を叩いてデータを取得したりする処理
this.items = await this.fetchItems();
this.loading = false;
// DOM が更新されるのを待つ
await this.$nextTick();
// 待った後、`$refs` にアクセス
this.offsetTop =
this.$refs.container.getBoundingClientRect().top + window.pageYOffset;
}
リアクティブの探求をすることで問題が解決しました。仮想 DOM は奥が深いですね。