Skip to content
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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFC]: au-slot, a replacement for the replaceable-parts #900

Closed
Sayan751 opened this issue Jun 19, 2020 · 17 comments 路 Fixed by #916
Closed

[RFC]: au-slot, a replacement for the replaceable-parts #900

Sayan751 opened this issue Jun 19, 2020 · 17 comments 路 Fixed by #916
Milestone

Comments

@Sayan751
Copy link
Contributor

Sayan751 commented Jun 19, 2020

馃挰 RFC

This RFC proposes an alternative and enhanced syntax for replaceable parts. It necessitated from the need of emulating the native slots to allow CSS penetration. See #654, and #898. However, emulating slots based on global, and custom-element-level configuration brings its own set of complications in terms of templating, binding, and managing scopes. Apart from that, the idea of emulating slots, feels going towards opposite direction of Aurelia's spirit of keeping things closer to the native behavior.

With that spirit, this RFC proposes not to emulate slot at all (hear me out 馃槂) and rather provide an alternative syntax to support the actual use cases. From a practical standpoint, deciding whether a custom element is going to use slot/shadow DOM or not is a non-trivial decision, as the custom element needs to be styled accordingly. That is almost always a non-trivial task in a real-life application. Therefore, the assumption that a flag can be set to emulate or provide native behavior is bit impractical.

馃敠 Context

In Aurelia2 new syntax of au-slot is being proposed instead. This provides slot like behavior but with CSS penetration. From this perspective it is similar to replaceable-part syntax. In fact au-slot is proposed to be an enhancement over replaceable-part with the addition of controlling the binding scope. Moreover, it explicitly points out that devs are not using the native slot, and therefore there is no question of emulating the same.

馃捇 Examples

Example#1: Simplest case with any number of content from the custom element as well as an optional default and any number of named 'slot's.

my-element.html

<content-from-my-element></content-from-my-element> <!-- <- there are 0 or more of this -->
<au-slot>
    <!-- [default slot with optional fallback content] -->
</au-slot>
<au-slot name="s1">
    <!-- [named slot with optional fallback content] -->
</au-slot>

my-app.html

<my-element>
    this goes to default slot
    <div au-slot="s1">
        this goes to slot s1
    </div>
    <span> this goes to default slot as well </span>
    <div au-slot="s1">
        this goes to slot s1 as well
    </div>
</my-element>

This produces loosely the following output.

<my-element>
    <content-from-my-element></content-from-my-element>
    <au-slot>
        this goes to default slot
        <span> this goes to default slot as well </span>
    </au-slot>
    <au-slot name="s1">
        <div>
            this goes to slot s1
        </div>
        <div>
            this goes to slot s1 as well
        </div>
    </au-slot>
</my-element>

Example#2: Controlling binding scopes with $host.

my-element.ts

export class MyElement {
    public message: string = "John Doe";
}

my-element.html

<au-slot name="s1">
    Hello ${message}
</au-slot>
<au-slot name="s2">
    Hello ${message}
</au-slot>

my-app.ts

export class MyApp {
    public message: string = "Max Mustermann";
}

my-app.html

<my-element>
    <div au-slot="s1">
        Bye ${$host.message} <!-- grabs the inner scope with `$host` -->
    </div>
    <div au-slot="s2">
        Hi ${message}
    </div>
</my-element>
<my-element>
    <div au-slot="s1">
        Bye ${message}
    </div>
</my-element>

This produces loosely the following output.

<my-element>
    <au-slot name="s1">
        <div>
            Bye John Doe        <!-- Uses the scope of my-element instance, and does not traverse to parent scope as message is available there -->
        </div>
    </au-slot>
    <au-slot name="s2">
        <div>
            Hi Max Mustermann   <!-- Uses the scope of my-app instance -->
        </div>
    </au-slot>
</my-element>
<my-element>
    <au-slot name="s1">
        <div>
            Bye John Doe    
        </div>
    </au-slot>
    <au-slot name="s2">
        <div>
            Hello John Doe     <!-- Parent does not project to s2; fallback uses own scope-->
        </div>
    </au-slot>
