Detect Vue component' slot position and size changes - vue.js

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?

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

Related

Vue.js (v3): How to have a unique data-v-* hash for each component instance

I have the following code:
blah-foo.vue:
<template>
<div>Hello {{ name }}</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
props: {
name: {
type: String,
}
},
});
</script>
<style scoped>
div {
color: white;
}
</style>
and App.vue:
<template>
<blah-foo
name="Alice"
></blah-foo>
<blah-foo
name="Bob"
></blah-foo>
</template>
The result in my browser is the following:
<div data-v-73bdd40c>Hello Alice</div>
<div data-v-73bdd40c>Hello Bob</div>
Is there any way I could tell the vue loader to generate an unique data-v-* attribute for each of them ?
What is happening in production is that since the component blah-foo is called on many different lazy-loaded pages, I end up having many times
div[data-v-73bdd40c] {
color: white;
}
all overriding each other.
That isn't a problem in itself, it just does seem very disgraceful (code wise) after having loaded a few pages when inspecting elements.
That is not possible with vue-loader. And it shouldn't be done anyway.
The whole point of the data-v-xxx attribute is to identify the components in the DOM so vue can apply to them the correct scoped css.
If it ever applied uniq data-v attributes, it would not be able to apply the correct css.
I understand your problem is that, on production, you see in the css inspector several times the css code, right?
My guess is that it's related with sourcemaps, which may mess with the css inspector. But I can't help more without additional details on your project configurations.
Even if your component is used on several pages, its module will be fetched and loaded only once. You don't have the scoped css loaded several times, it's just an inspector problem.

Vue 3: Styling a Named Slot

So I've looked through stackoverflow and the documentation in Vue 3 but can't quite find what I'm looking for.
I'm trying to find a way to target a named slot, penetrate the scoped element within that slot, and override one of its children's styles. I assume I need the ::slotted selector and the :deep selector for this mission. Does anyone know how to do this?
Here is an example of the situation I am trying to solve for (LayoutContainer Component):
<section>
<slot name="text"></slot>
<slot></slot>
<slot name="sidebar"></slot>
</section>
the component that will go into the "text" slot (Eyebrow Component):
<section class="eyebrow-container">
<h1>{{title}}</h1>
<h6>{{description"}}</h6>
</section>
a completed view of the code on a page component:
<LayoutContainer>
<template #text>
<Eyebrow :title='test' :description="this is a description"></Eyebrow>
</template>
<PageBody></PageBody>
<template #sidebar>
<PageSideBar></PageSideBar>
</template>
</LayoutContainer>
Solutions I have tried in SCSS with no success:
::slotted(h6) { color: red }
::slotted(text){
:deep(.eyebrow-container) {
h6 { color: red; }
}
}
::slotted(text) {
:deep(h6) { color: red; }
}
and a few others I have forgotten at this point.
Does anyone have any ideas on how to get to the h6 tag inside of the Eyebrow Component from the Page Component's SCSS?
The slot content is owned by the parent passing them in.
So you don't need to use :slotted. You can simply use the :deep selector
<style scoped>
:deep(h6) {
color: red;
}
</style>
See it live
If you are wondering how to use :slotted then in your case it would be used in LayoutContainer component trying to style what the parent component passes in.
Scoped styling and styling child components from a parent don't work as you might think if you use multi-root node components.
So if you use mutli-root node component and :deep doesn't work, See my other answer

Component rendered multiple times

I am separating blocks with a v-if-statement like:
<div v-if="resp > 1023">
<PdfViewer />
</div>
<div v-else>
<PdfViewer />
</div>
The problem I got is when viewing it on smaller screen widths PdfViewer gets rendered one time. On widths over 1023px it's rendered two times even if the second mention of it is in the if-else block. Other html elements inside it ain't shown, but the component is.
Resp is the screen width on mount:
mounted() {
this.resp = window.innerWidth;
console.log(this.resp);
}
I solved it by assigning the value in the created lifecycle hook, so the value gets set before v-if evaluates.
created() {
this.resp = window.innerWidth;
console.log(this.resp);
}

Control Vue component from nested layout using Inertia JS

I'm using Inertia JS using Vue and nested Layouts.
My main layout looks something like this:
<template>
<div>
<app-bar title="App title" type="back|dismiss|sidebar">
<!-- Slot for icons in the top right corner -->
</app-bar>
<slot />
</div>
</template>
So, an AppBar component accepting a title, a link with a back icon, dismiss icon or sidebar icon, and a slot (optionally) to provide icon links relevant to the current page.
<script>
import Layout from '#/Pages/Messenger/Layout';
export default {
metaInfo: { title: 'Report new problem' },
layout: [Layout],
...
</script>
This is a Page that is nested in the Layout.
So my question is: what is the best/preferred way to control the props and slot of the AppBar from nested Pages?
A bit like as you would do using Blade templates in Laraval or as Vue Meta does for the document page title as seen in the example above.
Maybe this is not even the best approach, in that case also let me know :)
If you are trying to pass information from your child component to your parent component such as a title, you can use $emit.
Here is a article describing how: https://hvekriya.medium.com/pass-data-from-child-to-parent-in-vue-js-b1ff917f70cc
And another SO question: https://stackoverflow.com/a/52479745/4517964
The fast way I found to pass data to persistent layouts is by:
in the child
use this:
layout: (h, page) => { return h(Layout, () => page) }
instead of:
layout: Layout,
and in the parent layout you can access your child with this.$slots.default()[0]

v-show alternative for Svelte

The case is that I'm showing Loading component on fetch request. I use store to set $loading to true and inside conditions is the Loading component. The problem is that the Loading component seems to be taking some time to show. It feels/looks like the reason is re-rendering of Loading component. So, I was looking for v-show like thing in Svelte, which I cannot find in Docs. (Don't get angry if its there, just tell me.)
Can anyone help with this case?
Either wrap it in an {#if someCondition} block, or slap a hidden={!someCondition} attribute on an element.
If you want a block of HTML that does not re-render when the condition is changed, here is a simple solution:
<script>
// Show.svelte
export let show = true;
</script>
<div class:hide={!show}>
<slot />
</div>
<style>
.hide {
display: none !important;
}
</style>
And then use the Show component to create that block:
<script>
import Show from "Show.svelte";
let show = true;
</script>
<button on:click={() => { show = !show}}>
Click to Show/Hide Content
</button>
<Show {show}>
<div>Content</div>
</Show>
I have posted the Show component as an npm package https://www.npmjs.com/package/svelte-show