Behind The Math
Behind The Math

Reputation: 575

Is there a way to detect if a scoped slot was passed in to a component?

I have a component that is nested inside several layers of components. A scoped slot can be passed down to this component. I want to detect when it is not.

Example:

Vue.component("parent", {
    template: `
        <child>
            <template slot="expand" slot-scope="{data}" v-if="$scopedSlots.expand">
                <slot name="expand" :data="data"></slot>
            </template>
        </child>
`,
});

Vue.component("child", {
    template: `
        <div>
            <slot name="expand" :data="1"></slot>
            <div @click="$scopedSlots.expand && log()">Child</div>
        </div>
`,
    methods: {
        log() {
            console.log(this.$scopedSlots.expand);
            console.log("This shouldn't work");
        }
    }
});

new Vue({
    el: "#app",
    template: `<parent></parent>`,
})

CodePen

In this example, the scoped slot is never passed in from the root component to <parent>. As a result, I would have thought that $scopedSlots.expand and the v-if in <parent> would both be falsy, so no slot would be passed to <child>, and clicking wouldn't do anything.

However, this appears to be wrong for 2 reasons:

Is there a way to make this work?

I know I can accomplish this by just passing a boolean prop that says whether to process the click. I'm trying to figure out if it's possible to determine from the slot itself.

Upvotes: 4

Views: 3091

Answers (1)

Decade Moon
Decade Moon

Reputation: 34306

I've been hit with this before, it's pretty annoying.

The Vue template compiler will compile this

<template v-if="false"/>

into essentially

vm._e()

where _e is an internal helper method which is createEmptyVNode. So off the bat we know to expect empty vnodes instead of falsey values.

If we investigate further by looking at the compiled code for this template

<child>
  <template slot="expand" slot-scope="scope" v-if="false"/>
</child>

we get this code (which I've simplified a bit to show only the relevant stuff)

_c('child', {
  scopedSlots: {
    expand(scope) {
      return undefined
    }
  }
})

To summarize: v-if="false" does not imply that the scoped slot function will be omitted, instead it will be provided but when called it will return undefined. So in the child component, you can expect $scopedSlots.expand to exist but when called will return undefined.

AFAIK there's no easy way to deal with this. A quick "fix" for your example would be

<div @click="$scopedSlots.expand() && log()">Child</div>
                                ^^

but be wary of

  • will your parent component be able to deal with slot-scope="{data}" for a nonexistent {data}? It might be best to call $scopedSlots.expand(scope) with real scope data.
  • calling $scopedSlots.expand() and discarding the potentially truthy result multiple times in a render cycle could be a performance issue.

FWIW I've had varied success with separating the v-if and slot into separate <template> nodes:

<template v-if="$scopedSlots.expand">
  <template slot="expand" slot-scope="{data}">
    <slot name="expand" :data="data"/>
  </template>
</template>

Upvotes: 5

Related Questions