2
votes

Updated code in light of comments about slot-scope

Coming from React, I am having a hard time understanding how Vue uses slots and slot-scope to pass props to child components.

In my current project, I have a DataSlicer component that receives a data prop and performs some manipulation on it. I would then like to pass the manipulated data on to child components via <slot>. I understand that slot-scope is the way to do this but it seems to only work when the parent component tells a child to pass something to its own children -- which seems to defeat the whole point (in terms of composition and separation of concerns).

This is what I have working right now, based on reading How to pass props using slots from parent to child -vuejs

App.vue

<DataSlicer
    v-if="data"
    y-axis-dimension="Subparameter"
    x-axis-dimension="Year"
    value-dimension="Total_Registrations"
    :filters="{}"
    :data="data"
>
    <template slot-scope="childProps">
        <Chart :title="title" :series-data="childProps.slicedData" />
    </template>
</DataSlicer>

DataSlicer.vue

<template>
  <div>
    <slot :slicedData="slicedData"/>
  </div>
</template>

My expectation was that I could define slot-scope on the <template> tag in DataSlicer.vue, but that doesn't seem to work.

Am I missing something? I don't understand why App.vue would need to know or care what DataSlicer is passing to its children. The problem is only compounded the more I split up my components (for example, if I stick DataSlicer inside another component to abstract the api calls, which are currently handled at the App.vue level, then App.vue also has to also tell that component to pass data on to its DataSlicer child[ren]).

Maybe I am just stuck in React thinking here and there's a different or better way to do this?

EDIT:

If it helps, I'm able to accomplish what I want using a render function like so:

render: function (createElement) {
    return createElement(
      'div',
      this.$slots.default.map(vNode => {
        if (vNode.componentOptions) {
          vNode.componentOptions.propsData.slicedData = this.slicedData;
        }
        return vNode;
      })
    )
  }

This feels rather hackish/fragile and also like I'm stretching Vue to do something in the "react way" when there is probably a better approach.

EDIT 2:

After more research and experimentation, I have also tried the provide/inject approach. This works (by using defineObjectProperty to bind a dynamic getter for the property being passed) but it means child components have to be explicitly written to accept provided props rather than just accepting the prop whether it's provided by a parent component or directly.

I've also tried using dynamic components and v-for (<component>), but it ends up being really messy as I am essentially re-initializing the already-defined components received in $slots.default as dynamic ones -- it also introduces some recursive weirdness when I want these components to also have children, and it's hard to deal with non-component children.

This is a very common pattern in React and trivial to implement, so I'm still curious if there is another way of accomplishing this that is more in line with the Vue way of doing things.

I am trying to build components that are reusable, self-contained, and composable, so having to specify slot-scope in a <template> tag in the parent component each time these are used together doesn't really make sense for my purposes. My child components should not be concerned with where their props come from, and my parent components should not be concerned with what props their children are passing on to their own children.

1
In DataSlicer.vue <slot :slicedData="slicedData"/> is defining the property 'slicedData' on the slot scope. In App.vue, <template slot-scope="{ slicedData }"> is assigning the slot scope to a variable named "{ slicedData }". Does this code work at all?Andrew Castellano
@AndrewCastellano Hi Andrew, yes this code works - somewhere I had picked up the idea that object destructuring syntax could be used to pass what I wanted, but now I think I understand that slot-scope is just the namespace. I've updated my code to make this clearer (this doesn't affect the underlying question of why the parent component should handle this).stuff and things

1 Answers

3
votes

I was finally able to do what I wanted using the new v-slot scope in Vue 2.6, described here: https://github.com/vuejs/vue/issues/9306

Part of my issue was that the Vue team sees the "state provider" approach I was going for as an anti-pattern because it's not clear where props are coming from. I see their point and I think the new syntax provides a good balance between being clear and avoiding the excessive boilerplate of <template> tags wrapping the child components. It also simplifies and makes explicit the ability for a component to take both provided props and "regular" props without caring where they come from (my problem with provide/inject).

The final result looks something like this:

App.vue

<DataGetter {...other props} v-slot="{ data }">
      <DataSlicer {...other props} :data="data" v-slot="{ slicedData }">
        <!-- Using provided data prop -->
        <Chart :data="slicedData" />
        <!-- Data prop explicitly defined -->
        <Chart :data="[ 1, 2, 3 ]" />
      </DataSlicer>
</DataGetter>

DataGetter and DataSlicer render functions:

render(h) {
    if (this.$scopedSlots.default) {
      return h(
        'div',
        this.$scopedSlots.default({ data: this.data })
      )
    }
  }

I hope this is helpful for anyone else coming from React. I'll leave this question open in case someone else comes along with a better answer.