3

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 :/.

1 Answers1

4

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.

tao
  • 82,996
  • 16
  • 114
  • 150
  • thanks for the answer. So you mean that I should use Vuex? But it seems like an overkill for this app. Or do mean, that I should use code by myself or something similar? – Dariusz Legizynski Apr 08 '22 at 06:40
  • 1
    I suggest you use `pinia`. It's a lot more intuitive than `vuex`. Another option is a simple export of `reactive()`. Ex: `export const obs = reactive({ observable: null })`. In any other files: `import { obs } from '../path'`; Modify `obs.observable` and the change will be in sync with any other component where you import `obs`. But still, my recommendation remains `pinia` as it has dev tools integration. You're going to end up using `pinia` a lot if you stick to `vue`. And really, it's perfect for state mgmt. – tao Apr 08 '22 at 18:00