DEV Community

Sam M.
Sam M.

Posted on

Using MutationObserver and ResizeObserver to measure a changing DOM element in Vue

There is something like the window's onresize event listener but for DOM elements. It's called ResizeObserver and makes measuring dynamic elements very easy. The only problem is it's not standard. 😑 It's been in the editor's draft list for years. Almost all browsers support it. Safari started supporting it since 13.1 but Internet Explorer and Firefox android still don't support it. So... is there another way to measure a changing element other than a setInterval? Enter MutationObserver.

Here's a demo of the code that will be explained below:

The <template> or HTML part

<template>
  <div class="resize-observer">
    <div class="box" ref="box">
      <h4>Resize Me</h4>
      <p>
        width: <span class="size">{{ width }}</span>
      </p>
      <p>
        height: <span class="size">{{ height }}</span>
      </p>
    </div>
  </div>
</template>

We'll be resizing the div element with class "box" by giving it a css property resize: both. This will give the rendered box a small triangle on the bottom-right corner that allows for resizing.

Since we'll need access to the actual HTML element and its changing dimensions, we'll be using a ref. You can read more about refs here.

The <script> Code for Resizing with MutationObserver

<script>
  export default {
    data() {
      return {
        width: null,
        height: null,
        observer: null,
      }
    },

    mounted() {
      const box = this.$refs.box,
        boxSize = box.getBoundingClientRect()

      this.width = boxSize.width + 'px'
      this.height = boxSize.height + 'px'
      // initialize the observer on mount
      this.initObserver()
    },

We'll add the width, height and the observer to our state. Then when the component is mounted, we'll set the width and height. We'll also initialize the observer by calling this.initObserver.

    beforeDestroy() {
      if (this.observer) this.observer.disconnect()
    },

We also need to make sure we disconnect the observer before destroy.

We've now reached the main part of this tutorial. In the initObserver method, we create and initialize an observer which is an instance of MutationObserver. This observer tracks our box element's mutations and calls another method - onResize when the mutation type is attributes (the box's width and height attributes will fall under this). onResize is our resize handler.

    methods: {
     initObserver() {
        const config = {
            attributes: true,
          },
          vm = this

        // create the observer
        const observer = new MutationObserver(function (mutations) {
          mutations.forEach(function (mutation) {
            // check if the mutation is attributes and
            // update the width and height data if it is.
            if (mutation.type === 'attributes') {
              // call resize handler on mutation
              vm.onResize()
            }
          })
        })

        // observe element's specified mutations
        observer.observe(this.$refs.box, config)
        // add the observer to data so we can disconnect it later
        this.observer = observer
      },

The resize handler updates the state when the dimensions change. Optionally, you can emit an event that other components can listen to. More info on emitting events with vue.

      // Resize handler
      onResize() {
        const box = this.$refs.box,
          vm = this
        let { width, height } = box.style

        this.width = width
        this.height = height
        // Optionally, emit event with dimensions
        this.$emit('resize', { width, height })
      },
    },
  }
</script>

The <script> code for Resizing with ResizeObserver

Here's how you would do it with ResizeObserver. You'll notice that it's a lot less code when you implement with ResizeObserver. The <template> and <style> portion will stay the same.

<script>
  export default {
    data() {
      return {
        width: null,
        height: null,
        observer: null,
      }
    },

    mounted() {
      // initialize the observer on mount
      this.initObserver()
    },

    // unobserve before destroy
    beforeDestroy() {
      if (this.observer) this.observer.unobserve(this.$refs.box)
    },

    methods: {
      onResize() {
        const box = this.$refs.box,
          width = this.$refs.box.offsetWidth + 'px',
          height = this.$refs.box.offsetHeight + 'px'

        this.width = width
        this.height = height

        this.$emit('resize', { width, height })
      },
      initObserver() {
        const observer = new ResizeObserver(this.onResize)
        observer.observe(this.$refs.box)
        this.observer = observer
      },
    },
  }
</script>

And here's the full code with <template>, <script> and <style> for resizing with MutationObserver.

<template>
  <div class="resize-observer">
    <div class="box" ref="box">
      <h4>Resize Me</h4>
      <p>
        width: <span class="size">{{ width }}</span>
      </p>
      <p>
        height: <span class="size">{{ height }}</span>
      </p>
    </div>
  </div>
</template>

<script>
  export default {
    data() {
      return {
        width: null,
        height: null,
        observer: null,
      }
    },

    mounted() {
      const box = this.$refs.box,
        boxSize = box.getBoundingClientRect()

      this.width = boxSize.width + 'px'
      this.height = boxSize.height + 'px'
      // initialize the observer on mount
      this.initObserver()
    },

    //disconnect the observer before destroy
    beforeDestroy() {
      if (this.observer) this.observer.disconnect()
    },

    methods: {
      // Resize handler
      onResize() {
        const box = this.$refs.box,
          vm = this
        let { width, height } = box.style

        this.width = width
        this.height = height
        // Optionally, emit event with dimensions
        this.$emit('resize', { width, height })
      },

      initObserver() {
        const config = {
            attributes: true,
          },
          vm = this

        // create the observer
        const observer = new MutationObserver(function (mutations) {
          mutations.forEach(function (mutation) {
            // check if the mutation is attributes and
            // update the width and height data if it is.
            if (mutation.type === 'attributes') {
              // call resize handler on mutation
              vm.onResize()
            }
          })
        })

        // observe element's specified mutations
        observer.observe(this.$refs.box, config)
        // add the observer to data so we can disconnect it later
        this.observer = observer
      },
    },
  }
</script>

<style lang="scss" scoped>
  .resize-observer {
    text-align: center;

    h4 {
      margin-top: 30px;
      text-align: center;
    }

    .box {
      box-sizing: border-box;
      width: 210px;
      height: 210px;
      border: 2px solid red;
      padding: 10px;
      margin: 0 auto;
      resize: both;
      overflow: auto;
    }
    .size {
      color: #2a9966;
      font-weight: 600;
    }
  }
</style>

Top comments (2)

Collapse
 
kanstantsin_hramyka_24f05 profile image
Kanstantsin Hramyka

MutationObserver doesn't work
It works only if you change property width, but otherwise if width is set to auto or 100% and someone change the size of the screen then mo won't observe those changes and won't trigger

Collapse
 
gquinteros93 profile image
German Quinteros

Great article, it helped to resolve an issue that I have. Thanks for the contribution.
Just a comment, if you are going to use the ResizeObserver you should use the polyfill, resize-observer-polyfill (1) because not all modern browser support the ResizeObserver yet. After adding the polyfill, it should work in all the browser.
(1): npmjs.com/package/resize-observer-...