</my-element>

Scoping rules in a nutshell:

  • IF parent projects to a slot
    • AND $host is used in the expression. the traversal starts with the inner scope.
    • ELSE ($host is not used) the parent scope is used for binding.
  • ELSE (the parent does not project to a particular slot) the fallback content uses the inner scope for binding.

Check this example out.

Example#3: Repeater

<!-- au-grid with @bindable items -->
<table>
    <thead>
        <tr>
            <au-slot name="header">
            </au-slot>
            <th>
                Actions
            </th>
        </tr>
    </thead>
    <tbody>
        <tr repeat.for="item of items">
            <au-slot name="content">
            </au-slot>
            <td>
                <button>edit</button>
                <button>delete</button>
            </td>
        </tr>
    </tbody>
</table>

<!-- Usage -->
<au-grid items.bind="items">
    <th au-slot="header">First Name</th>
    <td au-slot="content">${$host.item.FirstName}</td> <!-- $host lets us grab the item from repeater scope -->

    <th au-slot="header">Last Name</th>
    <td au-slot="content">${$host.item.LastName}</td>

    <th au-slot="header">Foo Bar</th>
    <td au-slot="content">
      <template if.bind="someConditionFromUsageVm">
          ${item.foo}
      </template>
      <template else>
        ${item.bar}
      </template>
    </td>
</au-grid>

馃搩 Summary

  1. The syntax is closer to the slot, as the replaceable does not support projecting multiple roots to a single slot. In case of the example#1, replaceable would have projected only <div>this goes to slot s1 as well</div>.
  2. With the new au-slot in place, the replaceable template controller and the replace parts can be removed altogether.
  3. Provides an alternative to slot, where devs can have CSS penetration as well as use the parent (outer) scope for binding. This is unlike the replaceable where the inner scope is used for binding, and the parent scope is only traversed if the property is not found in inner scope.

Let me know your feedback on this.

Edits

  • [2020-06-20]: Updated the scoping examples with $host and added the repeater example.
@HamedFathi
Copy link
Collaborator

HamedFathi commented Jun 19, 2020

@Sayan751

Very good, Just a question, Is it support conditional content projection with multiple slots?
Just like the issue and discussion here.

<button if.bind="someConditions">
    <au-slot></au-slot>
</button>
<a if.bind="someOtherConditions">
    <au-slot></au-slot>
</a>

Use case:

List group - Links and buttons

<div class="list-group">
  <a href="#" class="list-group-item list-group-item-action active">
    Cras justo odio
  </a>
  <a href="#" class="list-group-item list-group-item-action">Dapibus ac facilisis in</a>
  <a href="#" class="list-group-item list-group-item-action">Morbi leo risus</a>
  <a href="#" class="list-group-item list-group-item-action">Porta ac consectetur ac</a>
  <a href="#" class="list-group-item list-group-item-action disabled">Vestibulum at eros</a>
</div>

And

<div class="list-group">
  <button type="button" class="list-group-item list-group-item-action active">
    Cras justo odio
  </button>
  <button type="button" class="list-group-item list-group-item-action">Dapibus ac facilisis in</button>
  <button type="button" class="list-group-item list-group-item-action">Morbi leo risus</button>
  <button type="button" class="list-group-item list-group-item-action">Porta ac consectetur ac</button>
  <button type="button" class="list-group-item list-group-item-action" disabled>Vestibulum at eros</button>
</div>

@3cp
Copy link
Member

3cp commented Jun 19, 2020

I wonder if self-scoped feature is necessary. I could not think of a use case to use that feature, and the usage might be confusing unless user knew the inner implementation of the custom element.

It can be more readable if the attribute is at the place of using the custom element, instead of in the implementation of the custom element.

I changed the attribute to host-scoped since self-scoped is not suitable here.

<my-element>
    <div au-slot="s1" host-scoped>
        Bye ${message}
    </div>
    <div au-slot="s2">
        Hi ${message}
    </div>
</my-element>

