Vue: [Abandoned] RFC: Simplify scoped slot usage

Created on 11 Dec 2018  ·  36Comments  ·  Source: vuejs/vue

This is a follow up of https://github.com/vuejs/vue/issues/7740#issuecomment-371309357

Rational

Problems with current scoped slot usage:

  • Verbose if using <template slot-scope>
  • Limited to one element/component is using slot-scope directly on the slot element.

Proposal

Introduce a new v-scope directive, that can only be used on components:

<comp v-scope="scope">
  {{ scope.msg }}
</comp>

It would work the same as slot-scope for the default scoped slot for <comp> (with <comp> providing the scope value). So it also works with deconstructing:

<comp v-scope="{ msg }">
  {{ msg }}
</comp>

Why a New Directive

I believe the team briefly discussed what I proposed in https://github.com/vuejs/vue/issues/7740#issuecomment-371309357 on Slack some time back, but I could no longer find the chat record. Here's the reasoning behind a new directive:

  • slot-scope was introduced as a special attribute instead of a directive (attributes that start with v- prefix) because slot is an attribute, and we wanted to keep slot-related attributes consistent. slot was in turn introduced as a non-directive attribute because we want the usage to mirror the actual slot usage in the Shadow DOM standard. We figured it would be best to avoid having our own parallel v-slot when there's something that is conceptually the same in the standard.

  • Originally, slot-scope was designed to be only usable on <template> elements that act as abstract containers. But that was verbose - so we introduced the ability to use it directly on a slot element without the wrapping <template>. However, this also makes it impossible to allow using slot-scope directly on the component itself, because it would lead to ambiguity as illustrated here.

  • I thought about adding modifiers or special prefixes to slot-scope so that that we can use it on a component directly to indicate its slot content should be treated as the default scoped slot, but neither a modifier or a prefix like $ seem to be the right fit. The modifier by design should only be applied to directives, while new special syntax for a single use case is inconsistent with the whole syntax design.

  • For a very long time we've shied away from adding more directives, part of it being template syntax is something we want to keep as stable as possible, part of it being that we want to keep core directives to a minimum and only do things that users cannot easily do in userland. However, in this case scoped slot usage is important enough, and I think a new directive can be justified for making its usage significantly less noisy.

Concerns

  • The expression accepted by v-scope is different from most other directives: it expects a temporary variable name (which can also be a deconstruction), but not without a precedence: it acts just like the alias part of v-for. So conceptually v-scope falls into the same camp with v-for as a structural directive that creates temporary variables for its inner scope.

  • This would break the users code if the user has a custom directive named v-scope and is used on a component.

    • Since custom directives in v2 are primarily focused on direct DOM manipulations, it's relatively rare to see custom directives used on components, even more so for something that happens to be called v-scope, so the impact should be minimal.

    • Even in the case of it actually happening, it is straightforward to deal with by simply renaming the custom directive.

discussion intend to implement

Most helpful comment

Breaking down the problem

Trying to synthesize, it sounds like we want a solution that will:

  • reduce boilerplate needed to access data from scoped slots
  • avoid implying that data for the default slot is available in named slots
  • minimize the number of new APIs we have to introduce
  • don't reinvent APIs where web components spec already has an acceptable solution
  • stay explicit, to avoid confusion about where data came from

I might have a solution that addresses all of these! 🤞 With $event for v-on, we have a precedent of providing expressions run inside an implicit function with a named argument. People seem to enjoy that convenience and I don't see a lot of confusion caused by it, so maybe we should follow that example for scoped slots.

Proposed solution

Instead of using v-scope, we could make the slot scope available as $slot. For example:

<TodoList :todos="todos">
  {{ $slot.todo.text }}

  <template slot="metaBar">
    You have {{ $slot.totals.incomplete }} todos left.
  </template>
</TodoList>

For context, the child template might look like:

<ul>
  <li v-for="todo in todos">
    <slot :todo="todo" />
  </li>
</ul>

