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

RFC: New syntax for scoped slots #9306

Closed
yyx990803 opened this Issue Jan 12, 2019 · 15 comments

Comments

Projects
8 participants
@yyx990803
Copy link
Member

yyx990803 commented Jan 12, 2019

This is an update of #9180 where we attempt to finalize a new syntax proposal for scoped slots (in a backwards compatible way).

Rationale

When we first introduced scoped slots, it was verbose because it required always using <template slot-scope>:

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

To make it less verbose, in 2.5 we introduced the ability to use slot-scope directly on the slot element:

<foo>
  <div slot-scope="{ msg }">
    {{ msg }}
  </div>
</foo>

This means it works on component as slot as well:

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

However, the above usage leads to a problem: the placement of slot-scope doesn't always clearly reflect which component is actually providing the scope variable. Here slot-scope is placed on the <bar> component, but it's actually defining a scope variable provided by the default slot of <foo>.

This gets worse as the nesting deepens:

<foo>
  <bar slot-scope="foo">
    <baz slot-scope="bar">
      <div slot-scope="baz">
        {{ foo }} {{ bar }} {{ baz }}
      </div>
    </baz>
  </bar>
</foo>

It's not immediately clear which component is providing which variable in this template.

Someone suggested that we should allow using slot-scope on a component itself to denote its default slot's scope:

<foo slot-scope="foo">
  {{ foo }}
</foo>

Unfortunately, this cannot work as it would lead to ambiguity with component nesting:

<parent>
  <foo slot-scope="foo"> <!-- provided by parent or by foo? -->
    {{ foo }}
  </foo>
</parent>

This is why I now believe allowing using slot-scope without a template was a mistake.

In 2.6, we are planning to introduce a new syntax for scoped slots.

Goals of the new proposal

  • Still provide succinct syntax for most common use cases of scoped slots (default slots)

  • Clearer connection between scoped variable and the component that is providing it.

Syntax Details

  • Introducing a new special attribute: slot-props.

    • It can be used on a component to indicate that the component's default slot is a scoped slot, and that props passed to this slot will be available as the variable declared in its attribute value:

      <foo slot-props="{ msg }">
        {{ msg }}
      </foo>
    • It can also be used on <template> slot containers (exactly the same usage as slot-scope in this case):

      <foo>
        <template slot="header" slot-props="{ msg }">
          {{ msg }}
        </template>
      </foo>
    • It can NOT be used on normal elements.

  • slot-props also has a shorthand syntax: ().

    The above examples using shorthand syntax:

    <foo ()="{ msg }">
      {{ msg }}
    </foo>
    
    <foo>
      <template slot="header" ()="{ msg }">
        {{ msg }}
      </template>
    </foo>

    The shorthand is () because it resembles the starting parens of an arrow function and loosely relates to "creating a scope". An arrow function is also typically used for render props, the equivalent of scoped slots in JSX.

Comparison: New vs. Old

Let's review whether this proposal achieves our goals outlined above:

  • Still provide succinct syntax for most common use cases of scoped slots (single default slot):

    Can we get any more succinct than this?

    <foo ()="{ msg }">{{ msg }}</foo>
  • Clearer connection between scoped variable and the component that is providing it:

    Let's take another look at the deep-nesting example using current syntax (slot-scope) - notice how slot scope variables provided by <foo> is declared on <bar>, and the variable provided by <bar> is declared on <baz>...

    <foo>
      <bar slot-scope="foo">
        <baz slot-scope="bar">
          <div slot-scope="baz">
            {{ foo }} {{ bar }} {{ baz }}
          </div>
        </baz>
      </bar>
    </foo>

    This is the equivalent using the new syntax:

    <foo ()="foo">
      <bar ()="bar">
        <baz ()="baz">
          {{ foo }} {{ bar }} {{ baz }}
        </baz>
      </bar>
    </foo>

    Notice that the scope variable provided by a component is also declared on that component itself. The new syntax shows a clearer connection between slot variable declaration and the component providing the variable.

Here are some more usage examples using both the new and old syntax.

Q&A

Why a new attribute instead of fixing slot-scope?

If we can go back in time, I would probably change the semantics of slot-scope - but:

  1. That would be a breaking change now, and that means we will never be able to ship it in 2.x.

  2. Even if we change in in 3.x, changing the semantics of existing syntax can cause a LOT of confusion for future learners that Google into outdated learning materials. We definitely want to avoid that. So, we have to introduce a new attribute to differentiate from slot-scope.