@EisenbergEffect
Copy link
Contributor

One of the primary purposes of replaceable parts was to address the repeater scenario. In this case, the component developer has a repeat that is internal to the component, and provides a template for that repeat to generate from. They could then mark that repeat template as replaceable and then the consumer of the component could provide their own template, which would be injected into the component and used in place of its internal repeat template.

Can you talk about how this proposal addresses this use case?

@EisenbergEffect EisenbergEffect pinned this issue Jun 19, 2020
@Sayan751
Copy link
Contributor Author

support conditional content projection with multiple slots

@HamedFathi From the initial look, that use case can be supported. As far as I have understood that, there will be 2 different instructions, one for the if (template controller instruction) and another for the au-slot (hydration instruction). The 2nd one (probably) will be nested in the previous one. From this perspective that should work.

@3cp I was also inclined towards removing this explicit control over scoping altogether. In that case, the inner scope will be used iff parent has not projected to that slot. I think the slot also works that way, as anything injected via light DOM gets bound to the outer scope.

However, @EisenbergEffect has pointed out the repeater use case and in fact a prime use case of the self-scoped thing. As far as I see it this is how it might go with au-slot + self-scoped combination.

<!-- au-grid with @bindable items -->
<table>
    <thead>
        <tr>
            <au-slot name="header">
            </au-slot>
            <th>
                Actions
            </th>
        </tr>
    </thead>
    <tbody>
        <tr repeat.for="item of items">
            <au-slot self-scoped name="content">
            </au-slot>
            <td>
                <button>edit</button>
                <button>delete</button>
            </td>
        </tr>
    </tbody>
</table>

<!-- Usage -->
<au-grid items.bind="items">
    <th au-slot="header">First Name</th>
    <td au-slot="content">${item.FirstName}</td>

    <th au-slot="header">Last Name</th>
    <td au-slot="content">${item.LastName}</td>
</au-grid>

However, the original definition of the self-scoped is bit problematic in this case. Because by that definition, we cannot inject more complex templates such as

<td au-slot="content">
  <template if.bind="someConditionFromUsageVm">
    ${item.Prop1}
  </template>
  <template else>
    ${item.Prop2}
  </template>
</td>

And that is not a rare use case I would say. From this perspective, we ca define the self-scoped as "prioritize inner scope over outer scope". In every other case, if the parent has projected to slot, then use parent scope, else inner scope.

I can update the original post if we agree on this.

@3cp
Copy link
Member

3cp commented Jun 19, 2020

Love the repeater use case. Gonna use it.

@3cp
Copy link
Member

3cp commented Jun 19, 2020

I am think we might not need self-scoped to support this use case.

What about for au-slot to introduce an extra layer of overrideContext? Just like a normal template controller does.

User writes expressions to access their local things, but can access the host element's scope with context variable $host.item.firstName.

This can solve your dynamic use case. It also makes user code very explicit without ambiguity.

@3cp
Copy link
Member

3cp commented Jun 19, 2020

I got an interesting example.

<repeater-a>
  <let outer="$host"></let>
  <repeater-b>
    ${outer.item.foo} ${$host.item.bar}
  </repeater-b>
</repeater-a>

@Sayan751
Copy link
Contributor Author

I like that explicit nature of specifying the source of data.

I am unsure about how easy or difficult it is to introduce new keywords. Let's ask @fkleuver and @bigopon.

@fkleuver
Copy link
Member

I like it as well. A lot, actually.
It would be trivial to introduce a new keyword like that. It can sit right below the switch case for the parse path of $this and return a HostExpression. That new expression type would also give us the perfect place to put the specific logic for traveling up.

@3cp
Copy link
Member

3cp commented Jun 19, 2020

This thing has to a keyword in parser in au2 now?
I thought in au1, those $index $last were added freely by template controller on scope chain, which is beautiful.

@fkleuver
Copy link
Member

Nothing changed there.

$index, $last still sit as normal properties on the override context.

$this and $parent were also parser concerns in au1.

