只显示有内容的插槽

有没有办法只显示一个插槽,如果它有任何内容?

例如,我正在构建一个简单的 Card.vue组件,我只希望在页脚槽包含内容时才显示页脚:

模板

<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>
export default {
props: {
active: true,
type: {
type: String,
default: 'default',
},
},


computed: {
panelType() {
return `panel-${this.type}`;
},


hasFooterSlot() {
return this.$slots['footer']
}
}
}
</script>

视角:

<card type="success"></card>

因为上面的组件不包含页脚,所以不应该呈现它,但它确实呈现了。

我试过使用 this.$slots['footer'],但它返回未定义的值。

有人有什么建议吗?

89559 次浏览

应该可以在

this.$slots.footer

这个应该可以。

hasFooterSlot() {
return !!this.$slots.footer;
}

示例

你应该检查 vm.$slotsvm.$scopedSlots

hasSlot (name = 'default') {
return !!this.$slots[ name ] || !!this.$scopedSlots[ name ];
}

我曾经遇到过一个类似的问题,但是涉及的代码基础很广,当创建原子设计结构化组件时,编写 hasSlot()方法总是很累,而当涉及到 TDD 时——这又是一个需要测试的方法... ... 说到这个,你总是可以把原始逻辑放到 v-if中,但是我发现模板最终会变得杂乱无章,难以阅读,特别是对于一个正在检查代码结构的新开发人员来说。

我的任务是找到一种方法,在插槽没有提供的情况下移除插槽的父 div

问题:

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

正如您所看到的,问题在于我有一个几乎是“拖尾”的 div,当组件作者决定不需要 bar插槽时,它可能会提供样式问题。

当然我们可以选择 <div v-if="$slots.bar">...</div>或者 <div v-if="hasBar()">...</div>等等,但是就像我说的-那样会让人厌烦并且最终很难阅读。

解决方案

我的解决方案是制作一个通用的 slot组件,它只是渲染出一个带有周围 div 的槽... ... 见下文。

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

在尝试这个想法时,我遇到了用例问题,有时候需要为了这种方法的好处而改变的是我的标记结构。 这种方法减少了在每个组件模板中进行小时隙检查的需要。我想你可以把这个组件看作是 <conditional-div />组件。

同样值得注意的是,将属性应用到 slot-component实例化(<slot-component class="myClass" data-random="randomshjhsa" />)是可行的,因为属性会慢慢渗入到包含 slot-component模板的 div 中。

希望这个能帮上忙。

更新 我为此编写了一个插件,因此不再需要在每个消费者组件中导入 custom-slot组件,您只需要在 main.js 实例化中编写 Vue.use (SlotPlugin)。(见下文)

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

希望我没有理解错。如果槽是空的,为什么不使用未呈现的 <template>标记呢。

<slot name="foo"></slot>

像这样使用它:

<template slot="foo">
...
</template>

简而言之,以内联方式做这件事:

<template lang="pug">
div
h2(v-if="$slots.title")
slot(name="title")
h3(v-if="$slots['sub-title']")
slot(name="sub-title")
</template>

最初我认为 https://stackoverflow.com/a/50096300/752916是可行的,但是我不得不对它进行一些扩展,因为 $scopeSlot 返回一个函数,无论返回值是什么,该函数都是真值。这是我的解决方案,尽管我已经得出结论,这个问题的真正答案是“这样做是一个反模式,如果可能的话,您应该避免它”。例如,只要创建一个单独的页脚组件就可以了。

粗糙的解决方案

   hasFooterSlot() {
const ss = this.$scopedSlots;
const footerNodes = ss && ss.footer && ss.footer();
return footerNodes && footerNodes.length;
}

最佳实践(页脚的辅助组件)

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>

CSS 大大简化了这个过程!

.panel-footer:empty {
display: none;
}

这是 Vue 3组合 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>

测试过

所以这对我来说是第三点:

我使用 onMounted 首先获取该值,然后使用 onUpdate 更新该值,以便更新。

   <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 的回答似乎不适用于像 <template v-slot:foo="{data}"> ... </template>这样的动态模板。 我最后说:

 return (
Boolean(this.$slots.foo) ||
Boolean(typeof this.$scopedSlots.foo == 'function')
);

现在,在 Vue3组合 API 中,您可以使用 useSlots

<script setup>
const slots = useSlots()
</script>


<template>
<div v-if="slots.content" class="classname">
<slot name="content"></slot>
</div>
</template>

我喜欢@AlexMA 的解决方案,但是在我的情况下,我需要传递道具给函数,以便让节点显示出来。

下面是一个示例,演示如何将“ row”传递到作用域槽,在我的示例中,该行包含一个类型参数,我希望在调用组件中对其进行测试。

<other-component>
<template v-slot:expand="{ row }" v-if="!survey.editable">
<div v-if="row.type != 1" class="flex">
\{\{ row }}
</div>
</template>
</other-component>

在“ other-Component”中,我将模板定义为

<template>
<div>
<div v-for="(row, index) in rows">
\{\{ hasSlotContent(row) }}
<slot name="expand" :row="row"> </slot>
</div>
</div>
</template>

因为 v 槽需要传递“ row”给它,所以我创建了一个方法

methods:{
hasSlotContent(row){
const ss = this.$scopedSlots
const nodes = ss && ss.expand && ss.expand({ row: row })
return !!(nodes && nodes.length)
}
}

我在每个迭代中调用它,以便它能够评估自身并返回适当的响应。 您可以在需要的地方使用“ hasSlotContent (row)”方法,在我的示例中,我只是将 true 值输出到 DOM。

我希望这能帮助某人更快地找到解决办法。

第三集:

创建一个实用函数

    //utils.js
function isSlotHasContent(slotName, slots) {
return Boolean(!!slots[slotName] && slots[slotName]()[0].children.length > 0);
}

在您的组件中:

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

从 Github 重新发布 Vue 3解决方案,这个解决方案也可以与 Options API 一起使用,因为有一个相当赞成的方法来自于一个问题:

评论本身: https://github.com/vuejs/core/issues/4733#issuecomment-1024816095

该函数(如果不编写 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() !== '')
);
});
}

如果您删除 slotProps参数(除非您需要它) ,那么这种方法也可以很好地工作。