What happens to slot-scope?

It's going to be soft-deprecated: it will be marked deprecated in the docs, and we would encourage everyone to use / switch to the new syntax, but we won't bug you with deprecation messages just yet because we know it's not a top priority for everyone to always migrate to the newest stuff.

In 3.0 we do plan to eventually remove slot-scope, and only support slot-props and its shorthand. We will start emitting deprecation messages for slot-scope usage in the next 2.x minor release to ease the migration to 3.0.

Since this is a pretty well defined syntax change, we can potentially provide a migration tool that can automatically convert your templates to the new syntax.

yyx990803 added a commit that referenced this issue Jan 12, 2019

@leopiccionia

This comment has been minimized.

Copy link

leopiccionia commented Jan 12, 2019

I like both the new syntax (shorthand and not) and the new, more intuitive semantics. IMO $slot seemed as magical as 1.x $index.

Will it emit a compilation error if one use slot-props in parent and slot-scope in child, or is it someway valid?

@c01nd01r

This comment has been minimized.

Copy link

c01nd01r commented Jan 12, 2019

  1. slot-props
    Clear and obvious attribute 👍
  2. Shorthand syntax forslot-props
    It's confusing for me. At first glance, I don't understand what this means in the html template. It looks like a arrow function, but it's not. IMHO, It looks hostile to html templates.
    I do not think that slot-props is verbose.
  3. slot-props with <template>
    50/50. Does it make sense for the new syntax? Interesting to know the opinions of community members.

So, I think for scoped slots syntax must follow the rule: "There should be one - and preferably only one - obvious way to do it".

@yyx990803

This comment has been minimized.

Copy link
Member Author

yyx990803 commented Jan 12, 2019

@c01nd01r do you use the : and @ shorthands?

@dimensi

This comment has been minimized.

Copy link

dimensi commented Jan 12, 2019

@yyx990803 () ={} looks looks weird. It looks just as disgusting as render props in jsx. And looks like not from vue.

<AppWrapper class="wrapper"
     prop-one="one"
     prop-two="two"
     ()="{ wrap }">
      <AppTranslate t="hello" ()="{ text }">
        <AppButton @click="wrap.handler">{{ text }}</AppButton>
      </AppTranslate>
</AppWrapper>

the more components there are, the easier it will be in the markup to lose this shorthands.

@yyx990803

This comment has been minimized.

Copy link
Member Author

yyx990803 commented Jan 13, 2019

@dimensi so just use the long form slot-props? It's less distinguishable from a normal prop though, so the shorthand looking "weird" actually serves a purpose.

@c01nd01r

This comment has been minimized.

Copy link

c01nd01r commented Jan 13, 2019

@yyx990803 Yep, much more often than slot-scope. But using slot-scope requires more attention.

Anyway, I will be glad if slot-props will be implemented.
Hopefully, will be possible to prohibit the use of short syntax with eslint.

@yyx990803 yyx990803 added this to Todo in 2.6 via automation Jan 13, 2019

@yyx990803 yyx990803 moved this from Todo to In progress in 2.6 Jan 13, 2019

@Justineo

This comment has been minimized.

Copy link
Member

Justineo commented Jan 13, 2019

My thoughts on the naming/shorthand:

ATM I'm not feeling too verbose to use slot-scope since we only need to use it once at most for each component, unlike v-bind and v-on, which are usually repeated several times.

We now only have shorthands for directives (namely v-bind and v-on), and our guide suggests that shorthands are for v- prefixed stuff. () for slot-props will add an exception, which means more education/documentation.

