VueJS 3: Access root HTML element in a slot - vue.js

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

Related

How to change data in default layout from nested components in Nuxt.js

I have layout default.vue:
<template>
<div>
<Header></Header>
<div class="body-wrapper">
<Nuxt :numberThird="numberThird" />
</div>
<Footer></Footer>
</div>
</template>
<script>
export default {
data() {
return {
numberThird: 3
};
},
};
</script>
Here I am trying to pass prop numberThird.
I want to be able to change this value in the future through components that are deeply nested.
But there is a problem: my pages don't accept this prop (numberThird), they treat it as $parent.$attr.
The question is: can I somehow change this value through deeply nested child components?
As stated here: https://stackoverflow.com/a/67817642/8816585
Props and listeners are not the most friendly on either <nuxt> nor <nuxt-link>.
Kinda confirmed here by Alexander too: https://github.com/nuxt/nuxt.js/issues/8669#issuecomment-764006062
You better off using Vuex in this case, especially if you are aiming to something deep. Props drilling is not recommended most of the time.

_vm.$refs.menu.open is not a function

I'm using vue-context to modify the default context menu, which I call from a component but when I try to interact with it I get an error, here's my code
<!-- Main -->
<p #contextmenu.prevent="$refs.menu.open">test</p>
<Context reference="menu" />
<script>
import Context from './map/context.vue'
export default { components: { Context } }
</script>
<!-- Component -->
<vue-context :ref="reference" :close-on-click="true" :close-on-scroll="true">
<li>
<a>
Do something
</a>
</li>
<li>
<a>
Do something else
</a>
</li>
</vue-context>
<script>
import VueContext from 'vue-context'
import 'vue-context/dist/css/vue-context.css'
export default {
props: ['reference'],
components: {
VueContext
}
}
</script>
When I right click on the page, I get the error _vm.$refs.menu.open is not a function
Because you wrapped VueContext inside a separate vue instance, the outer Vue instance (the one containing the #contextmenu call) does not have a $refs.menu. You can access the child's $refs by setting a ref on the child itself:
<p #contextmenu.prevent="$refs.wrapper.$refs.menu.open">test</p>
<Context ref="wrapper" reference="menu" />
See it working here.
I'd also argue you shouldn't pass the string 'menu' from parent but specify it directly inside <Context>'s template.
Dynamic props only make sense when you have some benefit from them changing value, which is clearly not the case here. You need that child ref to always be 'menu' so you can access its methods:
<p #contextmenu.prevent="$refs.wrapper.$refs.menu.open">test</p>
<Context ref="wrapper" />
Context.vue:
<template>
<vue-context ref="menu" :close-on-click="true" :close-on-scroll="true">
<li><a>Do something</a></li>
<li><a>Do something else</a></li>
</vue-context>
</template>
<script>
import VueContext from "vue-context";
import "vue-context/dist/css/vue-context.css";
export default { components: { VueContext } };
</script>
To summarize: $refs is a unified mechanism allowing you to access template elements, whether they're DOM elements or Vue instances.
Each component only contains its own $refs. To access the $refs of one of its children, you have to give the child a ref in parent scope and use $refs on that particular reference.
This actually makes a lot of sense in a scenario where you want multiple context menus with different contents for different items in your parent component (although you're probably better off simply passing down the menu items and their actions dynamically to a single context menu instance).

VueJS - Generating html string of a component in computed property

I'm looking for a suggestion regarding a cleaner approach to generate a component data as html string and to pass it raw through the props of the component.
component-a.js
import componentB from './component-b'
computed: {
tooltipHTML() {
render "<componentB :name='user1'/>
}
}
I would prefer something similar to the above idea.
Generating HTML in a computed property and passing it as props to another component to be rendered in that component will not work.
What you are looking for is Slots
Since the complete code is not provided I guess you wanted to render <componentB :name='user1'/> inside another component( a tooltip component)
You would be doing it as follows using slots:
<tooltip-comp>
<componentB :name='user1'/>
</tooltip-comp>
In your tooltip component
//tooltip component
<template>
<div class="my-tooltip">
<p>my tooltip</p>
<slot></slot>
</div>
</template>

Custom Vue directive to omit tag but render tag's contents?

I'd like to create a custom Vue directive to omit the tag but render the tag's contents when the directive is true.
So for example, if the data for my vue instance is defined as
data:{
omitIt: true
}
And if the markup looks like this:
<div v-omit="omitIt" class="someClass">
Hello world!
</div>
When omitIt is set to false as it is above, I'd like the following rendered into the dom:
<div class="someClass">
Hello world!
</div>
But when omitIt is true I'd like only the following rendered into the dom:
Hello world!
I initially tried to solve this by doing the following (admittedly not a custom vue directive):
<template v-if="!omitIt">
<div class="someClass">
</template>
Hello world!
<template v-if="!omitIt">
</div>
</template>
The above isn't pretty but I thought perhaps it would work. But alas what gets rendered into the dom when omitIt is false is:
<div class="someClass"></div>
Hello world!
Any suggestions on how to achieve the results I'm looking for?
I thought #Nit's answer was a great and simple one and upvoted it, but it does have one flaw: a slot may not be a root element so the component will fail when the wrapper needs to be omitted. This is because slots can contain more than one element and if the slot does contain more than one, there could end up being more than one root element, which is not allowed.
I have a partial solution that renders just the first element in the slot if the component should not wrap.
Vue.component("wrapper", {
props:{
nowrap: {type: Boolean, default: false}
},
render(h){
// This will *only* render the *first* element contained in
// the default slot if `nowrap` is set. This is because a component
// *must* have a single root element
if (this.nowrap) return this.$slots.default[0]
// Otherwise, wrap the contents in a DIV and render the contents
return h('div', this.$slots.default)
}
})
Here is an example of it working.
console.clear()
Vue.component("wrapper", {
props:{
nowrap: {type: Boolean, default: false}
},
render(h){
// Log a warning if content is being omitted
const omissionMessage = "Wrapper component contains more than one root node with nowrap specified. Only the first node will be rendered."
if (this.$slots.default.length > 1 && this.nowrap)
console.warn(omissionMessage)
// This will *only* render the *first* element contained in
// the default slot if `nowrap` is set. This is because a component
// *must* have a single root element
if (this.nowrap) return this.$slots.default[0]
// Otherwise, wrap the contents in a DIV and render the contents
return h('div', this.$slots.default)
}
})
new Vue({
el: "#app"
})
.someClass{
color: blue
}
<script src="https://unpkg.com/vue#2.4.2"></script>
<div id="app">
<wrapper class="someClass">Hello World</wrapper>
<wrapper nowrap>No wrap, single root</wrapper> <br>
<wrapper nowrap>
No wrap, two roots. Paragraph is ommitted.
<p>Some other content</p>
</wrapper>
</div>
A couple notes: The component will always wrap unless you add nowrap as an attribute. Also, notice the class is added to the wrapped container without specifying it as a prop. This is because Vue automatically renders attributes that aren't specified as props on the root element of a component, unless you tell it not to.
This answer is wrong, slots cannot be used in this manner. Please see Bert's answer instead.
The easiest solution would be to create a wrapper component with slots for this purpose, passing the omitting argument as a prop.
The content distribution part becomes rather straightforward.
In the wrapper component template:
<slot v-if="omitIt"></slot>
<div v-else>
<slot></slot>
</div>
Wherever you want to use the wrapper:
<wrapper v-bind:omitIt="omitIt">
// Content
</wrapper>

Vue Multiselect does not update {{ value }} via v-model

I am using this example for Vue Multiselect "^2.0.0-beta.14" in Laravel 5.3. https://github.com/monterail/vue-multiselect/tree/2.0#install--basic-usage
The plugin renders correctly but I cannot get the selection via v-model. I am expecting #{{ selected }} to update with the current selection.
app.js
Vue.component('dropdown', require('./components/Multiselect.vue'));
VUE JS
<template>
<div>
<multiselect
v-model="value"
:options="options">
</multiselect>
</div>
</template>
<script>
import Multiselect from 'vue-multiselect'
export default {
components: { Multiselect },
data () {
return {
value: null,
options: ['list', 'of', 'options']
}
}
}
</script>
<style src="vue-multiselect/dist/vue-multiselect.min.css"></style>
HTML
<div id="app">
<h3>Dropdown</h3>
<div>
<label class="typo__label">Single select</label>
<dropdown></dropdown>
<pre class="language-json"><code>#{{ value }}</code></pre>
</div>
</div>
NB
The official example uses selected instead of value but this does not work either. According to the docs selection is replaced by value as of V2.
If you are using TypeScript Interfaces with Vue.js 2.0, avoid using a optional properties to store the value from child components. i.e. if your property is
value:? IMyCustomInterface instead use value: MyCustomObject|null and set the object to null in the constructor.
If the property is optional, it will compile fine, but child components won't update it properly.
The reason value is not showing up in root is because the data is isolated to the dropdown component. To get your data from a component to show up in the Root you need to use props.
See this question for a detailed explanation
How to get data from a component in VueJS