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 の最初の実装とアイデアに感謝します!