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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TwigComponent] new twig_component tag and slot #873

Closed

Conversation

WebMamba
Copy link
Contributor

@WebMamba WebMamba commented May 15, 2023

Q A
Bug fix? yes
New feature? yes
Tickets Fix #844
License MIT

Hey all!
Here is a proposal to add a new twig_component tag and a slot tag, as mentioned in this issue: #844.
The idea is to deprecate the component tag, in favor of the twig_component tag introduce in this PR.

What's gonna change?
Using blocks inside components ends up with issues and some limitations. So here instead of using blocks, we use slots. Slot is a new concept that only TwigComponent gonna care about. But for the rest, all the features stay untouched.
So let's illustrate with some example:

// src/Components/AlertComponent.php

use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;

#[AsTwigComponent('alert')]
class AlertComponent
{
    ...
}
// templates/components/Alert.html.twig

<div>
   <p>{{ slot }}</p>
</div>
// templates/index.html.twig

{% twig_component Alert %}
   You have a new message!
{% end_twig_component %}

As you can see everything you include in the twig_component tag can be retrieved in your component template by using the slot variable.

You can also use some custom slots by using the slot tag:

// templates/components/Alert.html.twig

<div>
   <p>{{ slot }}</p>
  {{ footer }}
</div>
// templates/index.html.twig

{% twig_component Alert %}
   You have a new message!
   {% slot footer %}
       <p>from Fabbot</p>
   {% endslot %}
{% end_twig_component %}

And the content inside the slot tag will be set into the footer variable.

This new approach came with a nice feature, the Anonymous component!!! 🎉
Let's imagine you have the following template:

   <p>Click here to sign up!</p>
   {% twig_component Button %}
        signUp
   {% end_twig_component %}

And the Button component does not exist, so Twig will search for a template:

  • templates/Button
  • templates/Button.html.twig
  • templates/components/Button
  • templates/components/Button.html.twig

if one of these templates exists Twig render it like an anonymous component.

Of course the goal is not to use this syntaxt directly but to use it with the new HTML syntax:

<p>Click here to sign up!</p>
   <twig:Button>
        signUp
   </twig:Button>

and if you want to use custom slot

<p>Click here to sign up!</p>
   <twig:Alert>
        You have a new message!
        <twig:slot name="footer">
             From fabbot
        </slot>
   </twig:Alert>

There is still some work to do:

  • Add tests
  • Integrate with the HTML syntax
  • Check we didn't use any feature from the component tag

But tell me what you think about it!
Thank you all! 😁

@kbond
Copy link
Member

kbond commented May 15, 2023

I haven't looked too deep yet but one initial question: does parent() work?

@weaverryan
Copy link
Member

I haven’t looked at the code yet, but yaaaaay!!! Is this a pretty close port of the other twig components library? Did the “slot attributes” also come along? Any complications or concerns so far?

Thank you 💯!

@WebMamba
Copy link
Contributor Author

does parent() work?

No, but is it really something we want we slot approach?

Is this a pretty close port of the other Twig components library?

Yes I started from the code: https://github.com/giorgiopogliani/twig-components and I tweak some parts to add our TwigComponent logic

Did the “slot attributes” also come along

Yes, I keep it. I think it's a cool feature

Any complications or concerns so far?

I just need to try that on a bigger project, I just made some try on some demo projects yet.

@kbond
Copy link
Member

kbond commented May 15, 2023

No, but is it really something we want we slot approach?

I'm... not sure... We have an example in the docs using it but it is contrived. (I think this particular example can be solved with slot attributes)

Copy link
Member

@Kocal Kocal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a check to prevent people to create a component named slot?

Copy link
Member

@weaverryan weaverryan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, let’s roll with this :). Tests and playing with this will likely uncover any issues.

