[Vuejs]-Input clientWidth and scrollWidth are always equal

3👍

1.) watch() variable, use nextTick()

The scrollWidth changes as a result of DOM manipulation. If you console.log the current scrollWidth of the input field, it will indeed change correctly in your code. However, the issue here is that these DOM data are not automatically updated in the Vue reactivity system.

To retrieve the updated values, you can use nextTick(). However, it’s important to await the value of nextTick() to ensure that the DOM manipulation is completed. Therefore, you should call it within an async function. Using computed() is typically not suitable for this purpose. Instead, it would be better to check if the scrollWidth has changed only when the value of interest changes.

To achieve this, you can use the watch() function. You specify which variable’s changes to observe and which function to execute when a change is detected. In our case, we will monitor the currentValue variable and execute an async function. So when the currentValue changes, the async function is executed, waits for the update using nextTick(), and then checks the difference between the new clientWidth and scrollWidth. The true/false value is then stored in a separate variable that can be referenced in your code.

<script setup lang="ts">
  import { ref, watch, nextTick } from "vue";

  const currentValue = ref("");
  const textFieldComponent = ref<VTextField>();

  // store boolean for disabled checking
  const isTextFieldCuttingOffContent = ref(false);

  // checks if the current scrollWidth of the input field is wider than the clientWidth
  const checkTextOverflow = async () => {
    await nextTick();

    const inputWidth = textFieldComponent.value.clientWidth;
    const textWidth = textFieldComponent.value.scrollWidth;

    isTextFieldCuttingOffContent.value = textWidth > inputWidth;
  };

  // call checkTextOverflow() function when currentValue changed
  watch(currentValue, checkTextOverflow);
</script>

<template>
  <v-container style="width: 300px">
    <v-tooltip :text="currentValue" :disabled="!isTextFieldCuttingOffContent">
      <template v-slot:activator="{ props }">
        <div v-bind="props">
          <v-text-field
            id="here"
            ref="textFieldComponent"
            label="label goes here"
            v-model="currentValue"
          />
        </div>
      </template>
    </v-tooltip>
  </v-container>
</template>
Example
const { createApp, ref, watch, nextTick } = Vue

const app = createApp({
  setup() {
    const currentValue = ref('')
    const textFieldComponent = ref(null)

    // store boolean for disabled checking
    const isTextFieldCuttingOffContent = ref(false)

    // checks if the current scrollWidth of the input field is wider than the clientWidth
    const checkTextOverflow = async () => {
      await nextTick() // check DOM updates

      const inputWidth = textFieldComponent.value.clientWidth
      const textWidth = textFieldComponent.value.scrollWidth

      isTextFieldCuttingOffContent.value = textWidth > inputWidth
    }

    // call checkTextOverflow() function when currentValue changed
    watch(currentValue, checkTextOverflow)
    
    return { currentValue, textFieldComponent, isTextFieldCuttingOffContent }
  },
}).mount('#app')
.container {
  width: 100px;
  resize: both;
  overflow: hidden;
}
  
input {
  width: 100%;
  height: 100%;
  box-sizing: border-box;
}
<!-- WithNextTick.vue -->

<script src="https://unpkg.com/vue@3.3.4/dist/vue.global.prod.js"></script>

<div id="app">
  <div class="container">
    <input ref="textFieldComponent" v-model="currentValue">
  </div>
  <p v-if="isTextFieldCuttingOffContent">Warning: value overflow detected</p>
</div>

Upgrade (2023-06-19 #1) (inspired by @tao’s comment)

2.) watch() variable, use Observer

If you need to not only check the scrollWidth vs clientWidth during typing but also during any manipulation of the width, such as resizing or other changes, then feel free to implement the Observer solution mentioned by @tao! However, it is important to note that my solution is still essential in this case because it primarily focuses on observing the scrollWidth changes during typing, which the Observer cannot directly track as it primarily monitors DOM manipulations and/or element resizing, which are not triggered when typing into the input field or modifying the variable with JavaScript.

To understand how Observers work, please read @tao’s informative answer.

import { ref, onMounted } from 'vue'

let resizeObserver = null
let mutationObserver = null

onMounted(() => {
  // declare ResizeObserver to textFieldComponent
  resizeObserver = new ResizeObserver(checkTextOverflow)
  resizeObserver.observe(textFieldComponent.value)

  // declare MutationObserver to textFieldComponent
  mutationObserver = new MutationObserver(checkTextOverflow)
  mutationObserver.observe(textFieldComponent.value, {
    childList: true,
    subtree: true,
    characterData: true,
    attributes: true
  })
})
Example
const { createApp, ref, watch, onMounted } = Vue

const app = createApp({
  setup() {
    let resizeObserver = null
    let mutationObserver = null

    const currentValue = ref('')
    const textFieldComponent = ref(null)

    // store boolean for disabled checking
    const isTextFieldCuttingOffContent = ref(false)

    // checks if the current scrollWidth of the input field is wider than the clientWidth
    const checkTextOverflow = () => { 
      const inputWidth = textFieldComponent.value?.clientWidth || 0
      const textWidth = textFieldComponent.value?.scrollWidth || 0

      isTextFieldCuttingOffContent.value = textWidth > inputWidth
    }

    // call checkTextOverflow() function when currentValue changed
    watch(currentValue, checkTextOverflow)

    // run function after dom loaded
    onMounted(() => {
      // declare ResizeObserver to textFieldComponent
      resizeObserver = new ResizeObserver(checkTextOverflow)
      resizeObserver.observe(textFieldComponent.value)

      // declare MutationObserver to textFieldComponent
      mutationObserver = new MutationObserver(checkTextOverflow)
      mutationObserver.observe(textFieldComponent.value, {
        childList: true,
        subtree: true,
        characterData: true,
        attributes: true
      })
    })
    
    return { currentValue, textFieldComponent, isTextFieldCuttingOffContent }
  },
}).mount('#app')
.container {
  width: 100px;
  resize: both;
  overflow: hidden;
}
  