So what about use v-scope (like originally proposed in #9180 ) instead of slot-props? It's distinguishable from normal props and easier to type. And plus we probably don't need shorthand for that...

<foo v-scope="foo">
  <bar v-scope="bar">
    <baz v-scope="baz">
      {{ foo }} {{ bar }} {{ baz }}
    </baz>
  </bar>
</foo>
@rellect

This comment has been minimized.

Copy link

rellect commented Jan 14, 2019

If I may suggest a different shorthand

<foo &="{ msg }">
  {{ msg }}
</foo>

The ampersand is known in programming as a reference/pointer to variables.

@dimensi

This comment has been minimized.

Copy link

dimensi commented Jan 14, 2019

@rellect it's more vue like, than (). Looks nice.
And we can abbreviate slot=header slot-props="{bar}" to &header="{bar}"

<foo &="{ fooText }">
  {{ fooText }}
  <template &header="{ msg }">
    {{ msg }}
  </template>
</foo>

or

<foo &="{ fooText }" &header="{ msg }">
  {{ fooText }} - {{ msg }}
  <header-block slot="header" :title="msg" />
  <bar &="baz" &side-bar="{ sideBarContent }">
     {{ baz }}
     <side-bar :content="sideBarContent" slot="side-bar" />
  </bar>
</foo>

// with slot-props
<foo slot-props="{ fooText }" slot-props.header="{ msg }">
  {{ fooText }} - {{ msg }}
  <header-block slot="header" :title="msg" />
  <bar slot-props="baz" slot-props.side-bar="{ sideBarContent }">
     {{ baz }}
     <side-bar :content="sideBarContent" slot="side-bar" />
  </bar>
</foo>

🤔

upd:
@yyx990803 i not understand, how will look slot-props on other elements not <template>?
We will return to the time where we write tons of template elements for named scoped slot?

<foo ()="{ fooText }">
  {{ fooText }}
  <template slot="header" ()="{ msg }">
    <div>
      {{ msg }}
    </div>
 </template>
</foo>
@Gudradain

This comment has been minimized.

Copy link

Gudradain commented Jan 14, 2019

How will that new syntax work with v-for like in the documentation or when you have multiple named slot to which you want to pass different variable?

When I consider those 2 scenarios the old syntax of declaring the provided variable on the "slot" rather than on the parent component made more sense.

@yyx990803

This comment has been minimized.

Copy link
Member Author

yyx990803 commented Jan 14, 2019

@Gudradain see usage examples - you'd have to use a <template>

@yyx990803

This comment has been minimized.

Copy link
Member Author

yyx990803 commented Jan 14, 2019

@rellect @dimensi I think the difference between () and & is just personal preference, but I guess we acknowledge that a shorthand can be useful to write more succinct templates.

For named slots yes <template> wrappers are required. But it can be made less verbose by the suggested idea of combining slot name into the shorthand (which I really like):

<foo ()="{ fooText }">
  {{ fooText }}
  <template (header)="{ msg }">
    {{ msg }}
  </template>
</foo>

<!-- or -->
<foo &="{ fooText }">
  {{ fooText }}
  <template &header="{ msg }">
    {{ msg }}
  </template>
</foo>

This can even apply to non scoped slots, effectively unifying slot and slot-props into a single syntax:

<foo>
  <div (header)>hello</div>
  <div (footer)>bye</div>
</foo>

<!-- or -->
<foo>
  <div &header>hello</div>
  <div &footer>bye</div>
</foo>
@LinusBorg

This comment has been minimized.

Copy link
Member

LinusBorg commented Jan 14, 2019

I was hesitant about the short notation, but when using it to set the slot name it's beginning to grow on me ...

...but as a sidenote it might confuse the hell out of people working on both Angular and Vue Projects - they use (eventName)="callback" for binding events, don't they?

@dimensi

This comment has been minimized.

Copy link

dimensi commented Jan 14, 2019

Yea, i see () syntax in angular, what why i call it not vue like syntax.
I want ask about my suggestion above.

<foo &="{ fooText }" &header="{ msg }">
  {{ fooText }} - {{ msg }}
  <header-block slot="header" :title="msg" />
  <bar &="baz" &side-bar="{ sideBarContent }">
     {{ baz }}
     <side-bar :content="sideBarContent" slot="side-bar" />
  </bar>
</foo>

As I understand it, we want to make the syntax clearer and more understandable, so that it is clear where this or that variable comes from. Pointing at the component itself all the variables from different slots at once, we immediately see what came from + this gives us the opportunity to get for rare cases the ability to mix variables from different scoped slots
I not api designer, maybe my idea have mistakes.

@yyx990803

This comment has been minimized.

Copy link
Member Author

yyx990803 commented Jan 14, 2019

@dimensi yeah I think having all slots declared on component root can be confusing.

BTW I moved this to the formal RFC process! vuejs/rfcs#2 - let's continue there.

@yyx990803 yyx990803 closed this Jan 14, 2019

2.6 automation moved this from In progress to Done Jan 14, 2019

@vuejs vuejs locked as resolved and limited conversation to collaborators Jan 15, 2019

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.