vueのカスタムディレクティブで関数を共有する

ハマったのでメモ

やりたかったこと

vue-routerのscrollBehaviorはページの遷移をフックしてwindow objectの遷移後のスクロールポジションを決定するものだが、同じタイミングをフックして子componentのスクロール位置を指定したかった。

https://router.vuejs.org/ja/api/#scrollbehavior

調査したこと

custom directiveのAPI

カスタムディレクティブとは?

Vue.js 本体で出荷されたディレクティブの標準セットに加えて (v-model と v-show)、カスタムディレクティブ (custom directive) を登録することができます。Vue 2.0 では、コードの再利用と抽象化における基本の形はコンポーネントです。しかしながら、通常の要素で低レベル DOM にアクセスしなければならないケースがあるかもしれません。こういった場面では、カスタムディレクティブが役立つでしょう。

https://jp.vuejs.org/v2/guide/custom-directive.html

フックを超えてデータを共有する場合は?

el を除いて、これらの全てのプロパティは読み込みのみ (read-only) で変更しないものとして扱わなくてはいけません。フックを超えてデータを共有する必要がある場合は, 要素の dataset を通じて行うことが推奨されています。

https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset

とのこと。サンプルは変数のみを扱っていて関数は特に扱っていない。

実装方法

このコメントが参考になり実装方針を決めた

@blake-newman I disagree, I think that each router-view (component) should define it’s scrollEl with a local scrollEl property on the component, or a v-scrollEl directive in html.

In terms of encapsulation, each component could be from a different source, as an app could be composed of many separate components from different authors on the web. Each component know’s which child element within needs to scroll. It would be inappropriate to let the app decide what element within all components should be scrollable (as you would have to go into each component and add the scroll class/id).

If anything, vue should implement an html directive ie: <div v-scrollEl></div> that vue automatically picks up to override scroll position tracking for that component only.

This way it:

  1. Is up to the component what child element scrolls
  2. Provides consistent syntax across components and teams for readability
  3. Does not pollute router config

各コンポーネントは様々なソースである可能性があるため、コンポーネント内のどの要素をスクロールさせるかをアプリに決定させるのは不適切。各コンポーネントにスクロール用のクラス/IDを付与する必要もある。ディレクティブを作成することによって、それを回避してスクロールの位置を追跡するための処理をコンポーネントごとに書かなくてもよくなる。

https://github.com/vuejs/vue-router/issues/1187#issuecomment-361733175

実際にはどう書いたか

ここissue参考に
https://github.com/vuejs/vue/issues/314

1
2
3
4
5
6
7
8
9
10
11
12
13
import Vue from 'vue';

Vue.directive('scrollElement', {
  bind: function(el) {
    (el as any).unbindEvent = (window as any).$nuxt.$router.afterEach(() => {
      el.scrollTop = 0;
      el.scrollLeft = 0;
    });
  },
  unbind: function(el) {
    (el as any).unbindEvent();
  },
});

ソースはtypescript。
注意した点は

The solution proposed by @HcySunYang looks like some kind of hack. $destroy is misleading as it seems like it’s something vue-related. It’s in fact an arbitrary key, using el.i_am_a_banana would work as well. @yyx990803 Can you confirm that this is the intended correct way of achieving this? Or maybe we should not remove event listeners at all? Does Vue’s DOM management guarantee that there will be no memory leak issues?

ハックのような書き方をしているのに、$を先頭につけると、vueに関連したもののように見える。
とのことなので確かにと思い変更。

参考

1 COMMENT

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です