-
-
Notifications
You must be signed in to change notification settings - Fork 33.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Abandoned] RFC: Simplify scoped slot usage #9180
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, <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 |
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 <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 I think we should definitely not end up with two different concepts ( Sidenote: Now, looking at that, people might get the impression from the hierarchy of elements & |
@LinusBorg for the name, an argument could do
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 |
@LinusBorg I think the usage would be confusing. I think conceptually it is:
I agree that having both |
Breaking down the problemTrying to synthesize, it sounds like we want a solution that will:
I might have a solution that addresses all of these! 🤞 With Proposed solutionInstead of using <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 <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, <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 Advantages
Disadvantages
|
I think a |
@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 |
Will this have any big impacts on JSX users? |
@donnysim no, this does not affect JSX in any way. |
@Justineo I had the same thought at first, but we do this for
@yyx990803 Great question! I have an idea that might work. What if in cases where it's ambiguous (nested slots using 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 A couple caveats:
Thoughts? |
@chrisvfritz Oh I do missed the And for proxying 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 |
@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 <div>
Default slot
<template slot-scope="foo">
Default scoped slot {{ foo }}
</template>
</div> would still work exactly the same. Does that make sense? |
I wasn't talking about merging them in 2.x. I was saying if you do this:
You can't distinguish |
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 #9180 (comment). It's about backward compatibility. |
@Justineo You can actually, as long as we process 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
})
} |
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 |
@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.
|
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... |
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
So the following would be equivalent: this.$slots.default
// would be the same as
this.$scopedSlots.default({} /* $slot */) With the above internal changes, Some usage samples using the proposed <!-- 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 <!-- nested usage -->
<foo>
<bar slot-scope="foo">
<baz slot-scope="bar">
{{ foo }} {{ bar }} {{ $slot }}
</baz>
</bar>
</foo> |
In you last example, which slot is |
@Akryum it’s |
@Akryum @Justineo hmm I can tell this could be confusing... we made <!-- nested usage -->
<foo slot-scope="foo">
<bar slot-scope="bar">
<baz slot-scope="baz">
{{ foo }} {{ bar }} {{ baz }}
</baz>
</bar>
</foo> |
Absolutely agree. Also, we can continue to use destructuring. <Validation :constraints="AuthForm" slot-scope="{ validate }">
<AuthForm @submit.prevent="validate">
<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>
</AuthForm>
</Validation> |
Closed via 7988a55 and 5d52262 Summary:
|
@yyx990803 Does |
@Justineo no merging, just the closest. |
Looking at scoped slot usage in the apps I have access to, it seems the <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 :markers="cities">
<template slot-scope="{ marker }">
<BaseIcon name="map-marker">
{{ marker.name }}
</BaseIcon>
</template>
</MapMarkers> So here are my concerns:
@yyx990803 Is your main concern with As a compromise though, here's a potential alternative: what if instead of merging nested scoped slots, we had |
The primary reason for avoiding merging is the non-explicitness: just by reading the template, you really don't know which
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 <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 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, 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 <foo>
<bar slot="a" slot-scope="a" slot-scope="b">
{{ a }} {{ b }}
</bar>
</foo> Finally, we can even give it a shorthand, <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. |
Does slot scope not working in shadow dom?
<div>
<slot :data="data"></slot>
</div>
<cust-element1>
<template slot-scope="scope">{{ scope.xxx.yyy }}</template>
</cust-element1> |
This is a follow up of #7740 (comment)
Rational
Problems with current scoped slot usage:
<template slot-scope>
slot-scope
directly on the slot element.Proposal
Introduce a new
v-scope
directive, that can only be used on components: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:Why a New Directive
I believe the team briefly discussed what I proposed in #7740 (comment) 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 withv-
prefix) becauseslot
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 parallelv-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 usingslot-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 ofv-for
. So conceptuallyv-scope
falls into the same camp withv-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.
The text was updated successfully, but these errors were encountered: