Home > front end >  Vue Template ref TypeError: Cannot read properties of null (reading 'scrollHeight')
Vue Template ref TypeError: Cannot read properties of null (reading 'scrollHeight')

Time:08-02

I am attempting to make a responsive sidebar in Vue and when I refactored my project to remove the state.js file my dropdown/collapse elements I give this TypeError: Cannot read properties of null (reading 'scrollHeight') that I am not sure how to solve.

I've read that

this error occurs when the scrollHeight property is accessed on an undefined value. To solve the error, make sure to only access the scrollHeight property on valid HTML elements.

But I am using a template ref here to grab the element, so I'm not sure why it wouldn't be accessable. Also, I had this working before I did my refactor and I just cannot figure out a way to work through this error.

Any ideas would be greatly appreciated!

Cheers!

Codesandbox Build

LangDropdown.vue (both dropdowns use the same update event where the error occurs)

<template>
  <div  @blur="dropdownIsOpen = false">
    <div  @click="dropdownIsOpen = !dropdownIsOpen">
      <div style="display: flex">
        <!-- <img style="width: 34px"  src="../assets/languages-icon.svg" alt="Change Language Icon" /> -->
        <div style="width: 34px" >
                <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="100%" height="100%" viewBox="0 0 34 34">
          <defs>
            <clipPath id="clip-path">
              <rect id="Rectangle_2210" data-name="Rectangle 2210" width="34" height="34" rx="5" transform="translate(-7 -5)" fill="#fff"/>
            </clipPath>
          </defs>
          <g id="languageIcon" transform="translate(7 5)">
            <g id="Group_2048" data-name="Group 2048" clip-path="url(#clip-path)">
              <path id="Path_3921" data-name="Path 3921" d="M7.267,10A17.388,17.388,0,0,0,9.89,4.15H1.142a1.042,1.042,0,1,1-.056-2.08c.449-.009.9,0,1.348,0h3.8c0-.339,0-.665,0-.992A1.01,1.01,0,0,1,6.625.231.987.987,0,0,1,7.678.091a.936.936,0,0,1,.61.845c.02.365,0,.732,0,1.132h.234q2.429,0,4.859,0a1.068,1.068,0,0,1,1.09.72,1.035,1.035,0,0,1-.98,1.36c-.247.007-.493,0-.74,0h-.736a19.4,19.4,0,0,1-3.413,7.545c.17.19.338.375.5.564a1.007,1.007,0,0,1,.2,1.094A.986.986,0,0,1,8.432,14a1.009,1.009,0,0,1-.861-.354c-.094-.1-.186-.206-.292-.323-.1.109-.2.215-.3.319a20.039,20.039,0,0,1-5.393,3.981,1.044,1.044,0,0,1-1.521-.531,1,1,0,0,1,.41-1.213c.467-.289.96-.536,1.432-.818a17.068,17.068,0,0,0,3.911-3.22.186.186,0,0,0,.012-.286,20.012,20.012,0,0,1-2.169-3.8,1.06,1.06,0,0,1,.25-1.277,1,1,0,0,1,1.32-.01,1.467,1.467,0,0,1,.356.5A19.431,19.431,0,0,0,7,9.613c.079.121.163.239.265.39" transform="translate(0 0)" fill="#fff"/>
              <path id="Subtraction_34" data-name="Subtraction 34" d="M1.045,12.441a1.017,1.017,0,0,1-.365-.068,1.148,1.148,0,0,1-.343-.208A1.016,1.016,0,0,1,0,11.327a.925.925,0,0,1,.079-.306c.166-.364.349-.728.527-1.079v0l.166-.33c1.338-2.674,2.8-5.6,4.472-8.934a1.335,1.335,0,0,1,.4-.508A.875.875,0,0,1,5.907.041,1.012,1.012,0,0,1,6.2,0a1.4,1.4,0,0,1,.2.015A.965.965,0,0,1,6.67.094a.923.923,0,0,1,.217.142A1.372,1.372,0,0,1,7.2.668C7.8,1.858,8.42,3.1,8.968,4.194l.017.034L9.607,5.47l.6,1.192.021.042c.673,1.344,1.37,2.735,2.052,4.11a2.577,2.577,0,0,1,.147.407h0c.019.063.039.129.06.192a1.237,1.237,0,0,1-.234.611,1.033,1.033,0,0,1-.223.218,1.015,1.015,0,0,1-.285.142,1.123,1.123,0,0,1-.339.053,1,1,0,0,1-.458-.109,1.02,1.02,0,0,1-.364-.317,1.9,1.9,0,0,1-.154-.261L10.3,11.5c-.333-.662-.678-1.347-1.01-2.025a.275.275,0,0,0-.1-.124.3.3,0,0,0-.155-.033h0l-2.729,0c-1.025,0-1.97,0-2.888,0h0a.288.288,0,0,0-.164.04.314.314,0,0,0-.1.131c-.394.8-.78,1.584-1.178,2.356a1.192,1.192,0,0,1-.177.256,1.06,1.06,0,0,1-.222.186A.991.991,0,0,1,1.045,12.441ZM6.223,3.349,5.267,5.262l-.081.162-.208.416-.7,1.407h3.9L7.457,5.818l-.215-.431L6.223,3.349Z" transform="translate(8.305 8.31)" fill="#fff"/>
            </g>
          </g>
        </svg>
        </div>
        <div style="flex: 1; display: flex; justify-content: space-between">
          <div v-if="!collapsed" >
            <div>
              {{ selected }}
            </div>
            <!-- <img  src="../assets/chevron-down.svg" alt="" /> -->
             <div >
              <svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" ><polyline points="6 9 12 15 18 9"></polyline></svg>
            </div>
          </div>
        </div>
      </div>
        <ul  ref="bodyEl" :style="bodyStyle">
          <li v-for="(option, i) of options" :key="i" @click="selectOption(option)">
            {{ option }}
          </li>
        </ul>
    </div>
  </div>
