VUE - load component only if slot exist - vue.js

I would like to load a component into a slot only if the slot exists (because I don't want to waste resources creating and mounting the component).
So I did it like that:
Test.vue
<slot :load="true" name="slotNo1"/
Wrapper.vue
<template slot="slotNo1" slot-scope="{ load }"> <Test v-if="load" /> </template>
But I wonder if there is an easier way without having to create the "load" variable.

Refer this
Vue.component('Custom', {
template: `
<div>
<span>always displayed</span>
<strong v-if="hasSlotData">
displayed only when slot passed: <slot></slot>
</strong>
</div>
`,
computed: {
hasSlotData() {
return this.$slots.default;
}
}
});
new Vue({
el: '#root'
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="root">
<custom></custom>
<custom>content</custom>
</div>
If you wanna control parent, this may help you https://michaelnthiessen.com/advanced-vue-controlling-parent-slots

Related

Executing js on slot

I'm a beginner in web development and I'm trying to help out friends restarting an old game. I'm in charge of the tooltip component but I hit a wall...
There are many Vue components and in a lot of them I want to call a child component named Tooltip, I'm using vue-tippy for easy configuration. This is the component:
<template>
<tippy class="tippy-tooltip">
<slot name='tooltip-trigger'></slot>
<template #content>
<slot name='tooltip-content'>
</slot>
</template>
</tippy>
</template>
<script>
import { formatText } from "#/utils/formatText";
export default {
name: "Tooltip",
methods:{
formatContent(value) {
if (! value) return '';
return formatText(value.toString());
}
},
}
</script>
In one of the other components I try to use the tooltip:
<template>
<a class="action-button" href="#">
<Tooltip>
<template #tooltip-trigger>
<span v-if="action.movementPointCost > 0">{{ action.movementPointCost }}<img src="#/assets/images/pm.png" alt="mp"></span>
<span v-else-if="action.actionPointCost > 0">{{ action.actionPointCost }}<img src="#/assets/images/pa.png" alt="ap"></span>
<span v-if="action.canExecute">{{ action.name }}</span>
<span v-else><s>{{ action.name }}</s></span>
<span v-if="action.successRate < 100" class="success-rate"> ({{ action.successRate }}%)</span>
</template>
<template #tooltip-content>
<h1>{{action.name}}</h1>
<p>{{action.description}}</p>
</template>
</Tooltip>
</a>
</template>
<script>
import Tooltip from "#/components/Utils/ToolTip";
export default {
props: {
action: Object
},
components: {Tooltip}
};
</script>
From here everything is fine, the tooltip is correctly displayed with the proper content.
The thing is, the text in the {{ named.description }} needs to be formatted with the formatContent content. I know I can use the props, the components would look like that:
Tooltip.vue:
<template>
<tippy class="tippy-tooltip">
<slot name='tooltip-trigger'></slot>
<template #content>
<h1 v-html="formatContent(title)" />
<p v-html="formatContent(content)"/>
</template>
</tippy>
</template>
<script>
import { formatText } from "#/utils/formatText";
export default {
name: "Tooltip",
methods:{
formatContent(value) {
if (! value) return '';
return formatText(value.toString());
}
},
props: {
title: {
type: String,
required: true
},
content: {
type: Array,
required: true
}
}
}
</script>
Parent.vue:
<template>
<a class="action-button" href="#">
<Tooltip :title="action.name" :content="action.description">
<template v-slot:tooltip-trigger>
<span v-if="action.movementPointCost > 0">{{ action.movementPointCost }}<img src="#/assets/images/pm.png" alt="mp"></span>
<span v-else-if="action.actionPointCost > 0">{{ action.actionPointCost }}<img src="#/assets/images/pa.png" alt="ap"></span>
<span v-if="action.canExecute">{{ action.name }}</span>
<span v-else><s>{{ action.name }}</s></span>
<span v-if="action.successRate < 100" class="success-rate"> ({{ action.successRate }}%)</span>
</template>
</Tooltip>
</a>
</template>
<script>
import Tooltip from "#/components/Utils/ToolTip";
export default {
props: {
action: Object
},
components: {Tooltip}
};
</script>
But I need to use a slot in the tooltip component because we'll have some "extensive" lists with v-for.
Is there a way to pass the data from a slot into a JS function?
If I understand you correctly, you're looking for scoped slots here.
These will allow you to pass information (including methods) from child components (the components with <slot> elements) back to the parents (the component(s) filling those slots), allowing parents to use chosen information directly in the slotted-in content.
In this case, we can give parents access to formatContent(), which will allow them to pass in content that uses it directly. This allows us to keep the flexibility of slots, with the data passing of props.
To add this to your example, we add some "scope" to your content slot in Tooltip.vue. This just means we one or more attributes to your <slot> element, in this case, formatContent:
<!-- Tooltip.vue -->
<template>
<tippy class="tippy-tooltip">
<slot name='tooltip-trigger'></slot>
<template #content>
<!-- Attributes we add or bind to this slot (eg. formatContent) -->
<!-- become available to components using the slot -->
<slot name='tooltip-content' :formatContent="formatContent"></slot>
</template>
</tippy>
</template>
<script>
import { formatText } from "#/utils/formatText";
export default {
name: "Tooltip",
methods: {
formatContent(value) {
// Rewrote as a ternary, but keep what you're comfortable with
return !value ? '' : formatText(value.toString());
}
},
}
</script>
Now that we've added some scope to the slot, parents filling the slot with content can use it by invoking a slot's "scope":
<!-- Parent.vue -->
<template>
<a class="action-button" href="#">
<Tooltip>
. . .
<template #tooltip-content="{ formatContent }">
<!-- Elements in this slot now have access to 'formatContent' -->
<h1>{{ formatContent(action.name) }}</h1>
<p>{{ formatContent(action.description) }}</p>
</template>
</Tooltip>
</a>
</template>
. . .
Sidenote: I prefer to use the destructured syntax for slot scope, because I feel it's clearer, and you only have to expose what you're actually using:
<template #tooltip-content="{ formatContent }">
But you can also use a variable name here if your prefer, which will become an object which has all your slot content as properties. Eg.:
<template #tooltip-content="slotProps">
<!-- 'formatContent' is now a property of 'slotProps' -->
<h1>{{ slotProps.formatContent(action.name) }}</h1>
<p>{{ slotProps.formatContent(action.description) }}</p>
</template>
If you still need the v-html rendering, you can still do that in the slot:
<template #tooltip-content="{ formatContent }">
<h1 v-html="formatContent(title)" />
<p v-html="formatContent(content)"/>
</template>

Passing Named Slot to Child Component

Due to some server side rendering, I'd like to be able to pass a named slot from a parent component into a child component, but I'm not sure the correct mechanism. I'm able to get the top level template to pass through, but the child named slot of title doesn't seem to be accessible.
<div id="app1">
<parent>
<template #forchild>
<div>
For child template area
<template #title>Title!</template>
</div>
</template>
</parent>
</div>
const Child = {
template: `
<div class="child">
Child area
<slot name="forchild">
<slot name="title"></slot>
</slot>
</div>
`,
};
const Parent = {
template: `
<div class="parent">
Parent Area
<child>
<template #forchild>
<slot name="forchild"></slot>
</template>
</child>
</div>
`,
components: {
Child,
},
};
new Vue({
el: '#app1',
components: {
Parent,
}
});
jsfiddle
I am not entirely sure what you are trying to accomplish here. But it is possible to pass slots to grandchildren. And if you want to pass two slots, that's also possible, but you have to pass them separately, not nested. And they also can't be nested inside the grandchild template.
But why not use a single slot?
<div id="app1">
<parent>
<template #forchild>
<div>
For child template area
</div>
<h1>
Title!
</h1>
</template>
</parent>
</div>
const Child = {
template: `
<div class="child">
Child area
<slot name="forchild"></slot>
</div>
`,
};
const Parent = {
template: `
<div class="parent">
Parent Area
<child>
<template #forchild>
<slot name="forchild"></slot>
</template>
</child>
</div>
`,
components: {
Child,
},
};
new Vue({
el: '#app1',
components: {
Parent,
}
});
For more advanced nesting of slots, check out this answer.

Nesting a slot in a slot for vue

Update: Here's a simplified version of what I'm trying to achieve here (from the threaded conversation below):
Accept Component A - Accept Component B - Accept a condition - if
condition is true : wrap Component B with Component A [and render]- else only
render component B.
I'm interested in creating a component that renders a wrapper conditionally. I figured a theoretical approach like this would probably be best**:**
<template>
<div>
<slot v-if="wrapIf" name="wrapper">
<slot name="content"></slot>
</slot>
<slot v-else name="content"></slot>
</div>
</template>
<script>
export default {
props: {
wrapIf: Boolean,
}
}
</script>
Then when we implement, it would look something like this:
...
<wrapper-if :wrap-if="!!link">
<a :href="link" slot="wrapper"><slot></slot></a>
<template slot="content">
content
</template>
</wrapper-if>
The idea being that, in this case, if there is a link, then let's wrap the content with the wrapper slot (which can be any component/element). If there isn't, then let's just render the content without the wrapped link. Pretty simple logic, but it seems that I'm misunderstanding some basic vue functionality because this particular example does not work.
What is wrong with my code or is there some kind of native api that already achieves this or perhaps a dependency that does this sort of thing already?
The output should look like this:
wrapIf === true
<a href="some.link">
content
</a>
wrapIf === false
content
Just focus on the content itself, and let the component worry about whether or not to wrap the default or named content slot.
If you need the wrapper to be dynamic, a dynamic component should solve that. I've updated my solution accordingly. So if you need the wrapper to be a label element, just set the tag property to it, and so on and so forth.
const WrapperIf = Vue.extend({
template: `
<div>
<component :is="tag" v-if="wrapIf" class="wrapper">
<slot name="content"></slot>
</component>
<slot v-else name="content"></slot>
</div>
`,
props: ['wrapIf', 'tag']
});
new Vue({
el: '#app',
data() {
return {
link: 'https://stackoverflow.com/company',
tagList: ['p', 'label'],
tag: 'p',
wrap: true
}
},
components: {
WrapperIf
}
})
.wrapper {
display: block;
padding: 10px;
}
p.wrapper {
background-color: lightgray;
}
label.wrapper {
background-color: lavender;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<wrapper-if :wrap-if="wrap" :tag="tag">
<a :href="link" slot="content">
content
</a>
</wrapper-if>
<div>
Change wrapper type:
<select v-model="tag">
<option v-for="tag in tagList">{{tag}}</option>
</select>
</div>
<button #click="wrap = !wrap">Toggle wrapper</button>
</div>

VueJS Modal component inside component

I have a component like this:
<test></test>
I declare this as follows:
Vue.component('test', {
data: {
showModal: true
},
methods: {
displayComponentModalDialog: function() {
this.showModal = true;
}
},
template: `<button #click='displayComponentModalDialog'>test</button>`
});
The <test></test> component is then placed somewhere inside the <div id="#app"> wrapper.
var app = new Vue({
router,
el: '#app',
// etc.
})
Now, I want to display another component inside the test component. So in this case I want a dialog to appear after I click the button in test component. I am not able to achieve this.
What I did is adding a new component:
Vue.component('dialog', {
template: '#dialog-template'
});
And then the following code, although I do not know for sure where to put it.
<!-- template for the modal component -->
<script type="text/x-template" id="dialog-template">
<transition name="dialog">
<div class="modal-mask">
<div class="modal-wrapper">
<div class="modal-container">
<div class="modal-header">
<slot name="header">
default header
</slot>
</div>
<div class="modal-body">
<slot name="body">
default body
</slot>
</div>
<div class="modal-footer">
<slot name="footer">
<button class="btn btn-primary" #click="$emit('close')">
OK
</button>
</slot>
</div>
</div>
</div>
</div>
</transition>
</script>
<!-- use the modal component, pass in the prop -->
<dialog v-if="showModal" #close="showModal = false">
<h3 slot="header">header</h3>
<p slot="body">
test
</p>
</dialog>
I tried putting this code inside the <test></test> but doesn't work. If I put it inside the template attribute in the component structure, it complains about only one root element.
So it is clear I miss some basic conception how this actually works in VueJS. Someone can help me clarify? Thanks.
As far as I can see your component indeed doesn't have a root tag. Templates have to have a root tag.
This is NOT a valid Vue template:
<div>
<h1>Stuff</h1>
</div>
<h2>Other stuff</h2>
This IS a valid Vue template:
<div>
<div>
<h1>Stuff</h1>
</div>
<h2>Other stuff</h2>
</div>
Note that in the second version we have a single root element for the template, a <div>, whereas in the first one we do not.
You have both a <script></script> and a <dialog></dialog> in your component template.
if you want to add another component in your test component . you can use slot on it.
You can refer to this documentation: https://v2.vuejs.org/v2/guide/components-slots.html
Example:
//component 1
<template>
<div id="modal">
// do something for your modal here.
<slot></slot> // reserve area for your another html or component.
</div>
</template>
// component 2
<template>
<your-modal-component>
<another-component>
</your-modal-component>
</template>

Only show slot if it has content

Is there a way to only display a slot if it has any content?
For example, I'm building a simple Card.vue component, and I only want the footer displayed if the footer slot has content:
Template
<template>
<div class="panel" :class="panelType">
<div class="panel-heading">
<h3 class="panel-title">
<slot name="title">
Default Title
</slot>
</h3>
</div>
<div class="panel-body">
<slot name="body"></slot>
<p class="category">
<slot name="category"></slot>
</p>
</div>
<div class="panel-footer" v-if="hasFooterSlot">
<slot name="footer"></slot>
</div>
</div>
</template>
Script
<script>
export default {
props: {
active: true,
type: {
type: String,
default: 'default',
},
},
computed: {
panelType() {
return `panel-${this.type}`;
},
hasFooterSlot() {
return this.$slots['footer']
}
}
}
</script>
In in View:
<card type="success"></card>
Since the above component doesn't contain a footer, it should not be rendered, but it is.
I've tried using this.$slots['footer'], but this returns undefined.
Does anyone have any tips?
It should be available at
this.$slots.footer
So, this should work.
hasFooterSlot() {
return !!this.$slots.footer;
}
Example.
You should check vm.$slots and also vm.$scopedSlots for it.
hasSlot (name = 'default') {
return !!this.$slots[ name ] || !!this.$scopedSlots[ name ];
}
CSS simplifies this a lot. Just use the following code and voila!
.panel-footer:empty {
display: none;
}
This is the solution for Vue 3 composition API:
<template>
<div class="md:grid md:grid-cols-5 md:gap-6">
<!-- Here, you hide the wrapper if there is no used slot or empty -->
<div class="md:col-span-2" v-if="hasTitle">
<slot name="title"></slot>
</div>
<div class="mt-5 md:mt-0"
:class="{'md:col-span-3': hasTitle, 'md:col-span-5': !hasTitle}">
<div class="bg-white rounded-md shadow">
<div class="py-7">
<slot></slot>
</div>
</div>
</div>
</div>
</template>
<script>
import {ref} from "vue";
export default {
setup(props, {slots}) {
const hasTitle = ref(false)
// Check if the slot exists by name and has content.
// It returns an empty array if it's empty.
if (slots.title && slots.title().length) {
hasTitle.value = true
}
return {
hasTitle
}
}
}
</script>
Now, in Vue3 composition API , you can use useSlots.
<script setup>
import { useSlots } from 'vue'
const slots = useSlots()
</script>
<template>
<div v-if="slots.content" class="classname">
<slot name="content"></slot>
</div>
</template>
In short do this in inline:
<template lang="pug">
div
h2(v-if="$slots.title")
slot(name="title")
h3(v-if="$slots['sub-title']")
slot(name="sub-title")
</template>
I have ran into a similiar issue but across a wide code base and when creating atomic design structured components it can be tiring writing hasSlot() methods all the time and when it comes to TDD - its one more method to test... Saying that, you can always put the raw logic in a v-if but i have found that the template end up cluttered and harder to read on occasions especially for a new dev checking out the code structure.
I was tasked to find out a way of removing parent divs of slots when the slot isnt provided.
Issue:
<template>
<div>
<div class="hello">
<slot name="foo" />
</div>
<div class="world">
<slot name="bar" />
</div>
</div>
</template>
//instantiation
<my-component>
<span slot="foo">show me</span>
</my-component>
//renders
<div>
<div class="hello">
<span slot="foo">show me</span>
</div>
<div class="world"></div>
</div>
as you can see, the issue is that i have an almost 'trailing' div, that could provide styling issues when the component author decides there is no need for a bar slot.
ofcourse we could go <div v-if="$slots.bar">...</div> or <div v-if="hasBar()">...</div> etc but like i said - that can get tiresome and eventually end up harder to read.
Solution
My solution was to make a generic slot component that just rendered out a slot with a surrounding div...see below.
//slot component
<template>
<div v-if="!!$slots.default">
<slot />
</div>
</template>
//usage within <my-component/>
<template>
<div>
<slot-component class="hello">
<slot name="foo"/>
</slot-component>
<slot-component class="world">
<slot name="bar"/>
</slot-component>
</div>
</template>
//instantiation
<my-component>
<span slot="foo">show me</span>
</my-component>
//renders
<div>
<div class="hello">
<span>show me</span>
</div>
</div>
I came into use-case issues when trying this idea and sometimes it was my markup structure that needed to change for the benefit of this approach.
This approach reduces the need for small slot checks within each component template. i suppose you could see the component as a <conditional-div /> component...
It is also worth noting that applying attributes to the slot-component instantiation (<slot-component class="myClass" data-random="randomshjhsa" />) is fine as the attributes trickle into the containing div of the slot-component template.
Hope this helps.
UPDATE
I wrote a plugin for this so the need for importing the custom-slot component in each consumer component is not needed anymore and you will only have to write Vue.use(SlotPlugin) in your main.js instantiation. (see below)
const SLOT_COMPONENT = {
name: 'custom-slot',
template: `
<div v-if="$slots.default">
<slot />
</div>
`
}
const SLOT_PLUGIN = {
install (Vue) {
Vue.component(SLOT_COMPONENT.name, SLOT_COMPONENT)
}
}
export default SLOT_PLUGIN
//main.js
import SlotPlugin from 'path/to/plugin'
Vue.use(SlotPlugin)
//...rest of code
Initially I thought https://stackoverflow.com/a/50096300/752916 was working, but I had to expand on it a bit since $scopeSlots returns a function which is always truthy regardless of its return value. This is my solution, though I've come to the conclusion that the real answer to this question is "doing this is an antipattern and you should avoid it if possible". E.g. just make a separate footer component that could be slotted in.
Hacky solution
hasFooterSlot() {
const ss = this.$scopedSlots;
const footerNodes = ss && ss.footer && ss.footer();
return footerNodes && footerNodes.length;
}
Best Practice (helper component for footer)
const panelComponent = {
template: `
<div class="nice-panel">
<div class="nice-panel-content">
<!-- Slot for main content -->
<slot />
</div>
<!-- Slot for optional footer -->
<slot name="footer"></slot>
</div>
`
}
const footerComponent = {
template: `
<div class="nice-panel-footer">
<slot />
</div>
`
}
var app = new Vue({
el: '#app',
components: {
panelComponent,
footerComponent
},
data() {
return {
name: 'Vue'
}
}
})
.nice-panel {
max-width: 200px;
border: 1px solid lightgray;
}
.nice-panel-content {
padding: 30px;
}
.nice-panel-footer {
background-color: lightgray;
padding: 5px 30px;
text-align: center;
}
<script src="https://unpkg.com/vue#2.6.11/dist/vue.min.js"></script>
<div id="app">
<h1>Panel with footer</h1>
<panel-component>
lorem ipsum
<template #footer>
<footer-component> Some Footer Content</footer-component>
</template>
</panel-component>
<h1>Panel without footer</h1>
<panel-component>
lorem ipsum
</panel-component>
</div>
Hope I understand this right. Why not using a <template> tag, which is not rendered, if the slot is empty.
<slot name="foo"></slot>
Use it like this:
<template slot="foo">
...
</template>
For Vue 3:
Create an utility function
//utils.js
function isSlotHasContent(slotName, slots) {
return Boolean(!!slots[slotName] && slots[slotName]()[0].children.length > 0);
}
In your component:
<script setup>
import { isSlotHasContent } from 'path/to/utils.js';
const slots = useSlots();
// "computed" props has a better performance
const isFooSlotHasContent = computed(() => isSlotHasContent('foo', slots));
</script>
<template>
<div>
<div v-if="isFooSlotHasContent">
<slot name="foo" />
</div>
<div v-if="!isFooSlotHasContent">
Some placeholder
</div>
</div>
</template>
TESTED
So this work for me in vue 3:
I use onMounted to first get the value, and then onUpdate so the value can update.
<template>
<div v-if="content" class="w-1/2">
<slot name="content"></slot>
</div>
</template>
<script>
import { ref, onMounted, defineComponent, onUpdated } from "vue";
export default defineComponent({
setup(props, { slots }) {
const content = ref()
onMounted(() => {
if (slots.content && slots.content().length) {
content.value = true
}
})
onUpdated(() => {
content.value = slots.content().length
console.log('CHECK VALUE', content.value)
})
})
</script>
#Bert answer does not seem to work for dynamic templates like <template v-slot:foo="{data}"> ... </template>.
i ended up using:
return (
Boolean(this.$slots.foo) ||
Boolean(typeof this.$scopedSlots.foo == 'function')
);
I like the Solution of #AlexMA however in my case I needed to pass props to the function in order to get the nodes to show up.
Here is an example of how I am passing the "row" to the scoped slot, in my case the row contains a type param that I want to test against in the calling component.
<other-component>
<template v-slot:expand="{ row }" v-if="!survey.editable">
<div v-if="row.type != 1" class="flex">
{{ row }}
</div>
</template>
</other-component>
In "other-component" I have the template defined as
<template>
<div>
<div v-for="(row, index) in rows">
{{ hasSlotContent(row) }}
<slot name="expand" :row="row"> </slot>
</div>
</div>
</template>
Because the v-slot requires "row" to be passed to it I created a a method
methods:{
hasSlotContent(row){
const ss = this.$scopedSlots
const nodes = ss && ss.expand && ss.expand({ row: row })
return !!(nodes && nodes.length)
}
}
I call this on each iteration so that it can evaluate itself and give back the appropriate response.
you can use the "hasSlotContent(row)" method where-ever you need it, in my example I'm just outputting the truthy value to the DOM.
I hope this helps someone come to a quicker solution.
Reposting a Vue 3 solution from Github, which also works with Options API, since there was a fairly upvoted method from an Issue there:
The comment itself: https://github.com/vuejs/core/issues/4733#issuecomment-1024816095
The function (remove types if you're not writing TypeScript):
import {
Comment,
Text,
Slot,
VNode,
} from 'vue';
export function hasSlotContent(slot: Slot|undefined, slotProps = {}): boolean {
if (!slot) return false;
return slot(slotProps).some((vnode: VNode) => {
if (vnode.type === Comment) return false;
if (Array.isArray(vnode.children) && !vnode.children.length) return false;
return (
vnode.type !== Text
|| (typeof vnode.children === 'string' && vnode.children.trim() !== '')
);
});
}
This works just as fine, if you delete the slotProps argument (unless you need it).