3

I have a Vue-structure like this:

App
 |--{ data: { scrollInfo {...} } }
 |
 |--Component1
 |      |--{ data: { sidebarWidth: 500 }
 |
 |--Component2
 |      |--{ data: { overlayItemWidth: 389 }
 |
 |--Component3
 ...
 ...

In each of my components, I have elements that has variables that should change on scroll and resize (such as sidebarWidth or overlayItemWidth ). I found this post here that shows this way of adding a listener for scroll and resize.

I've added it to the main instance, like so:

data: {
  scrollInfo: {
    scrollFromTop: 0,
    viewportHeight: 0,
    viewportWidth: 0,
    pageHeight: 0
  }
},
created() {
  window.addEventListener( 'scroll', this.calculateScroll );
  window.addEventListener( 'resize', this.calculateViewport );
  window.addEventListener( 'resize', this.calculatePageScrollSpecs );
  this.calculateViewport();
  this.calculateScroll();
  this.calculatePageScrollSpecs();
},
destroyed() {
  window.removeEventListener( 'scroll', this.calculateScroll );
  window.removeEventListener( 'resize', this.calculateViewport );
  window.removeEventListener( 'resize', this.calculatePageScrollSpecs );
}

I wont show the contents of the methods (calculateScroll, calculateViewport, ... ), since it's not relevant to this question.

Now... In my components I have variables that should change and reevaluate on scroll and on resize as well. But every time I have such a variable, then I currently add those same listeners in created and destroyed, and then add the same event-listeners to the given component, and then do the calculations from a new method in there. It seems long and clumbsy.

Are there a way that I can get around having those window.AddEventListeners in each component, but only have those in my root instance?

... I was thinking, if I had an array of 'things-that-need-to-be-recalculated-on-scroll-or-resize' in my main instance, but I'm not sure if it would get cluttered, since the variables for the components then actually wouldn't be kept in the component, but instead be referenced with this.$root.sidebarWidth. And this would also make my main instance massive.

Any suggestions?

Zeth
  • 2,273
  • 4
  • 43
  • 91
  • Perfect use case for a [custom directive](https://css-tricks.com/power-custom-directives-vue/), or you could use a [third party](https://github.com/wangpin34/vue-scroll) implementation – Stephen Thomas Jan 16 '19 at 01:05
  • Have you tried mixins? – Styx Feb 03 '19 at 19:26
  • Hmm... Ehm... No, - can you give an example of such an implementation? – Zeth Feb 03 '19 at 19:29
  • Hi, mixins are not the way to go since they don't remove the event duplication problem. You can use Vuex for a dedicated state management (it's basically a global variables manager) but it would maybe be overkill in your case. Your best bet is to calculate your data in App component and pass it as `props` to all the child components. This way they get the same data at the same time and can manipulate it individually. – kursus Feb 09 '19 at 22:57
  • is there a reason why you don't use `computed` in your components and just pass `scrollInfo` as a prop? basically they will watch `scrollInfo` and reevaluate every time it changes. – fila90 Feb 10 '19 at 15:12

3 Answers3

1

Okay... No answers at all, - so I dived down into it myself. This is the best I could come up with. I hope it helps others in the need of the same.

I was torn between using extends (source) or mixins (source).

After a bit of research, I ended up using mixins (this video hit the nail on the head regarding what I was trying to achieve and how to solve it). If you want to make the mixin global, for all components, then look at this video around 4:11 and onwards. I'm not doing that in below-written explanation.

Please note, that I'm using webpack alongside laravel-mix with below-written solution. It's used in a WordPress-installation.

Disclosure

This is a mix of a bazillion different answers and articles. It would take forever to credit it all to the correct owners and sources, so I'm not even gonna try. Sorry.

The mixins-file (./mixins/scrollAndResize.js).

export const scrollAndResizeMixin = {
  created() {
    console.log( 'scrollAndResizes loaded' );
    this.calcScroll();
    this.calcPageAndViewport();
  },
  data: function() {
    return {
      scrollFromTop: 0,
      viewportHeight: 0,
      viewportWidth: 0,
      pageHeight: 0
    }
  },
  methods: {
    calcScroll: function (){
      let doc = document.documentElement;
      this.scrollFromTop = (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0);

      // DEBUG (uncomment and scroll, to check if it works)
      // console.log( (window.pageYOffset || doc.scrollTop) );
      // console.log( (doc.clientTop || 0) );
    },
    calcPageAndViewport: function(){
      // Viewport info
      this.viewportHeight = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
      this.viewportWidth = Math.min(document.documentElement.clientWidth, window.innerWidth || 0);

      // Page length
      var body = document.body;
      var html = document.documentElement;
      this.pageHeight = Math.max(
        body.scrollHeight,
        body.offsetHeight,
        html.clientHeight,
        html.scrollHeight,
        html.offsetHeight
      );
    },

  }
};

The main js-file (./app.js).

// Vue
import Vue from 'vue';

// Mixins
import { scrollAndResizeMixin } from './mixins/scrollAndResize';

// The Application
const app = new Vue({
  mixins: [
    scrollAndResizeMixin
  ],
  computed: {
    mobile: function() {
      return this.viewportWidth < 992; // This uses the mixin
    }
  },
  created() {
    window.addEventListener( 'scroll', this.calcScroll );
    window.addEventListener( 'resize', this.calcPageAndViewport );
  },
  destroyed() {
    window.removeEventListener( 'scroll', this.calcScroll );
    window.removeEventListener( 'resize', this.calcPageAndViewport );
  }
}); 

And/or use it only in a component like so...

<template>

  <div>
    <p v-if="viewportWidth > 992">
      Desktop
    </p>
    <p v-else>
      Mobile
    </p>   
  </div>

</template>



<script>
  import { scrollAndResizeMixin } from '../mixins/scrollAndResize';

  export default {
    mounted() {
    },
    mixins: [
      scrollAndResizeMixin
    ],
    created() {
      window.addEventListener('scroll', this.calcScroll);
      window.addEventListener('resize', this.calcPageAndViewport);
    },
    destroyed() {
      window.removeEventListener('scroll', this.calcScroll);
      window.removeEventListener('resize', this.calcPageAndViewport);
    }
  }
</script>
tony19
  • 125,647
  • 18
  • 229
  • 307
Zeth
  • 2,273
  • 4
  • 43
  • 91
  • For reference here's a [codesandbox](https://codesandbox.io/s/agitated-heisenberg-n3q94?file=/src/App.vue) for this. – Kalnode May 26 '20 at 00:35
1

Glad you found a solution for yourself, but for future visitors who may want a little more comfortable/general solution: I had the same problem some months ago and put a Vue mixin on npm that does exactly this — handling global data once-per-lifetime, not once per mounted component.

It's called Reactivator and this is how you'd create a global handler for window.resize with it:

// viewport-width.js

export default {
  // Reactivator uses `getInitialState` to fetch the
  // initial value before any listener was attached.
  getInitialState() {
    return window.innerWidth
  },

  // Reactivator calls `listen` when the first component using
  // it is created and executes the returned cleanup callback
  // when it's the last component using it is destroyed.
  listen(set) {
    const resizeHandler = () => set(window.innerWidth)

    window.addEventListener('resize', resizeHandler)

    return () => window.removeEventListener('resize', resizeHandler)
  }
}


// component.js

import reactivator from 'vue-reactivator'
import viewportWidth from './viewport-width.js'

export default {
  mixins: [
    // `viewportWidth` will now be a reactive property in this component!
    reactivator({ viewportWidth })
  ]
}

That said, watching viewport size is such a common case that I have put it (and other useful things like scroll position or mouse position) in an extra package of common Reactivator implementations. You can find it on npm as vue-browser-state.

Loilo
  • 13,466
  • 8
  • 37
  • 47
0

You should be making a custom component that can accept as props the things that change and that does all the operations needed (such as adding and removing event listeners).

Michael
  • 4,538
  • 5
  • 31
  • 58