Keeping Vue slot reactivity when manually adding it to DOM via appendChild() - vue.js

I have a Vue component that has a default slot.
The contents of the slot needs to be added to different DOM element (element created by 3rd-party JS lib).
<slot></slot> is not used in the template but the slot VNode is still accessible via this.$slots.default.
Adding VNode to DOM
const slotContainer = document.createElement('div');
targetElement.appendChild(slotContainer);
const slot = this.$slots.default;
const v = new Vue({
render: () => slot
}).$mount(slotContainer);
The above code renders the slot in the DOM but reactivity is not working.
Getting reactivity working
I noticed if I add <slot></slot> to the component template then the slot becomes reactive. Although this works I would prefer to exclude the <slot></slot> in the template because then I have to use v-if to hide it. Vue obviously does something special to a slot when it is defined in the template that enables reactivity, does anyone have any ideas?

Related

VueJS 3: Access root HTML element in a slot

how do I reliably access the root HTML element in a slot? I tried slots.default()[0].el but its not consistent. I realized if the slot is a simple html, it is not null, which is great, but if it has some vue directives or components, it will be null. So how can I reliably get hold of the root HTML Element in a slot?
I found one possible solution: that is to have the slot content provider to explicitly set the element you want to reference to by providing a slot-prop method to invoke. And also since Vue 3 template supports multiple root elements, its not really reliable to assume that the slot will always have one root element.
Here is the example of the solution:
<template>
<slot :set-element="(el) => element = el"></slot>
</template>
<script setup lang="ts">
import { ref, watchEffect } from "vue";
const element = ref<Element | null>(null);
watchEffect(() => {
console.log(element.value);
});
</script>
In the usage of the component, inject the slot props and use the Function Refs syntax
<MyComponent v-slot="{ setElement }">
<div :ref="(el) => setElement(el)">...</div>
<div>...</div>
</MyComponent>
You could access a slot's root HTML element directly with the slot's vnode property and in your component script, you can using this.$refs.root.
Here is an example:
<template v-slot:[slotName]="{ vnode }">
<div ref="root">
<!-- your slot content goes here -->
</div>
</template>
mounted() {
const root = this.$refs.root as HTMLElement;
console.log(root);
}
EDIT
The official documentation for v-slot can be found here: https://vuejs.org/api/built-in-directives.html#v-slot
Instead, the official documentation for $refs can be found here: https://v3.vuejs.org/guide/composition-api-template-refs.html

vue 2 components, one for display, one for changing?

I would like to have two components, one for displaying a value, and one for changing it with a text field. I can't get this to work? Is there another way of doing this?
I get this error message:
"Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "forpris""
Vue.component('prislapp-forpris', {
props: ['forpris'],
template: '<div class="prislappForpris">[[ forpris ]],-</div>',
delimiters: ['[[',']]']
});
Vue.component('input-forpris', {
props: ['forpris'],
template: '<input type="text" v-model="forpris" />'
});
var app = new Vue({
el: '.previewPage',
data: {
lapp: {
id: 1,
forpris: 30595
}
}
});
It's all about v-model directly mutating the forpris prop. As the warning states, you should avoid to mutate a prop from a component.
 Rationale behind the warning
The reason is that allowing child component to modify props that belong to their parents make programs more error prone and difficult to reason about.
Instead, the idea behind Vue and other component oriented architectures and frameworks is that child components emit events to their parents, and then the parents change their own state, which in turn modify the child component via events from their children.
This ensures that the component passing down the props have full control of the state and may, or may not, allow the desired state changes that come via props.
How to fix your code to avoid the warning
v-model is syntax sugar over a :value and an #input on the input element. A really good read to understand how v-model innerly works is this article.
What you should do, is to drop v-model on the input for this:
template: '<input type="text" :value="forpris" #input="$emit('input', $event)" />'
This will set forpris as the value of the input (as v-model was already doing), but, instead of automatically modifying it, now the component will emit an input event when the user writes in the input.
So you now need to listen for this event in the parent and react accordingly. Now from your code is not absolutely clear who is rendering the two component, I guess the rendering comes from the .previewPage element in the Vue template, so the Vue instance is the parent component here.
You don't show the html of that template, but I guess it is something like the following:
<div class="previewPage">
<prislapp-forpris :forpriss="lapp.forpris" />
<input-forpris :forpriss="lapp.forpris" />
</div>
You now should listen to the #input event in the input-forpriss component:
<div class="previewPage">
<prislapp-forpris :forpriss="lapp.forpris" />
<input-forpris :forpriss="lapp.forpris" #input="handleInput" />
</div>
So, whenever we receive an #input event, we call the handleInput method. We also need to add such method to the Vue instance:
var app = new Vue({
el: '.previewPage',
data: {
lapp: {
id: 1,
forpris: 30595
}
},
methods: {
handleInput(value){
console.log(value); // now I'm not 100% sure if this
// is the value or a DOM event, better check
this.lapp.forpriss = value;
},
}
});

Fire event when changing route before DOM changes and outside the route itself?