</template>

<script>
import { defineComponent, ref, watchEffect, onMounted, onBeforeUnmount } from "vue"

export default defineComponent({
  props: {
      options: { type: Array, required: true },
      default: { type: String, required: true },
      collapsed: { type: Boolean, default: false },
    },
  emits: 'input',
  setup(props, context){
    const selected = ref(props.default ? props.default : props.options.length > 0 ? props.options[0] : null)
    const dropdownIsOpen = ref(true)
    if (props.collapsed) dropdownIsOpen.value = false
    const bodyEl = ref(null)
    const bodyStyle = ref({})
    const update = () => bodyStyle.value = {
      maxHeight: `${dropdownIsOpen.value ? bodyEl.value.scrollHeight : 0}px`
    }
    watchEffect(update)
    onMounted(()=> window.addEventListener('resize', update))
    onBeforeUnmount(()=> window.removeEventListener('resize', update))
    function selectOption(_option) {
      selected.value = _option
      dropdownIsOpen.value = false
      context.emit("input", _option)
    }
    return {
      selectOption, 
      selected,
      dropdownIsOpen,
      bodyEl,
      bodyStyle
    }
  }
});

</script>

<style lang="sass">
._custom-select
  position: relative
  width: 100%
  text-align: left
  outline: none
  font-size: 16px
  border-radius: 6px
  margin-bottom: 2em
  &:hover
    ._icon,
    ._selected-option
      background-color: var(--sidebar-item-hover)
._custom-select-inner
  display: flex
  flex-direction: column
  justify-content: space-between
._selected-option
  flex: 1
  display: flex
  justify-content: space-between
  margin-left: 1em
  text-align: left
  border-radius: 6px
  padding: 8px 18px
  cursor: pointer
  user-select: none
  line-height: 26px
._icon
  width: 24px
  border-radius: 6px
  cursor: pointer
._options
  overflow: hidden
  max-height: 0
  padding: 0 1rem
  transition: max-height .3s cubic-bezier(.4,0,.2,1)
  margin-left: auto
  list-style-type: none
  > *
    border-radius: 6px
    text-align: left
    cursor: pointer
    user-select: none
    padding: 6px
    width: 100%
  > *:hover
    background-color: var( --sidebar-item-hover)

._custom-select ._options div:hover
  background-color: var( --sidebar-item-hover)
</style>

CodePudding user response:

watchEffect call the update function before the DOM has been mounted, so bodyEl will still be null in this case.

You can use the watch method instead, and you can call update method on onMounted lifecycle.

I update the code, please check.

https://codesandbox.io/s/custom-sidemenu-forked-pukubl

  • Related