Component rendered multiple times - vue.js

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);
}

Related

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?
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

Vue 3 replacing the HTML tags where v-html is called with the provided HTML

This is about a Vue 3 app with Vite, not webpack.
For now, as you can see from this issue on vite's issue page, vite doesn't have a convenient way of inlining SVGs without using external plugins. Vite does however, support importing files as raw text strings. As such, I had an idea to use this feature and to inline SVG's by passing the raw SVG strings into an element's v-html.
It actually works great, the SVG shows up on the page as expected and I can do the usual CSS transforms (the whole purpose of inlining them like this), but it's not perfect. As it currently stands, the element that receives the v-html directive simply places the provided HTML nested as a child. For example, if I do <span v-html="svgRaw" />, the final HTML comes out something like this
<span>
<svg>
<!-- SVG attributes go here -->
</svg>
</span>
Is there any way for me to essentially replace the parent element on which v-html is declared with the top-level element being passed to it? In the above example, it would mean the <span> just becomes an <svg>
EDIT:
Thanks to tony19 for mentioning custom directives.
My final result looks like this:
// main.ts
import { createApp } from "vue";
import App from "./App.vue";
const app = createApp(App);
app.directive("inline", (element) => {
element.replaceWith(...element.children);
});
app.mount("#app");
Then, in the component I simply use the directive, <svg v-html="svgRaw" v-inline /> and it works great!
You could create a custom directive that replaces the wrapper element with its contents:
Use app.directive() to create a global directive, named v-inline-svg:
// main.js
import { createApp } from 'vue'
import App from './App.vue'
createApp(App)
.directive('inline-svg', el => {
if (!el) {
return
}
// copy attributes to first child
const content = el.tagName === 'TEMPLATE' ? el.content : el
if (content.children.length === 1) {
;[...el.attributes].forEach((attr) => content.firstChild.setAttribute(attr.name, attr.value))
}
// replace element with content
if (el.tagName === 'TEMPLATE') {
el.replaceWith(el.content)
} else {
el.replaceWith(...el.children)
}
})
.mount('#app')
In your component, include v-inline-svg on the v-html wrapper element (also works on <template> in Vue 3):
<svg v-html="svgRaw" v-inline-svg />
<!-- OR -->
<template v-html="svgRaw" v-inline-svg />
demo
I found that using the method above works but is only good for a single rendering of the svg... The element starts throwing errors if I try to change the svg contents dynamically, not sure why but assuming that the dom replacement has something to do with it.
I modified the code slightly for my use case.
app.directive('inline-svg', {
updated: (element) => {
if (element.children.length === 0) {
return
}
const svg = element.children[0]
if(svg.tagName.toLowerCase() !== 'svg') {
return
}
for (let i = 0; i < svg.attributes.length; i++) {
const attr = svg.attributes.item(i)
element.setAttribute(attr.nodeName, attr.nodeValue)
}
svg.replaceWith(...svg.children)
}
})
In my component I have.
<svg v-if="linkType !== null" v-html="linkType" v-inline-svg></svg>
The directive now copies the svg attributes across from the child to the parent and then replaces the child with it's children.
Coming from Vue2. I think this still works:
Instead of span you can use the special Vue tag template:
<template v-html="svgRaw" />
This will not render <template /> as a tag itself, but render the elements given in v-html without a parent element.

How can I listen to the scroll event in a sidebar [Bootstrap Vue]