input {
  width: 100%;
  height: 100%;
  box-sizing: border-box;
}
<!-- WithObserver.vue -->

<script src="https://unpkg.com/vue@3.3.4/dist/vue.global.prod.js"></script>

<div id="app">
  <div class="container">
    <input ref="textFieldComponent" v-model="currentValue">
  </div>
  <p v-if="isTextFieldCuttingOffContent">Warning: value overflow detected</p>
</div>

Summary

Monitoring the variable is necessary and unavoidable in any case. Additionally, with the feature mentioned by @tao, you can also consider unexpected events such as resizing the browser window to ensure that the tooltip display works as expected. It depends on the specific circumstances and requirements whether you need anything beyond monitoring the variable.

I have prepared a sample code snippet for demonstration purposes so that you can compare the codes and the results.

Try solutions on Vue SFC Playground

2👍

@rozsazoltan’s answer 1 (wrapping the difference between .scrollWidth and .clientWidth into a nextTick(), which waits for DOM updates) is a quick fix which will cover most of the cases. Most likely, it is what you need in the current case.

But, since you asked for a canonical answer (which means you want something which works in all possible cases), that solution is not enough, because it assumes .clientWidth and .scrollWidth of the element only change when the text content change, which is not true.

They can also change on:

  • changes in size to any of the element’s ancestors (including window object) or to its proceeding siblings
  • changes to CSS property values (own or inherited) resulting in different wrapping of the text (e.g: font-size, font-family, white-space, word-wrap, etc…)

In order to catch (and therefore react to) all those changes, you need:

  • two observers on the element (a ResizeObserver and a MutationObserver)
  • a function reading the element’s current .scrollWidth and .clientWidth values, placing them somewhere in component state. With this setup the difference between the two will be reactive without the need for nextTick(). And it will cover all cases.

Proof of concept (pseudo-code):

<!-- ... -->
  <v-tooltip :disabled="!state.hasTooltip">
   <!-- ... -->
     <v-text-field
       ref="textFieldComponent"
       v-model="currentValue"
     />
import { useResizeObserver, useMutationObserver } from '@vueuse/core'

const currentValue = ref('')

const state = reactive({
  scrollWidth: 0,
  clientWidth: 0,
  hasTooltip: computed(
    () => state.clientWidth > state.scrollWidth
  )
})

const updateState = () => {
  state.scrollWidth = +textFieldComponent.value.scrollWidth || 0
  state.clientWidth = +textFieldComponent.value.clientWidth || 0
}

watch(currentValue, updateState, { immediate: true })

onMounted(() => {
  useResizeObserver(textFieldComponent.value, updateState)
  useMutationObserver(textFieldComponent.value, updateState)
})

If you need an actual implementation example, let me know and I’ll create one starting from your sandbox.


1 – at the time I answered rozsazolnan’s answer didn’t yet contain the resize and mutation observers, which is the main reason I added mine (and also to showcase a slightly different syntax). Now Zoltan’s answer is complete.

👤tao

-1👍

Checking the clientWidth and scrollWidth properties could be your issue. Timing may be the culprit here – specifically, the moment when you attempt to access them. The final dimensions of the element may not have been established yet or the rendering process may not be complete.

Incorporating a reactive watcher can guarantee precise measurements by constantly checking the dimensions when currentValue undergoes a change. To implement this technique, here is a revised version of your code.

<script setup lang="ts">
  import { ref, computed, watch } from "vue";

  const currentValue = ref("");
  const textFieldComponent = ref<VTextField>();
  const isTextFieldCuttingOffContent = ref(false);

  const checkTextOverflow = () => {
    if (!textFieldComponent.value) {
      return;
    }

    if (!currentValue.value) {
      isTextFieldCuttingOffContent.value = false;
      return;
    }

    const el = textFieldComponent.value.$el;
    isTextFieldCuttingOffContent.value = el.clientWidth < el.scrollWidth;
  };

  watch(currentValue, checkTextOverflow);
</script>

<template>
  <v-container style="width: 300px">
    <v-tooltip :text="currentValue" :disabled="!isTextFieldCuttingOffContent">
      <template v-slot:activator="{ props }">
        <div v-bind="props">
          <v-text-field
            ref="textFieldComponent"
            label="label goes here"
            v-model="currentValue"
          />
        </div>
      </template>
    </v-tooltip>
  </v-container>
</template>

Whenever the value of currentValue changes, the checkTextOverflow function is called to guarantee that the measurements of clientWidth and scrollWidth are taken after the DOM updates.

Using a reactive watcher to modify the isTextFieldCuttingOffContent variable allows for precise identification of when the text field is truncating content. 🙂
Hope this helps!

Leave a comment