<div>
  <slot name="metaBar" v-bind="itemsStats" />
</div>

When slots are nested, the $slot object would merge slot data, with the inner-most slots taking override priority. For example, in:

<Outer>
  <Middle>
    <Inner>
      {{ $slot }}
    </Inner>
  </Middle>
</Outer>

merging might look like this:

$slot = {
  ...outerSlotData,
  ...middleSlotData,
  ...innerSlotData
}

You may be worrying/wondering about how to handle namespace overlap and in 99.9% of cases, I really don't think it'll be an issue. For example:

<UserData id="chrisvfritz">
  My email is {{ $slot.user.email }}, but you can reach Sarah at
  <UserData id="sdras">
    {{ $slot.user.email }}
  </UserData>.
</UserData>

In the above example, $slot.user will always have the correct value, despite the nesting scopes. However, sometimes you'll really need access to both properties at the same time, like in:

<UserData id="chrisvfritz">
  <template slot-scope="me">
    <UserData id="sdras">
      <template slot-scope="sarah">
        <CollaborationPageLink :user-ids="[me.user.id, sarah.user.id]">
          View your collaboration history
        </CollaborationPageLink>
      </template>
    </UserData>
  </template>
</UserData>

For these rare edge cases, users could still use slot-scope with its current behavior as an escape hatch. But v-scope would still be unnecessary, because if I understand correctly, its purpose would be to simplify the most common use cases, which the $slot object would already have accomplished without causing the same problems.

Advantages

  • When a user wants to access data provided by a slot, the only boilerplate is $slot, which is about as brief as it can be while remaining explicit. It's immediately obvious that data came from a slot, without needing to search through the component to see where a property is defined.

  • To know which data they can access, users only have to think about which slot(s) the content will be rendered to. I think with v-scope, there would be a lot of confusion with people assuming it works like v-for, because that's the only other directive that defines scoped context properties.

  • Users don't have to be familiar and comfortable with destructuring to use scoped slots, thus reducing the learning curve.

  • In some cases, especially with many nested slots that all provide state, it won't be obvious _which_ component some state came from and users will have to reach slot-scope again. This might sound like a disadvantage, but when I see nested scoped slots, it's almost always for the state provider pattern, which I strongly feel is an anti-pattern. Here's my reasoning. So the fact that many nested scoped slots would require more boilerplate could actually decrease the use of anti-patterns.

  • The API surface area is drastically reduced, because most Vue developers will only have to remember $slot, which is just an object.

  • By using a $-prefixed property, we're building on the web component slot API in a way that makes it clear that $slot is a Vue thing.

  • Currently, libraries often do something like <slot v-bind="user" /> so that their users can save a few characters with slot-scope="user" instead of slot-scope="{ user }. It seems elegant at first glance, but I've come to experience as an anti-pattern. The problem arises when the component wants to expose data _other than_ the user. Then they have two choices: make a breaking change to their API or force this new property onto the user object, even though it may have very little to do with the user. Neither is a great option. Fortunately, $slot would remove the temptation to make components less future-proof, because while $slot is still shorter than $slot.user, you lose important context by aliasing the user as $slot.

Disadvantages

  • In some nested scoped slots, there are some edge cases where it won't be obvious _which_ component some data came from. In these cases though, users can still reach for slot-scope when they need maximum explicitness so I don't think it's a big deal. Plus, as I mentioned earlier, I think it's very likely the user is shooting themselves in the foot with state provider components if they have this problem in the first place.

All 36 comments

Looks good. I was thinking about using the argument to provide a slot name but this would allow multiple directives v scope to be used on the same component and therefore reusing the content provided to the component. Which I'm not sure if it's useful or if it could lead to problems

Overral, this will improve api usage of renderless components, which are being more used with time 🙌

If I understand correctly, v-scope only serves one use case. Instead of writing:

<foo>
  <template slot-scope="{ item }">{{ item.id }}</template>
</foo>

We can write:

<foo v-scope="{ item }">{{ item.id }}</foo>

It does reduced quite a lot noise in this case but it seems to be the only case. It feels like some kind of overkill to introduce a new directive (which works quite differently from other directives, even from v-for because it works only on components and relies on <slot> logic underneath). Another concern is that when I search for v-scope in this repo, the only two occurrences are both discussing about reducing the data scope of a template subtree (https://github.com/vuejs/vue/issues/5269#issuecomment-288912328, https://github.com/vuejs/vue/issues/6913), like how with works in JavaScript.

I like it. One question remaining for me would be how we want to deal with named slots. If we want to allow for named slots to use the same directive, we would need to allow it on <template> as well:

<comp v-scope="scope">
  {{ scope.msg }} <!-- default slot, inheriting from the scope from above -->
  <template slot="someName" v-scope="otherScope">
    <p>{{ otherScope }}</p>
    <div> whatever</div>
  </template>
</comp>

We could use this opportunity to deprecate sot-scope and remove it in Vue 3, replacing it with v-scope

I think we should definitely not end up with two different concepts (v-scope directive and slot-scope attibute) for the same thing.

Sidenote: Now, looking at that, people might get the impression from the hierarchy of elements &
directives, that they can access otherScope and scope in the named slot. Might be a downside.

@LinusBorg for the name, an argument could do v-scope:slotName. I think the point of only allowing this on components is to replace what @Justineo said (https://github.com/vuejs/vue/issues/9180#issuecomment-446168296)

Sidenote: Now, looking at that, people might get the impression from the hierarchy of elements &
directives, that they can access otherScope and scope in the named slot. Might be a downside.

That does work, right? 🤔 I'm pretty sure I've nested slot scopes

That does work, right? 🤔 I'm pretty sure I've nested slot scopes

I didn't nest them, though, they are sibling slots. I defined the default slot's scope with v-scope on the component, and the named slot (which is a sibling slot) with a <template slot="someName">
See? It's confusing ^^

@LinusBorg I think the usage would be confusing. I think conceptually it is:

  • When v-scope is used on the component directly, it means you are only using the default slot.
  • If you want to use named slots... you still need to use slot + slot-scope.

I agree that having both v-scope and slot-scope could be inconsistent/confusing, especially for new users not aware of how we arrived at the current design.

Breaking down the problem

Trying to synthesize, it sounds like we want a solution that will:

  • reduce boilerplate needed to access data from scoped slots
  • avoid implying that data for the default slot is available in named slots
  • minimize the number of new APIs we have to introduce
  • don't reinvent APIs where web components spec already has an acceptable solution
  • stay explicit, to avoid confusion about where data came from

I might have a solution that addresses all of these! 🤞 With $event for v-on, we have a precedent of providing expressions run inside an implicit function with a named argument. People seem to enjoy that convenience and I don't see a lot of confusion caused by it, so maybe we should follow that example for scoped slots.

Proposed solution

Instead of using v-scope, we could make the slot scope available as $slot. For example:

<TodoList :todos="todos">
  {{ $slot.todo.text }}

  <template slot="metaBar">
    You have {{ $slot.totals.incomplete }} todos left.
  </template>
</TodoList>

For context, the child template might look like:

<ul>
  <li v-for="todo in todos">
    <slot :todo="todo" />
  </li>
</ul>

<div>
  <slot name="metaBar" v-bind="itemsStats" />
</div>

When slots are nested, the $slot object would merge slot data, with the inner-most slots taking override priority. For example, in:

<Outer>
  <Middle>
    <Inner>
      {{ $slot }}
    </Inner>
  </Middle>
</Outer>

merging might look like this:

$slot = {
  ...outerSlotData,
  ...middleSlotData,
  ...innerSlotData
}

You may be worrying/wondering about how to handle namespace overlap and in 99.9% of cases, I really don't think it'll be an issue. For example:

<UserData id="chrisvfritz">
  My email is {{ $slot.user.email }}, but you can reach Sarah at
  <UserData id="sdras">
    {{ $slot.user.email }}
  </UserData>.
</UserData>

In the above example, $slot.user will always have the correct value, despite the nesting scopes. However, sometimes you'll really need access to both properties at the same time, like in:

<UserData id="chrisvfritz">
  <template slot-scope="me">
    <UserData id="sdras">
      <template slot-scope="sarah">
        <CollaborationPageLink :user-ids="[me.user.id, sarah.user.id]">
          View your collaboration history
        </CollaborationPageLink>
      </template>
    </UserData>
  </template>
</UserData>

For these rare edge cases, users could still use slot-scope with its current behavior as an escape hatch. But v-scope would still be unnecessary, because if I understand correctly, its purpose would be to simplify the most common use cases, which the $slot object would already have accomplished without causing the same problems.

Advantages

  • When a user wants to access data provided by a slot, the only boilerplate is $slot, which is about as brief as it can be while remaining explicit. It's immediately obvious that data came from a slot, without needing to search through the component to see where a property is defined.

  • To know which data they can access, users only have to think about which slot(s) the content will be rendered to. I think with v-scope, there would be a lot of confusion with people assuming it works like v-for, because that's the only other directive that defines scoped context properties.

  • Users don't have to be familiar and comfortable with destructuring to use scoped slots, thus reducing the learning curve.

  • In some cases, especially with many nested slots that all provide state, it won't be obvious _which_ component some state came from and users will have to reach slot-scope again. This might sound like a disadvantage, but when I see nested scoped slots, it's almost always for the state provider pattern, which I strongly feel is an anti-pattern. Here's my reasoning. So the fact that many nested scoped slots would require more boilerplate could actually decrease the use of anti-patterns.

  • The API surface area is drastically reduced, because most Vue developers will only have to remember $slot, which is just an object.

  • By using a $-prefixed property, we're building on the web component slot API in a way that makes it clear that $slot is a Vue thing.

  • Currently, libraries often do something like <slot v-bind="user" /> so that their users can save a few characters with slot-scope="user" instead of slot-scope="{ user }. It seems elegant at first glance, but I've come to experience as an anti-pattern. The problem arises when the component wants to expose data _other than_ the user. Then they have two choices: make a breaking change to their API or force this new property onto the user object, even though it may have very little to do with the user. Neither is a great option. Fortunately, $slot would remove the temptation to make components less future-proof, because while $slot is still shorter than $slot.user, you lose important context by aliasing the user as $slot.

Disadvantages

  • In some nested scoped slots, there are some edge cases where it won't be obvious _which_ component some data came from. In these cases though, users can still reach for slot-scope when they need maximum explicitness so I don't think it's a big deal. Plus, as I mentioned earlier, I think it's very likely the user is shooting themselves in the foot with state provider components if they have this problem in the first place.

I think a $-prefixed property should be consistent inside a whole Vue instance. I'm not sure if it will cause confusion by implicitly injecting a $slot into each slot and they actually stand for a local argument inside these slot scopes. It's kind of a convention (according to my observation) that $-prefixed properties stand for something available to every Vue instance, so they can be used inside anywhere, including lifecycle hooks, methods and render functions. Adding this special $slot will break this convention.

@chrisvfritz this is an interesting proposal, but it kind of relies on normal slots and scoped slots being unified (so maybe something that can be considered in v3). The biggest problem with this is should the slot content be available in the child component's $slots or $scopedSlots? The only hint is the presence of $slot somewhere in expressions (can be anywhere down the tree), which is not as explicit as slot-scope (which can only be at slot root). Although technically we can detect this in the compiler, for the user the slot will move from $slots to $scopedSlots whenever the user starts using $slot in the template...

Will this have any big impacts on JSX users?

@donnysim no, this does not affect JSX in any way.

I think a $-prefixed property should be consistent inside a whole Vue instance. I'm not sure if it will cause confusion by implicitly injecting a $slot into each slot and they actually stand for a local argument inside these slot scopes.

@Justineo I had the same thought at first, but we do this for $event and no one seems to complain, so there _is_ precedent already and we have evidence that users typically aren't confused and really enjoy the convenience.

The biggest problem with this is should the slot content be available in the child component's $slots or $scopedSlots?

@yyx990803 Great question! I have an idea that might work. What if in cases where it's ambiguous (nested slots using $slot), we just compile all slots to a scoped slot, but also make all $scopedSlots available under $slots as getters? For example, something like:

for (const slotName in vm.$scopedSlots) {
  // Don't override existing slots of the same name,
  // since that's _technically_ possible right now.
  if (vm.$slots[slotName]) continue

  Object.defineProperty(vm.$slots, slotName, {
    get: vm.$scopedSlots[slotName],
    enumerable: true,
    configurable: true,
    writable: true
  })
}

That way, in the case of:

<A>
  <B>
    {{ $slot.foo }}
  </B>
</A>

We could compile to something like:

// Assume new render helper:
// _r = mergeSlotData

_c('A', {
  scopedSlots: _u([
    {
      key: 'default',
      fn: function(_sd) {
        var $slot = _r($slot, _sd)
        return [
          _c('B', {
            scopedSlots: _u([
              {
                key: 'default',
                fn: function(_sd) {
                  var $slot = _r($slot, _sd)
                  return [_v(_s($slot.foo))]
                },
              },
            ]),
          }),
        ]
      },
    },
  ]),
}) 

And it won't matter whether foo comes from <A> or <B>, because inside those components both this.$slots.default and this.$scopedSlots.default(someData) will work.

A couple caveats:

  • In cases with nested slots and only some of them are actually scoped, the compiled render functions from templates using $slot would be slightly larger than if using slot-scope. Not very significantly though, so I think it's fine.

  • I can't think of a real example, but there may be edge cases where a user is iterating over $slots/$scopedSlots and having new or duplicate properties could result in unexpected behavior. I've iterated over dynamically named $slots and $scopedSlots before to enable some interesting patterns, but this change wouldn't affect any of the use cases I've run into.

Thoughts?

we do this for $event and no one seems to complain

@chrisvfritz Oh I do missed the $event thing. Though it's clearly an exception to the convention (I use to believe it is 😅), $event is only available in a very limited scope (only inside the v-bind attribute literals & no nesting).

And for proxying $scopedSlots on $slots:

I used to believe slots and scoped slots are different concepts and Vue have separate use for them, which means I can have both a slot and a scoped slot sharing the same name but serve different purposes. But later I found that Vue actually fallbacks to slot with the same name when the specified scoped slot is unavailable. To me this means we should regard them as the same thing but since we have both $slots and $scopedSlots available on each instance, we can always use them in render functions in a more flexible/unexpected way, like using a default slot to override all list items and a default scoped slot to override a single item. This is actually not encouraged (we haven't suggested the recommended usage on this in our docs and the style guide AFAIK) as we are likely to merge them into a single concept of slot in 3.0, but doing so in 2.x will break some usage allowed since quite a long time back.

we are likely to merge them into a single concept of slot in 3.0, but doing so in 2.x will break some usage allowed since quite a long time back.

@Justineo I might be misunderstanding, but I'm not suggesting merging them in 2.x already - just extending the current fallback behavior. The use case you described, where both $slots and $scopedSlots use the same namespace, would still be possible because the behavior of slot-scope would remain unchanged. For example:

<div>
  Default slot
  <template slot-scope="foo">
    Default scoped slot {{ foo }}
  </template>
</div>

would still work exactly the same. Does that make sense?

@chrisvfritz

I wasn't talking about merging them in 2.x. I was saying if you do this:

make all $scopedSlots available under $slots as getters

You can't distinguish $slots.foo from $scopedSlots.foo and use them separately in render functions anymore.

You can't distinguish $slots.foo from $scopedSlots.foo and use them separately in render functions anymore.

Honestly, I'd be happy if everything (slots and scopedSlots) would be accessible in scopedSlots as functions, it just means no more useless checks what slot to render. It really makes no difference from a components perspective what type of slot the developer uses. Coming from a mixed JSX and template syntax user.

@donnysim Yes I agree with you that they should be merged. I've explained why I have such concern in https://github.com/vuejs/vue/issues/9180#issuecomment-447185512. It's about backward compatibility.

You can't distinguish $slots.foo from $scopedSlots.foo and use them separately in render functions anymore.

@Justineo You _can_ actually, as long as we process $slots first and don't replace already-defined slots. See the example implementation I posted above:

for (const slotName in vm.$scopedSlots) {
  // Don't override existing slots of the same name,
  // since that's _technically_ possible right now.
  if (vm.$slots[slotName]) continue

  Object.defineProperty(vm.$slots, slotName, {
    get: vm.$scopedSlots[slotName],
    enumerable: true,
    configurable: true,
    writable: true
  })
}

@chrisvfritz

Consider the following example:

render (h) {
  if (!this.$slots.default) {
    return h(
      'div',
      this.items.map(item => this.$scopedSlots.default(item))
    )
  }
  return h('div', this.$slots.default)
}

If the component user only passed in vm.$scopedSlots.default, it should loop through this.items and render items into the scoped slot. But now that vm.$scopedSlot.default is now the scoped slot and it exists, the scoped slot will be invoked instead of the slot.

@Justineo I think this use case would be very rare, but the pattern would still be possible by changing:

if (!this.$slots.default) {

to:

if (this.$scopedSlots.default) {

or, if the user will sometimes provide a scoped slot that's supposed to be ignored for some reason:

if (!Object.getOwnPropertyDescriptor(this.$slots, 'default').value) {

I still wouldn't consider it a breaking change though. First, we've never documented/recommended the use of slots and scoped slots of the same name, so it was never part of the public contract. Second, as you mentioned, scoped slots in templates already fall back to a non-scoped slot of the same name, which serves as good historical evidence that we never meant for the scoped/non-scoped boundary to be used like this.

Does that make sense? Also, have you seen any real use cases for reusing slot names? Or can you think of any? I can't, but if there _are_ useful and unique patterns it enables, it would be good to learn about them now because it would also influence our decision to merge them in Vue 3.

I think scoped slots and slots with the same name for different purposes is
a bad idea. I used this in the pré version of Vue promised and removed it
because it was confusing. And now I have to check for both the default slot
and scoped slot so the dev can provide a slot without consuming the data.
And if I recall correctly, you cannot have the same name when using
templates.

@chrisvfritz

I wasn't saying the pattern is helpful and should be supported. It can indeed cause confusion. I was just saying, it is possible to break existing code. We've never documented sharing the same name between slots and scoped slots, but we've never suggest users not to either. And the fallback logic in templates are not documented. It may be not intentional but at least I myself used to believe it is a legitimate usage until I learned the fallback logic in templates...

@posva Thanks for sharing your experience!

@Justineo In your opinion, do you feel like this edge case would cause too much pain for users, or is it acceptable for the convenience of improving the scoped slot API in Vue 2.x?

This was moved to "Todo" - Do we have a consensus on what to implement?

I have been thinking about this the past week. I like @chrisvfritz 's $slot variable proposal and was trying to work out the implementation blockers. Here's what I think is feasible:

  • All slots are compiled as functions (like in 3.0), so no more static slots (this leads to more accurate component update tracking)
  • $scopedSlots will expose every slot as a function
  • $slots will expose every slot as the result of calling the corresponding function with an empty object.

So the following would be equivalent:

this.$slots.default
// would be the same as
this.$scopedSlots.default({} /* $slot */)

With the above internal changes, $slots and $scopedSlots are essentially unified but should be backwards compatible! It should also lead to easier migration to 3.0.

Some usage samples using the proposed $slot variable:

<!-- list and item composition -->
<fancy-list>
  <fancy-item :item="$slot.item"/>
</fancy-list>
<!-- hypothetical data fetching component -->
<fetch :url="`/api/posts/${id}`">
  <div slot="success">{{ $slot.data }}</div>
  <div slot="error">{{ $slot.error }}</div>
  <div slot="pending">pending...</div>
</fetch>

I don't think merging nested scopes is a good idea. I think it's better to be explicit when nesting is involved (i.e. always use slot-scope if you have nested scoped slots):

<!-- nested usage -->
<foo>
  <bar slot-scope="foo">
    <baz slot-scope="bar">
      {{ foo }} {{ bar }} {{ $slot }}
    </baz>
  </bar>
</foo>

In you last example, which slot is $slot referring to?

@Akryum it’s { ...foo, ...bar }.

@Akryum @Justineo hmm I can tell this could be confusing... we made slot-scope usable on the root element of a slot, i.e. here foo is the slot scope provided by <foo/>, bar is provided by <bar/> and $slot is provided by <baz/>... now I think it was a mistake to allow such usage, because the more intuitive usage in this case would be something like this:

<!-- nested usage -->
<foo slot-scope="foo">
  <bar slot-scope="bar">
    <baz slot-scope="baz">
      {{ foo }} {{ bar }} {{ baz }}
    </baz>
  </bar>
</foo>

this could be confusing

more intuitive usage in this case would be something like this

Absolutely agree. Also, we can continue to use destructuring.
For example:
```vue

<FieldValidation field="login" slot-scope="{ value, setValue, error }">
  <LoginInput
    :value="value"
    :error="error"
    @input="setValue"
  />
</FieldValidation>

<FieldValidation field="password" slot-scope="{ value, setValue, error }">
  <PasswordInput
    :value="value"
    :error="error"
    @input="setValue"
  />
</FieldValidation>

Closed via 7988a554 and 5d52262f

Summary:

  • Support $slot variable in all slots. (The presence of $slot causes the slot to be compiled as a scoped slot).

  • All slots, including normal slots, are now exposed on this.$scopedSlots as functions. This means assuming this.$slots.default exists, this.$scopedSlots.default() will return its value. This allows render function users to always use this.$scopedSlot and no longer worry about whether the slot being passed in is scoped or not. This is also consistent with 3.0 where all slots are exposed as functions (but on this.$slots instead).

  • No change to slot-scope usage, no introduction of new directive. We want to make syntax changes minimal and leave potential breakage to 3.0.

@yyx990803 Does $slot merge all outer slot scopes, or just refer to the closest slot?

@Justineo no merging, just the closest.

Looking at scoped slot usage in the apps I have access to, it seems the $slot shorthand wouldn't actually be usable in just over half of cases without the nested slot merging behavior. The problem is that base components are typically used in place of raw HTML elements, and these components often have their own slots. For example:

<MapMarkers :markers="cities">
  <BaseIcon name="map-marker">
    {{ $slot.marker.name }}
  </BaseIcon>
</MapMarkers>

In the example above, there's only a single scoped slot, for the <MapMarkers> component. <BaseIcon> accepts a non-scoped slot, but because it accepts a slot at all, $slot would be undefined or an empty object without the merging behavior, forcing a refactor to the clunkier:

<MapMarkers :markers="cities">
  <template slot-scope="{ marker }">
    <BaseIcon name="map-marker">
      {{ marker.name }}
    </BaseIcon>
  </template>
</MapMarkers>

So here are my concerns:

  • At least from my own sampling, it appears _most_ of the benefit of $slot could be lost if it only refers to the inner-most slot. In fact, it might even do more harm than good, since there's more API for people to learn.

  • UI libraries might start over-using props with v-html where slots would be more appropriate, in response to user complaints about not being able to use $slot.

  • Users might start avoiding base components in some cases, to keep scoped slot usage more elegant, leading to less maintainable code.

@yyx990803 Is your main concern with $slot merging that it would be difficult to tell which component slot data came from? If so, it may help to know that the only cases where apps I have access to used nested scoped slots were for state provider components, which as I mentioned earlier I've found to be an anti-pattern. So especially with well-named slot properties, I think cases of actual ambiguity in legitimate use cases would be very rare. And with the automatic merging behavior, users would be able to decide for themselves whether they needed more explicitness - and when they do, that's what slot-scope would exist for.

As a compromise though, here's a potential alternative: what if instead of merging nested scoped slots, we had $slot refer to the closest slot _that provided a scope_? That could eliminate cases with the most ambiguity, while still addressing my concerns. Thoughts?

The primary reason for avoiding merging is the non-explicitness: just by reading the template, you really don't know which $slot property comes from which provider component. This requires you to be fully aware of the implementation details of each component you are looking at to be sure of what is going on, and that adds a lot of mental load. It may sound alright the moment you are writing it because you have the full context in your mind at that point of time, but it makes the code much more difficult to understand for a future reader (whether that being you or another team member).

$slot refer to the closest slot that provided a scope

I don't think that would work. It leads to the same problem: you'd need to know about the implementation details of the components to be sure of what's going on. I'd say it's even more implicit and potentially confusing than merging.


Long term wise I think the best solution is changing the semantics of slot-scope so that it becomes a way to alias/destructure $slot. So your example would look like:

<MapMarkers :markers="cities" slot-scope="{ marker }">
  <BaseIcon name="map-marker">
    {{ marker.name }}
  </BaseIcon>
</MapMarkers>

This eliminates the need for merging, is explicit about which variables comes from which provider, and is not overly verbose. (I think this is what we are probably going to do in 3.0)

The awkward spot we are in right now is that we allowed slot-scope to be used on non-templates, and now that prevents us from using it on the component itself.

Changing its semantics in 3.0 could also lead to a lot of confusion and migration pain.

Maybe we can sidestep the while problem by introducing a new property, slot-alias, which does exactly what it says - aliasing $slot to something else. Unlike slot-scope, it can only be used on the component itself or a template slot container (this does mean it would work exactly the same with slot-scope when used on a <template>):

Nested default slots:

<MapMarkers :markers="cities" slot-alias="{ marker }">
  <BaseIcon name="map-marker">
    {{ marker.name }}
  </BaseIcon>
</MapMarkers>
<foo slot-alias="foo">
  <bar slot-alias="bar">
    <baz slot-alias="baz">
      {{ foo }} {{ bar }} {{ baz }}
    </baz>
  </bar>
</foo>

Named slots without nesting:

The only case where it inevitably becomes verbose, is named slots with nesting:

<foo>
  <template slot="a" slot-alias="a">
     <bar slot-alias="b">
        {{ a }} {{ b }}
     </bar>
  </template>
</foo>

(This can in fact be less verbose by using both slot-scope AND slot-alias, but I think that can be rather confusing. I'm in favor of deprecating slot-scope altogether)

<foo>
   <bar slot="a" slot-scope="a" slot-scope="b">
      {{ a }} {{ b }}
   </bar>
</foo>

Finally, we can even give it a shorthand, () (since in JSX render props are usually arrow functions that start with ()):

<MapMarkers :markers="cities" ()="{ marker }">
  <BaseIcon name="map-marker">
    {{ marker.name }}
  </BaseIcon>
</MapMarkers>
<foo ()="foo">
  <bar ()="bar">
    <baz ()="baz">
      {{ foo }} {{ bar }} {{ baz }}
    </baz>
  </bar>
</foo>

Compare the above to the equivalent JSX:

<foo>
  {foo => (
    <bar>
      {bar => (
        <baz>
          {baz => `${foo} ${bar} ${baz}`}
        </baz>
      )}
    </bar>
  )}
</foo>

Closing since the design is now very different from what was originally proposed. Opening new thread instead.

Was this page helpful?
0 / 5 - 0 ratings