I am using the Sidebar component from BootstrapVue to render a navigation for my application.
As an example, I have 30 menu items in my navigation, so the content within the sidebar scrolls vertically. I would like to bind to that scroll event to dynamiclly add/remove some classes.
I have tried to create a custom scroll directive:
Vue.directive('scroll', {
inserted: function(el, binding) {
console.log('el', el);
let f = function(evt) {
if (binding.value(evt, el)) {
window.removeEventListener('scroll', f);
}
};
window.addEventListener('scroll', f);
}
});
...then register that to the component within my vue file:
<b-sidebar
v-scroll="handleScroll"
title="Menu"
shadow="lg"
backdrop
#change="$emit('sidebar-change')"
...
handleScroll() {
console.log('handleScroll');
},
The directive is being picked up properly, but my handleScroll method is firing when the main body is scrolling, not the sidebar.
In my directive, I am logging to see what element it thinks it's working with:
<div tabindex="-1" class="b-sidebar-outer">...</div>
Since Bootstrap is dynamiclly creating the markup for the overlay, that's the parent element -- looking closer, I believe I need to attach my directive to this:
<div class="b-sidebar-body">...</div>
That is the <div> that looks to be scrolling. However, since it is generated at runtime, I don't know how to hook into that.
I have also tried using #native.scroll="myMethod" on the component...no luck there either.
How can I listen for the scroll event within my sidebar component? Thank you for any suggestions!
Your scroll listener fires on the main window because the directive attached the event listener to window, and not the element.
To listen to scroll events on the contents of b-sidebar, the listener should be on an element inside the default slot of b-sidebar (not the b-sidebar itself).
Put a wrapper div inside b-sidebar's default slot, and style it to enable scrolling:
<template>
<b-sidebar>
<div class="wrapper">
<!-- your actual contents here -->
</div>
</b-sidebar>
</template>
<style>
.wrapper {
overflow: auto;
height: 100%;
}
</style>
Add the custom v-scroll directive on the wrapper div:
<div class="wrapper" v-scroll="handleScroll">
Update the custom directive to add the binding value as the event listener on the given element's scroll event:
Vue.directive('scroll', (el, binding) => {
let f = (evt) => {
if (binding.value(evt, el)) {
el.removeEventListener('scroll', f)
}
}
el.addEventListener('scroll', f)
})
demo
You need to check whether the event's target is the sidebar and only execute your function if it is.

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

How to disable transition-group only on page load?

I have a transition-group that renders a div, and within that is a component with a v-for attribute that renders several items. I then have a button that adds a new item to the beginning of the array. That transition works perfectly.
The only thing I don't like, is that the entire list loads with the transition on page load, and I'd like to disable it only on page load. I've searched Stack and Google but couldn't find a way. Is there a way to do this, so that transitions still works on button click, but is disabled for page load?
<transition-group
name="item-list"
tag="div">
<item-row
v-for="item in items"
:key="item.id"
:item="item" />
</transition-group>
.item-list-enter-active,
.item-list-leave-active,
.item-list-move {
transition : 250ms cubic-bezier(0.59, 0.12, 0.34, 0.95);
transition-property: opacity, transform;
}
.item-list-enter {
opacity : 0;
transform: translateX(50px) scaleY(0.5);
}
.item-list-enter-to {
opacity : 1;
transform: translateX(0) scaleY(1);
}
.item-list-leave-active {
position: absolute;
}
.item-list-leave-to {
opacity : 0;
transform : scaleY(0);
transform-origin: center top;
}
I wish I could've found a more "Vue-y" way of handling this, however I ended up going this route. Essentially I added a class to the body and removed all transitions. Then on the created lifecycle of my component, I remove that class. This removes the transition on page load, but still keeps the transition on click of the button like I want.
You can dynamically change the name value of the transition-group. Maybe on page load have a value different from the value that has the correct class name that the CSS targets. Then in the mounted lifecycle hook you can change it back to the correct class name.
You need to bind the duration for transition-group
template:
<transition-group
:duration="duration"
name="item-list"
tag="div">
<item-row
v-for="item in items"
:key="item.id"
:item="item" />
</transition-group>
script:
data() {
return {
duration: 0,
items: [
{id: 1},
{id: 2}
]
}
},
methods: {
add() {
if(this.duration===0) this.duration = 250
this.items.push({id: 'xxx'})
}
}
In case anyone comes across this like I did.
I ended up achieving this by having a transitionsOn flag added to the data (didn't seem to matter what it was initialised to), and a computed name for the transition, i.e.
<transition-group :name="transitionName">
in computed, I then had, for a transition called 'flash'
computed: {
transitionName() {
return this.transitionsOn ? 'flash' : 'disabled';
},
},
I would then set this.transitionsOn = true when I wanted it to fire.
Took a lot of fiddling about to figure this out but it seems to work OK