'In Vuejs3 how to make the sticky header component change background colour when intersecting with other components?

I am just wondering, how to structure my code to make my sticky <Header /> component change its background-color when intersecting with other components (in this case with <Hero /> and <Footer />.

To make things more complicated, I use vue-router.

Link to my CodeSandbox

The structure is as follows:

App.vue

<template>
  <ul class="nav">
    <li>
      <router-link to="/">Home</router-link>
    </li>
    <li>
      <router-link to="/about">About</router-link>
    </li>
  </ul>
  <Header />
  <router-view />
</template>

views/Home.vue

<template>
  <Child1 />
  <Child2 />
  <Hero />
  <Child3 />
  <Footer />
</template>

<script>
import Hero from "@/components/Hero";
import Child1 from "@/components/Child1";
import Child2 from "@/components/Child2";
import Child3 from "@/components/Child3";
import Footer from "@/components/Footer";

export default {
  components: {
    Hero,
    Child1,
    Child2,
    Child3,
    Footer,
  },
};
</script>

When I follow some tutorials on yt, the authors use the IntersectionObserver, so I wanted to give it a try:

Header.vue

<template>
  <section
    class="header"
  >
    <div class="header__title"
         :class="{
           header__scroll: isContrastActive,
         }"
    >
      I am an awesome header that is changing it's background colour <br />
      when intersecting with other components
    </div>
  </section>
</template>

<script>
export default {
  data() {
    return {
      isContrastActive: false,
      observer: null
    };
  },
  methods: {
    activateObserver() {
      this.observer = new IntersectionObserver(
        ([entry]) => {
          console.log("isIntersecting: ", entry.isIntersecting);
          if (!entry.isIntersecting) {
            this.isContrastActive = true;
          } else {
            this.isContrastActive = false;
          }
        },
        { rootMargin: "-5% 0px 0px 0px" }
      );
      document
        .querySelectorAll(".observer")
        .forEach((el) => this.observer.observe(el));
    },
  },
  mounted() {
    this.activateObserver();
  },
};
</script>

<style scoped>
.header {
  position: sticky;
  top: 0;
  width: 100vw;
  height: auto;
  z-index: 10;
}
.header__title {
  background-color: yellow;
  color: black;
  padding: 8px 4px;
  text-align: center;
}
.header__scroll {
  background-color: orange;
}
</style>

Also added the class="observer" to the <Hero /> and <Footer /> components. But it doesn't seem to work.

I have read, that in vue the use of querySelector is considered to be an antipattern.

If this is the case then how I could structure my code to get what I want?

Thanks in advance


EDIT1:
I noticed, that if I change the text in the <Header /> component and save then everything seems to work just fine. But on page refresh, it is still not working.

Think that the querySelectorAll could be the reason.

SOLUTION 1: The most obvious solution is of course:

mounted() {
    setTimeout(() => {
      this.activateObserver()
    }, 500)
  },

but it ain't an elegant solution :/.



Solution 1:[1]

The code you're using makes the assumption all observed elements are already in DOM.

You need to make the observable available to all your pages (use the preferred state management solution).

Your activateObserver should become:

createObserver() {
  someStore().observer = new IntersectionObserver(
    ([entry]) => {
      if (!entry.isIntersecting) {
         this.isContrastActive = true;
      } else {
         this.isContrastActive = false;
      }
    },
    { rootMargin: "-5% 0px 0px 0px" }
  );
}

Then, in any page which has observed elements, use template refs to get all elements you want observed. In onMounted you start observing them, in onBeforeUnmount you stop observing them.

const observed = ref([]);

onMounted(() => {
  observed.value.forEach(someStore().observer.observe)
})
onBeforeUnmount(() => {
  observed.value.forEach(someStore().observer.unobserve)
})

How you select your elements using template refs is irrelevant for this answer. The point is you need to start observing the DOM elements referenced after the component has been mounted and unobserve them before unmount.


Note: in theory, it is possible that any of your views is rendered before the observer has been added to the store. (In practice it's a rare case, so you probably don't need this).

However, if that's the case, you want to create a watcher for the observer in your pages and start observing only after you have the observer, which might be after the page has been mounted.

The principle is simple: you need both observer and observed for this to work. observer is available after it is added to store, observed are available after onMounted until onBeforeUnmount in any of the pages containing them.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1