However, $host would not be just another "freely added" template controller thing. You might think something like this would work:
scope.overrideContext.$host = parentScope.bindingContext

It doesn't, because that gives you the root bindingContext that is the viewModel of the owning element. What you need is the immediate binding context of the declaring side (which might be multiple levels of synthetic binding contexts deeper, if there are template controllers in-between) and from there on travel further upwards up until the element boundary.

In addition to that, if slot content ends up being distributed into a slot that is itself also slot content for another slot, you need to skip over a scope boundary, etc, etc.
There are several tricky things to it that definitely warrant a hard-coded keyword in the parser for a new expression type to handle all this logic.

@3cp
Copy link
Member

3cp commented Jun 19, 2020

This feature should work in both au-slot and slot, right?

@Sayan751
Copy link
Contributor Author

Updated the RFC: the scoping examples with $host and added the repeater example.

@EisenbergEffect
Copy link
Contributor

I don't quite understand the repeat example. It seems like there will then be multiple instances of the "content" slot, which would seem invalid. It certainly is with native slots. Also, replaceable was designed to replace the entire template of a repeater/if/etc with a completely new template. This would mean replacing the entire tr tag with a different template. So, I'm not sure that is fulfilling the original use case yet.

@Sayan751
Copy link
Contributor Author

@EisenbergEffect You are right. When you compare this with slots then that example is not valid in context of repeater, as that would mean the same slot is being defined multiple times. However, I was hoping that au-slot will support the replace-parts use-cases as well. This is also the reason, I am not inclined to refer this as slot emulator. We can discuss whether or not that is useful.

BTW a very similar variant of the repeater example, I gave earlier, works in Au1. For my particular use case, I need the partial replacement of the tr, and it works. I think that's not a rare use-case. Having said that, it does not limit devs to use the au-slot in "complete" replacement context.

@fkleuver
Copy link
Member

fkleuver commented Jun 20, 2020

This is also the reason, I am not inclined to refer this as slot emulator.

That is kind of the goal though, in order to become less confusing. Maybe that's not what we want to name it per se, but it should be able to work as native slots do.
All we need for that, is for <au-slot> to "disappear", but we can easily do that by making it containerless. It could then be used where you would otherwise use a <template>. If you need to place it on another element like a template controller, you could use as-element (just need to make sure we drop out of containerless mode in that case.. we probably want to do that anyway)

To quote an example written by @davismj in 2016 here: http://www.foursails.co/blog/semantic-template-parts/

v1:

<template class="ui accordion">
  <template repeat.for="item of items" part="item-template">
    <div class="title" part="title-template" replaceable>
      <i class="dropdown icon"></i>
      ${item.title}
    </div>
    <div class="content">
      <template part="content-template" replaceable>
        <p>${item.content}</p>
      </template>
    </div>
  </template>
</template>

v2:

<template class="ui accordion">
  <au-slot repeat.for="item of items" name="item-template">
    <div class="title" name="title-template" as-element="au-slot">
      <i class="dropdown icon"></i>
      ${item.title}
    </div>

    <div class="content">
      <au-slot name="content-template">
        <p>${item.content}</p>
      </au-slot>
    </div>
  </au-slot>
</template>

As a side note specifically on the topic of the table elements, tr cannot have children other than td / th (so au-slot would be ejected to the nearest non-table parent). It doesn't change anything about the principles discussed w.r.t. the functionalities, but just to avoid confusion to folks who read this RFC as a potential example to follow.

@Sayan751
Copy link
Contributor Author

Although that is a valid example, I think making the repeater root replaceable is a very rare use case. For a reusable component, I don't see one want to replace the repeater root. One use-case might be that a 'list' CE is written with a table layout, but during use a ul+li template is projected to root. However, that's a confusing use-case in itself. In such cases, having a second CE makes more sense.

Having said that, the example shows an interesting use-case.

@fkleuver fkleuver added this to the v2.0-beta milestone Jul 17, 2020
fkleuver added a commit that referenced this issue Oct 3, 2020
@Sayan751 Sayan751 unpinned this issue Oct 15, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants