vue.jsvuejs3vue-slot

Detect Vue component' slot position and size changes


I have a component which receive named slots:

<Child>
    <template #content>
        <p>tooltip content</p>
    </template>
    <template #activator>
        <button>hover me</button>
    </template>
</Child>

I wanna know the position and size of the activator slot no matter where I'm gonna use it and what I'm gonna do with it. If I do like this:

<template>
    <div style="margin-top: 13px;" :style="styleObject">
        <Child>
            <template #content>
                <p>tooltip content</p>
            </template>
            <template #activator>
                <button>hover me</button>
            </template>
        </Child>
    </div>
</template>

<script setup lang="ts">
import {reactive, ref} from "vue";
import Child from "./components/Child.vue";

const styleObject = reactive({
    marginLeft: '16px'
})

setTimeout(() => {
    styleObject.marginLeft = '30px'
}, 2000)
</script>

Inside <Child> component I want to detect position changed after 2 seconds. I was able to get initial position and size with this:

const slots = useSlots()

const activatorStyles = reactive({
    top: 0,
    left: 0,
    height: 0,
    width: 0
})

const getActivatorStyles = () => {
    if (slots?.activator) {
        activatorStyles.top = slots.activator()[0]?.el?.offsetTop
        activatorStyles.left = slots.activator()[0]?.el?.offsetLeft
        activatorStyles.height = slots.activator()[0]?.el?.offsetHeight
        activatorStyles.width = slots.activator()[0]?.el?.offsetWidth
        console.log('activatorStyles', activatorStyles)
    }
}

onUpdated(getActivatorStyles)
onMounted(getActivatorStyles)

but I'm not sure how to detect that in any of the parent components something changed which resulted in this <Child> component position or size change. For example this timeout from snippet above.

I was trying onUpdate but this seems to be working only on DOM Nodes changes (not styles). I was also trying to make this object as a computed property but no luck. Here is vue playground where initial size and position is correctly gathered but after timeout execution it doesn't detect that left changed and it stays 24.

My question is how can I can keep my activatorStyles object up-to-date no matter what will happen in parent components?


EDIT: I tried MutationObserver on parent but problem is that I don't know from where the changes of position / size might come. If I observer parentElement as suggested it works very well if the styles binding are on direct parent. If you I have more <div> nested and style binding is happening somewhere deeper the mutationObserver is not triggering anymore. To make it work I would need to pass document.body to observer which is not best performance, isn't it? playground example?


Solution

  • A component will only update if its props/data/computed changed. What happens there is that the update happens on the parent.

    If you simply just want to access the parent from child, just use the $parent property and check/watch the property that holds the style.

    Docs: https://vuejs.org/api/component-instance.html#parent

    NOTE:

    $parent is a reference to whatever Vue component rendered your component.

    <A>
      <B />
    </A>
    

    In this example, B's $parent would be A.

    If you're going to teleport/move the element manually to another element, then what you want is

    $el.parentElement
    

    Example: https://www.w3schools.com/jsref/prop_node_parentelement.asp


    Another option would be to check DOM changes via MutationObserver or using library like https://popper.js.org/

    Docs: https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver

    Example: https://stackoverflow.com/a/20559787/10975709


    My opinionated answer though would be to suggest encapsulating the idea of styling the parent as part of your component that way your component can safely check that prop always.

    Your example looks similar to some of Vuetify components like the Dialog for example (because of the activator slot).

    Vuetify encapsulates the responsibilities everything on its own and doesn't rely on the code of whoever uses it.

    Docs: https://vuetifyjs.com/en/components/dialogs/#usage