public function metadataForTwigComponent(string $name): ?ComponentMetadata
{
if (!$config = $this->config[$name] ?? null) {
return null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You want to return null here now instead of an exception? Is that for the Antonio’ anonymous components?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes exactly! If we don't find a corresponding component then we try to render it as anonymous

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about the ability to distinguish attributes from properties for anon components as discussed here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I missed your comment. If you look at the test I used |default to reproduce the behavior we discussed. But maybe I can add this tag in a follow-up PR if this one gets merged.

src/TwigComponent/src/Twig/SlotNode.php Show resolved Hide resolved
@yguedidi
Copy link

Great work!
If I understood correctly, when using the new twig_component tag it will act like an include? and so be in compiled code as a component template class call?
Just curious, and clearly out of scope of this PR, but thinking about very small components ("atoms"), that could be used multiple times in a page, would it be possible to compile the templates using a component like the component code was written in them directly?
Kind of transforming the twig_component block to the corresponding Twig code of the component directly in the Twig code of the template using it, before it get compiled to PHP.
Wondering about multiple includes performance in case of lot of component usages, but maybe it's not an issue at all and PHP is fast enough? or not worth the complexity :)
Thank you

@weaverryan
Copy link
Member

Wondering about multiple includes performance in case of lot of component usages, but maybe it's not an issue at all and PHP is fast enough? or not worth the complexity :)

My answer is… I have no idea! It would be cool to setup some really complex pages and profile to find out if there are any bottlenecks. And sure, if it turns out those bottlenecks could best be alleviated with how the twig code is complied, that is possible to address (likely challenging, but possible).

@yguedidi
Copy link

Good to know that it's possible at least! Thanks

@WebMamba WebMamba force-pushed the matheo/twig_component_slot_approach branch 2 times, most recently from 1aa4129 to 59b8f78 Compare May 18, 2023 22:00
@WebMamba
Copy link
Contributor Author

WebMamba commented Jun 6, 2023

Hey all! In #863 @weaverryan suggest not choosing between slots and blocks and making these two concepts work together! So I made some changes in this PR and now you can use slot and block in your components! 😁 I have to confess it spin my head! I still need two make some more tests, but if you can still give your early thought. Thanks all!

@weaverryan
Copy link
Member

Hey all! In #863 @weaverryan suggest not choosing between slots and blocks and making these two concepts work together!

Wow, ok! 😍 . I will give all of the Twig component-related PRs a review tomorrow!

$compiler
->write('$context,[')
->write("'slot' => new ".ComponentSlot::class." (\$slot),\n")
->write("'attributes' => new ".AttributeBag::class.'(');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to conflict / run over the normal attributes variable inside of a Twig component.

In general, I was thinking that instead of the default slot becoming a slot variable and then named slots becoming their own local variables, WDYT about having a slots variable?

<div>
    {{ slots.content }}

    <div {{ slots.footer.attributes }}>
        {{ slots.footer }}
    </div>
</div>

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the slots variable is a good idea, I push a commit to add it 😁. And for the attributes, I will keep as it currently is. And use the original ComponentAttributes class for slot as well or a similar interface.

@weaverryan
Copy link
Member

@WebMamba could you do a rebase?

I'm still thinking about all this stuff, but current opinion is:

  • allowing blocks is probably still needed (so thanks for adding it here - though I still need to dig in deeper to see how it works).
  • inside of a <twig:FooBar> tag, probably we should have access to the FooBar component - e.g. via component or parent:
I am another template!

<twig:FooBar>
    {# the "this" variable would either be undefined, or refer to the component that this entire template is for, if we're already inside of a component template #}
    {{ component.methodOnFooBar() }}
</twig:FooBar>
  • Inside of a <twig:FooBar> tag, probably the other variables should remain the same: we should have access to the variables from the "host" template only. If you need access to something inside of the "child" component, you would use the component or parent variable.
Rendering {{ firstName }} from host template.

<twig:FooBar>
    The {{ firstName }} variable also exists here.
</twig:FooBar>

If you choose to override blocks inside of a template, then the rules change, but this is for more advanced use-cases.

Rendering {{ firstName }} from host template.

<twig:FooBar>
    {% block header %}
        Inside here, I would have access to whatever variables are inside of the `header` block inside `FooBar.html.twig` - that's how the current system works. The `this` variable is now `FooBar`
    {% endblock %}
</twig:FooBar>

I'm not sure how close we are to this yet... or if you or @sneakyvv will have some disagreements.

@weaverryan
Copy link
Member

Btw, about this PR, you test it a bit on the ux.symfony.com codebase for some realistic examples - that's where I spotted the attributes variable being overridden - any live demo page is a pretty good example - e.g. /live-component/demos/invoice

I also noticed the, if I put some default content, it appears to fill in both the content block and the slot? For example, in invoice.html.twig:

<twig:CodeWithExplanationRow filename="src/Twig/InvoiceCreator.php">
I am some content
</twig:CodeWithExplanationRow>

Then in CodeWithExplanationRow.html.twig:

I noticed that either of these work:

{% apply markdown_to_html %}
    {% block content %}{% endblock %}
    {{ slot }}
{% endapply %}

In fact, if you try that exactly, you'll see it's printed twice. Are we, perhaps, still wrapping {% block content %} around the content when using the HTML syntax? Also, if I try this:

{% twig_component CodeWithExplanationRow with {
    filename: 'src/Twig/InvoiceCreator.php'
} %}
    {% slot anything %}
        Some content in the anything slot!
    {% endslot %}
{% end_twig_component %}

I get:

A template that extends another one cannot include content outside Twig blocks. Did you forget to put the content inside a {% block %} tag?

I'm guessing my request to mix slots into the existing "embed" syntax is causing some complications. Let me know what you think - or if what I'm imagining is impossible / not feasible.

Thanks!

@WebMamba WebMamba force-pushed the matheo/twig_component_slot_approach branch from de2d297 to c6312ae Compare June 9, 2023 10:39
@WebMamba
Copy link
Contributor Author

WebMamba commented Jun 9, 2023

@WebMamba could you do a rebase?

Done 😁

I'm not sure how close we are to this yet... or if you or @sneakyvv will have some disagreements.

I don't know... Let me try things out this week-end and I will give you an answer. 😁 No worries, about that, I just like to try thing before making any decision 😎

it appears to fill in both the content block and the slot?

Yes, I made that on purpose. What do you think about it? Do you think we should only keep the default slot?

I'm guessing my request to mix slots into the existing "embed" syntax is causing some complications. Let me know what you think - or if what I'm imagining is impossible / not feasible.

Humm no that is the bug in the ComponentLexer, it should add all the slot and content in the default block

@WebMamba
Copy link
Contributor Author

WebMamba commented Jun 11, 2023

Hey @weaverryan! I simplify a lot this PR. So instead of adding a new tag, I used the component tag. I am focusing here on adding slot and anonymous components.
I fixed the issue about component Attribute and I am using the ComponentAttribute class as before.
So I think this PR is no anymore in conflict with any @sneakyvv PR's, and should work side by side with them 😁.

@weaverryan
Copy link
Member

Hey @WebMamba!

Sorry for the delay - this is a complex issue to get right! I'm interested to play with this PR and see how it feels, especially in comparison with using a lot of blocks.

I tried pulling this in on ux.symfony.com, but it bombs on the homepage:

  • A) We render {{ component('HomepageTerminalSwapper') }} on the homepage
  • B) In HomepageTerminalSwapper.html.twig, we render {% component Terminal ... %}
  • C) Finally, in Terminal.html.twig, the this variable appears to be an instance of HomepageTerminalSwapper instead of Terminal.

Is that a bug? Expected? Also, to clarify the scope, does this PR still attempt to make the "default" content inside of an embedded component - e.g. FooBar in {% component Hello %}FooBar{% endcomponent %} a slot instead of a block called content? When testing, it looks like leaving FooBar inside there always results in the A template that extends another one cannot include content outside Twig blocks. error.

Thanks!

@seb-jean
Copy link
Contributor

Is there an advantage to having this feature?

@WebMamba
Copy link
Contributor Author

I am closing this

@WebMamba WebMamba closed this Aug 18, 2023
@norkunas
Copy link
Contributor

norkunas commented Aug 18, 2023

I am closing this

😢 it's still a pain to work with twig:block and outerBlocks 😁
also #873 (comment) was very promising

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 this pull request may close these issues.

[TwigComponent] Impossible to declare higher level component content block
7 participants