I opened a similar topic a few days ago, where I was suggested to use beforeRouteLeave within the route component definition.
However, I'm creating a Vue component and I won't have control over how developers wants to define their route components. Therefore, I need a way to fire an event within my own component and don't rely on external route components.
When changing from one route to another, the beforeDestroy gets fired after the DOM structure changes.
I've tried using beforeUpdate and updated events on my component definition, but none seems to fire before the DOM changes.
import Vue from 'vue'
import MyComponent from '../myComponent/' // <-- Need to fire the event here
import router from './router'
Vue.use(MyComponent)
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
}).$mount('#app')
In the Vue instance lifecycle, the hook beforeDestroy gets called once the DOM has changed.
You are most likely looking for a beforeUnmount hook, which would be in-between mounted and beforeDestroy, but that is not available:
However, you could take advantage of JavaScript hooks. There is a JavaScript hook called leave, where you can access the DOM before it changes.
leave: function (el, done) {
// ...
done()
},
For this to work, you would need to wrap your element in a <transition> wrapper component.
ie.
<transition
:css="false"
#leave="leave"
>
<div>
<!-- ... -->
</div>
</transition>
...
methods: {
leave(el, done) {
// access to DOM element
done()
}
}

Why v-on doen't work on Vue component while using it in outer template?

The situation
Let's say I have a component BaseButton which is just a simple wrapper arround a <button> element:
Vue.component('base-button', {
template: `
<button>
<slot />
</button>
`
});
When using the button, I'll want to bind handler to a click event on this button:
<base-button #click="handler">
Click me
</base-button>
Pen with above code
The problem
The above solution doesn't work. The handler is not fired at all.
Can someone explain me why exactly? I'm guessing the handler is bound to the element, before it gets replaced with the component template, but it's just a guess - I can't find anything about it in vue.js docs. In addition to that docs
state:
A non-prop attribute is an attribute that is passed to a component,
but does not have a corresponding prop defined.
While explicitly defined props are preferred for passing information
to a child component, authors of component libraries can’t always
foresee the contexts in which their components might be used. That’s
why components can accept arbitrary attributes, which are added to the
component’s root element.
#click (v-on:click) seems to me to be a non-prop attribute and according to the above text should get inherited. But it's not.
Prop solution
I know I can declare a prop and pass the handler inside the component (code below). Then it works as expected.
The problem with this solution for me is that I don't have a fine grain control over how the handler is declared. What if in one usage of BaseButton I'd like to use on #click
some of the event modifiers Vue.js exposes (e.g. .stop, .prevent, .capture)? I'd have to use another prop (like capture) and use v-if, but it'd get the component template very messy. If I leave the handler in the template, where I use it, I can modify the event declaration as I want in a clean and flexible way.
Vue.component('base-button', {
prop: {
clickHandler: {
type: Function,
required: true
}
},
template: `
<button>
<slot />
</button>
`
});
<base-button :click-handler="handler">
Click me
</base-button>
The v-on directive behaves differently when used on a normal / native DOM element or on a Vue custom element component, as stated in the API docs:
When used on a normal element, it listens to native DOM events only. When used on a custom element component, it listens to custom events emitted on that child component.
In your case you apply it on your custom <base-button> element component, therefore it will listen only to custom event, i.e. ones that you explicitly $emit on this component instance.
Native "click" events bubbling phase from your underlying <button> will not trigger your #click listener…
…unless you use the .native modifier:
.native - listen for a native event on the root element of component.
Vue.component('base-button', {
template: `
<button>
<slot />
</button>
`
});
new Vue({
el: '#app',
data: {
handler(event) {
console.log('submit from where the component was used');
//console.log(event);
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.13/vue.min.js"></script>
<div id="app">
<base-button #click.native="handler">
Click
</base-button>
</div>

Vue.js best practice to remove component

My website overwrites a specific DOM instead of asking for a page load. Sometimes, inside the DOM there will be some Vue.js components (all are single page components), which will be overwritten. Most of them reside in an app created for the sole purpose of building the component.
Two questions:
do I need to destroy those components? If YES: how?
Is there any better solution to this approach?
There is a destroy command to erase components:
Completely destroy a vm. Clean up its connections with other existing vms, unbind all its directives, turn off all event listeners.
Triggers the beforeDestroy and destroyed hooks.
https://v2.vuejs.org/v2/api/#vm-destroy
But if you only want the remove/hide the components, which are inside of one or many instances (i.e. the ones that are mounted via .$mount() or el: "#app" ) you should try to remove the components via v-if or hide via v-show.
In the instance template this will look like:
<my-component v-if="showMyComponent"></my-component>
<my-component v-show="showMyComponent"></my-component>
The v-show will just set display:none while v-if will remove the component from the DOM in the same way as $destroy() does. https://v2.vuejs.org/v2/guide/conditional.html#v-if-vs-v-show
You must use a local data prop and use in in directives v-show and v-if
<div id="application">
<vm-test v-if="active"></vm-test>
<div #click="toggle()">toggle</div>
</div>
Vue.component('vm-test', {
template: '<div>test</div>'
});
var vm = new Vue ({
el: "#application",
data: function() {
return {
active:true
}
},
methods: {
toggle: function() {
this.active = !this.active;
}
}
})
https://jsfiddle.net/99yxdacy/2/