Passing slot into slot in Vue.js

Viewed 2628

I'm trying to pass slot into the slot but child component which requires passed down slot doesn't see it.

In the child (TheTable) I have table component from Core UI for Vue (CDataTable) which requires certain slots which I want to pass from a parent:

  <CDataTable
      class="overflow-auto"
      :items="this.items"
      :fields="fieldData"
      :sorter="{ external: true, resetable: true }"
      :sorter-value="this.sorterValue"
      :table-filter="{ external: true, lazy: false, placeholder: 'Enter here', label: 'Search:'}"
      :responsive="false"
      :loading="this.loading"
      :hover="true"
      :items-per-page-select="true"
      :items-per-page="this.perPage"
      @pagination-change="paginationChanged"
      @update:sorter-value="sorterUpdated"
      @update:table-filter-value="filterUpdated"
  >
      <slot name="requiredTableFields"></slot>
  </CDataTable>

In the parent component I have:

<TheTable
        v-bind:fields="fields"
        v-bind:endpoint-url="endpointUrl"
>
    <template slot="requiredTableFields">
        <td slot="name" slot-scope="{item}">
            <div>{{ item['attributes.email'] }}</div>
            <div class="small text-muted">
                {{ item['attributes.firstname'] }} {{ item['attributes.lastname'] }}
            </div>
        </td>
        <td slot="registered" slot-scope="{item}">
            <template v-if="item['attributes.created_at']">{{ item['attributes.created_at'] }}</template>
            <template v-else>Not setup yet</template>
        </td>
    </template>
</TheTable>

Is there any way to make it work? Cheers, Casper

1 Answers

Merging the underlying slots into a single requiredTableFields slot isn't going to work because there's no (easy) way to break the child slots back out once they've been merged.

Instead you can just keep the slots separate:

<TheTable ...>
  <template v-slot:name="{ item }">
    <td>
      ...
    </td>
  </template >
  <template v-slot:registered="{ item }">
    <td>
      ...
    </td>
  </template>
</TheTable>

This is passing two scoped slots, name and registered, into TheTable.

Assuming you don't want to hard-code the slot names into TheTable you'd then need to iterate over the $scopedSlots to include them dynamically.

<CDataTable ...>
  <template v-for="(x, slotName) in $scopedSlots" v-slot:[slotName]="context">
    <slot :name="slotName" v-bind="context" />
  </template>
</CDataTable>

Some notes on this:

  1. x is not used, we just need to loop over the slot names.
  2. The 'data' associated with the scoped slot is referred to as context and is just passed on.
  3. If the slots weren't scoped slots it'd be slightly different. We'd iterate over $slots instead and remove all the parts that refer to context.
  4. There is a : at the start of the :name attribute as we want to pass a dynamic name. It is an unfortunate coincidence that one of the slots in the original question is also called name, potentially leading to some confusion here.
  5. The v-bind="context" part is analogous to the JavaScript spread operator, if that makes it any clearer. Think of it as attributes = { name: slotName, ...context }.

Below is a complete example illustrating this technique outlined above. It doesn't use CDataTable but the core principle for passing on the slots is exactly the same.

const Comp2 = {
  template: `
    <div>
      <h4>Left</h4>
      <div><slot name="top" item="Red" /></div>
      <h4>Right</h4>
      <div><slot name="bottom" item="Green" /></div>
    </div>
  `
}

const Comp1 = {
  template: `
    <div>
      <comp2>
        <template v-for="(x, slotName) in $scopedSlots" v-slot:[slotName]="context">
          <slot :name="slotName" v-bind="context" />
        </template>
      </comp2>
    </div>
  `,
  
  components: {
    Comp2
  }
}

new Vue({
  el: '#app',

  components: {
    Comp1
  }
})
<script src="https://unpkg.com/vue@2.6.11/dist/vue.js"></script>

<div id="app">
  <comp1>
    <template v-slot:top="{ item }">
      <button>Slot 1 - {{ item }}</button>
    </template>
    <template v-slot:bottom="{ item }">
      <button>Slot 2 - {{ item }}</button>
    </template>
  </comp1>
</div>

Related