Vueコンポーネントは素晴らしいですよね? これらは、アプリのビューと動作を小さな構成可能な部分にカプセル化します。 それらに少し追加の機能が必要な場合は、ディレクティブを添付するだけです! つまり、ディレクティブはかなり柔軟性がなく、すべてを実行できるわけではありません。 たとえば、ディレクティブは(簡単に)イベントを発行できません。 もちろん、これはVueであり、解決策があります。 抽象的なコンポーネント!

抽象コンポーネントは、DOMに何もレンダリングしないことを除いて、通常のコンポーネントと似ています。 それらは、既存の動作に追加の動作を追加するだけです。 <transition><component><slot>など、Vueの組み込みコンポーネントの抽象コンポーネントに精通しているかもしれません。

抽象コンポーネントの優れたユースケースは、要素がIntersectionObserverでビューポートに入るタイミングを追跡することです。 ここでそれを処理するための単純な抽象コンポーネントの実装を見てみましょう。

これを適切に本番環境で実装したい場合は、このチュートリアルのベースとなっているvue-intersectをご覧ください。

入門

まず、コンテンツをレンダリングするだけの簡単な抽象コンポーネントを作成します。 これを実現するために、レンダリング機能について簡単に説明します。

IntersectionObserver.vue
export default {
   // Enables an abstract component in Vue.
   // This property is undocumented and may change at any time,
   // but your component should work without it.
  abstract: true,
  // Yay, render functions!
  render() {
    // Without using a wrapper component, we can only render one child component.
    try {
      return this.$slots.default[0];
    } catch (e) {
      throw new Error('IntersectionObserver.vue can only render one, and exactly one child component.');
    }

    return null;
  }
}

おめでとう! これで、何もしない抽象的なコンポーネントができました。 子をレンダリングするだけです。

IntersectionObserverの追加

では、IntersectionObserverのロジックに固執しましょう。

IntersectionObserverはIEまたはSafariでネイティブにサポートされていないため、polyfillを入手することをお勧めします。

IntersectionObserver.vue
export default {
   // Enables an abstract component in Vue.
   // This property is undocumented and may change at any time,
   // but your component should work without it.
  abstract: true,
  // Yay, render functions!
  render() {
    // Without using a wrapper component, we can only render one child component.
    try {
      return this.$slots.default[0];
    } catch (e) {
      throw new Error('IntersectionObserver.vue can only render one, and exactly one child component.');
    }

    return null;
  },

  mounted () {
    // There's no real need to declare observer as a data property,
    // since it doesn't need to be reactive.

    this.observer = new IntersectionObserver((entries) => {
      this.$emit(entries[0].isIntersecting ? 'intersect-enter' : 'intersect-leave', [entries[0]]);
    });

    // You have to wait for the next tick so that the child element has been rendered.
    this.$nextTick(() => {
      this.observer.observe(this.$slots.default[0].elm);
    });
  }
}

これで、次のように使用できる抽象的なコンポーネントができました。

<intersection-observer @intersect-enter="handleEnter" @intersect-leave="handleLeave">
  <my-honest-to-goodness-component></my-honest-to-goodness-component>
</intersection-observer>

まだ終わっていません…

仕上げ

コンポーネントがDOMから削除されるときに、ぶら下がっているIntersectionObserversを残さないようにする必要があるので、今すぐ修正しましょう。

IntersectionObserver.vue
export default {
   // Enables an abstract component in Vue.
   // This property is undocumented and may change at any time,
   // but your component should work without it.
  abstract: true,
  // Yay, render functions!
  render() {
    // Without using a wrapper component, we can only render one child component.
    try {
      return this.$slots.default[0];
    } catch (e) {
      throw new Error('IntersectionObserver.vue can only render one, and exactly one child component.');
    }

    return null;
  },

  mounted() {
    // There's no real need to declare observer as a data property,
    // since it doesn't need to be reactive.

    this.observer = new IntersectionObserver((entries) => {
      this.$emit(entries[0].isIntersecting ? 'intersect-enter' : 'intersect-leave', [entries[0]]);
    });

    // You have to wait for the next tick so that the child element has been rendered.
    this.$nextTick(() => {
      this.observer.observe(this.$slots.default[0].elm);
    });
  },

  destroyed() {
    // Why did the W3C choose "disconnect" as the method anyway?
    this.observer.disconnect();
  }
}

そしてボーナスポイントのためだけに、オブザーバーのしきい値を小道具で構成できるようにしましょう。

IntersectionObserver.vue
export default {
   // Enables an abstract component in Vue.
   // This property is undocumented and may change at any time,
   // but your component should work without it.
  abstract: true,

  // Props work just fine in abstract components!
  props: {
    threshold: {
      type: Array
    }
  },

  // Yay, render functions!
  render() {
    // Without using a wrapper component, we can only render one child component.
    try {
      return this.$slots.default[0];
    } catch (e) {
      throw new Error('IntersectionObserver.vue can only render one, and exactly one child component.');
    }

    return null;
  },

  mounted() {
    // There's no real need to declare observer as a data property,
    // since it doesn't need to be reactive.

    this.observer = new IntersectionObserver((entries) => {
      this.$emit(entries[0].isIntersecting ? 'intersect-enter' : 'intersect-leave', [entries[0]]);
    }, {
      threshold: this.threshold || 0
    });

    // You have to wait for the next tick so that the child element has been rendered.
    this.$nextTick(() => {
      this.observer.observe(this.$slots.default[0].elm);
    });
  },

  destroyed() {
    // Why did the W3C choose "disconnect" as the method anyway?
    this.observer.disconnect();
  }
}

最終的な使用法は次のようになります。

<intersection-observer @intersect-enter="handleEnter" @intersect-leave="handleLeave" :threshold="[0, 0.5, 1]">
  <my-honest-to-goodness-component></my-honest-to-goodness-component>
</intersection-observer>

どうぞ! 最初の抽象コンポーネント。

ThomasKjærgaard/ Heavyy の最初の実装とアイデアに感謝します!