Skip to content

Anonymous TwigComponent #802

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

Merged
merged 20 commits into from
Aug 7, 2023

Conversation

WebMamba
Copy link
Contributor

@WebMamba WebMamba commented Apr 17, 2023

Q A
Bug fix? no
New feature? yes
Tickets
License MIT

Hey all!
TwigComponent getting really cool: a custom syntax, embedded component, nested component, and so on. Why not extends all of this to anonymous components?
What I call an anonymous component is a simple template.
For example if you have the following template:

  // {% components/button/button.html.twig %}
  <button class="primary large actions">
    <span>
        {% block content %}
        {% endblock %}
    </span>
</button>

You can now use it like so:

  {% index.html.twig %}

 ...
<div class="cart-actions">
   <twig:Button:Button>
       Click Me!
   </twig:Button:Button>
</div>

As can see you can use : to navigate through the directories.
And this PR came also with twig_entension configuration key (default to .html.twig), to find your template with the extension you want. 😁

You now have the {% props %} tag. This tag allows you to define required props for a static component or to set a default value.

 // {% components/button/button.html.twig %}

{% props label, primary = true %}

  <button class="{{ primary ? = 'primary' ? 'secondary' }} large actions">
    <span>
        {{ label }}
    </span>
</button>

So here you have one require prop label, and one optional prop primary with a default value set to true. You can now use your anonymous component like so:

 {% index.html.twig %}

...
<div class="cart-actions">
  <twig:Button.Button label="Click me">
</div>

@kbond
Copy link
Member

kbond commented Apr 18, 2023

Love this! (attempted this in #283 a while back but now that we have html-syntax and default slots, it's much more useful).

What about component properties/attributes?

<twig:button.button class="foo" :username="user.name" :isAdmin>
    Click Me!
</twig:button.button>

@weaverryan
Copy link
Member

Ah, awesome! I'd really like to get this working too! To follow-up on Kevin's comment:

We need a way to differentiate a "variable" being passed to the template vs an attribute. Fortunately, we are not the first to face this problem ;) https://laravel.com/docs/10.x/blade#data-properties-attributes - so it may require some new tag on top of the component template - like {% prop type='info', message %} for a type prop that defaults to info and also a message prop with no default. I'm open to other syntax ideas! And can we get this working?

I'm also not sure, as a convention, to start mixing <twig:SuccessButton/> uppercase syntax with <twig:button.success> lowercase syntax. I can see how this differentiation could help you recognize an anonymous component from a class-backed component. But I'd like it to be easy to switch from an anonymous component to a class-backed one, without needing to go find everywhere you use that component in your templates and update the name.

@kbond
Copy link
Member

kbond commented Apr 18, 2023

I'm open to other syntax ideas! And can we get this working?

I like this syntax (if it's possible).

I'm also not sure, as a convention, to start mixing twig:SuccessButton/ uppercase syntax with twig:button.success lowercase syntax.

If I understand the new html syntax correctly, upper camel case is not required. <twig:SuccessButton/>, <twig:success-button/> and <twig:success_button/> are all valid (as long it matches your components name).

Assuming that's true, I don't know we can have a universal way to easily migrate from anon to non-anon. I feel like find-and-replace would be the best migration path.

I wonder if, to start, we should pass the full template?

<twig:path/to/button.html.twig type="info" class="mb-1"/>

Or maybe we can drop the .html.twig:

<twig:path/to/button type="info" class="mb-1"/>

@weaverryan
Copy link
Member

If we, as a standard, always used upper camel case - like twig:SuccessButton - even for anonymous components (so, the template would be SuccessButton.html.twig, then we’re good. Simply introducing a SuccessButton component class would be enough.

What likely wouldn’t be as clean are anonymous components that live in sub-directories.

@kbond
Copy link
Member

kbond commented Apr 18, 2023

I think this would still be possible with what I propose.

For <twig:{name}/>, the following chain could occur (first match wins):

  1. Check if {name} is a real component
  2. {name} exists as an absolute twig template
  3. {name}.html.twig exists as an absolute twig template
  4. components/{name} exists as twig template
  5. components/{name}.html.twig exists as twig template

@WebMamba
Copy link
Contributor Author

WebMamba commented Apr 19, 2023

so it may require some new tag on top of the component template - like {% prop type='info', message %} for a type prop that defaults to info and also a message prop with no default. I'm open to other syntax ideas!

I was thinking about something similar, but I think it's important to let the ability to type checking. So my proposal is something like so:

{% prop 'info' is 'string'  default 'info' %}
{% prop 'foo' is 'bool' default 'false' %}

But I'd like it to be easy to switch from an anonymous component to a class-backed one,

This is a really good point! Because in my use the front-end developer first is the template, and then a Symfony developer as the back-end logic. So I think we should let the ability to add the component class without touching the templates.

What likely wouldn’t be as clean are anonymous components that live in sub-directories

And why not also look at every subdirectory, and we just force to have every template with a different name under the components directory?

I think this would still be possible with what I propose.

Hum yes, this is interesting, I like this priority check idea!

By mixing your comments the solution could be:

first, imagine you have the following template: components/Button/Primary.html.twig

then in an other template you use like so:

   <twig:Button.Primary/>

and then if you create a new component class:

#[AsTwigComponent('Button.Primary')]
class ButtonPrimary
{
}

@weaverryan
Copy link
Member

I was thinking about something similar, but I think it's important to let the ability to type checking. So my proposal is something like so:

That syntax looks complex to me :). My opinion: if you are about types, use a class-backed component. There is nowhere else in Twig (e.g. include()) where there is type-safety when calling a template. Besides, we don't want to start seeing things like {% prop 'foo' is 'App\\Entity\\Foo' default 'null' %} ;)

So, I'd go back to my original proposal (well, I've modified to props with an s):

{% props type='info', message %}

For twig:{name}/, the following chain could occur (first match wins):

  1. Check if {name} is a real component
  2. {name} exists as an absolute twig template
  3. {name}.html.twig exists as an absolute twig template
  4. components/{name} exists as twig template
  5. components/{name}.html.twig exists as twig template

I might remove 2 & 3 (motivation: keep these anonymous components looking/feeling like everything else)? And is 4 really needed - I guess you'd have <twig:foo.html.twig>, which looks weird to me.

For 2 & 3, if we want to allow Twig components to live in other paths, probably we could add a different extension point for that - e.g. Blade components allow you to configure a "namespace" of sorts - e.g. a Dashboard namespace could be configured to look in templates/dashboard/ and then if you use <twig:dashboard:Panel>, that would look in templates/dashboard/Panel.html.twig. But... that's to think about later.

@kbond
Copy link
Member

kbond commented Apr 19, 2023

first, imagine you have the following template: components/Button/Primary.html.twig

I think we should keep the directory separator as / for now so there's no need for any string manipulation.

I might remove 2 & 3 (motivation: keep these anonymous components looking/feeling like everything else)? And is 4 really needed - I guess you'd have twig:foo.html.twig, which looks weird to me.

As long as (for an end-app), <twig:form/input/text /> resolves to components/form/input/text.html.twig I'm good but...

While it looks weird, I think this is the easiest way to allow including anon. templates from 3rd party bundles: <twig:@SomeBundle/form/input/text />

Is the performance of running the chain part of your objection? I do think we should put the best-practice/most-common scenarios highest on the chain.

@kbond
Copy link
Member

kbond commented Apr 19, 2023

(motivation: keep these anonymous components looking/feeling like everything else)

I think a lot of people, myself included, would want to use these as super-charged include tags and may not have any intention of using real components.

@weaverryan
Copy link
Member

Ok fair - we can keep 2 & 3 :)

@AmProsius
Copy link

I don't want to wake sleeping dogs, but why is the directory separator different from the template namespace?

That would mean an anonymous twig component would be written differently than a class component:

{# components/button/button.html.twig #}

<button class="btn btn-{{ variant }}">
    {% block content %}{% endblock %}
</button>

{# Anonymous component #}

<twig:button.button variant="primary">Click me</twig:button.button>
{# or #}
<twig:button/button variant="primary">Click me</twig:button/button>

{# Class component #}

<twig:button:button variant="primary">Click me</twig:button:button>

It would be great if the directory separators would be similar. That way, devs will be able to just remove the component class and the components would still work.
Plus it removes mental overload, because you don't have to remember when to use which separator.

@WebMamba
Copy link
Contributor Author

WebMamba commented May 5, 2023

Hey @AmProsius! Thanks for your comment! I think you are right and this is the direction we gonna took here! I will keep working on this PR this week! 😁 I was just focused on the bug we have with the new twig component syntax 😁 Cheers!

@seb-jean
Copy link
Contributor

seb-jean commented May 8, 2023

For this PR you could take help from https://laravel.com/docs/10.x/blade#anonymous-components 😄

@WebMamba
Copy link
Contributor Author

Hey all! I think we need to work first on what @weaverryan talks about in this comment #844 (comment) before continuing this PR 😁

@WebMamba WebMamba force-pushed the matheo/static_twig_component branch from fd2cfa1 to 04b4c30 Compare July 1, 2023 08:26
@WebMamba
Copy link
Contributor Author

WebMamba commented Jul 1, 2023

Hey all! I am back on this! I updated the first comment if you want some examples, but what I did is:

  • now you put your static components in template templates/, templates/.html.twig, templates/component/, templates/component/.html.twig
  • You have new props that let you define optional or require props for your component

Tell me what you think about this change! Cheers 🧡

@WebMamba WebMamba force-pushed the matheo/static_twig_component branch from 04b4c30 to 2bffe7c Compare July 1, 2023 08:46
Copy link
Member

@kbond kbond left a comment

Choose a reason for hiding this comment

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

Looking good, I'm excited! I'm happy you got the prop system working.

now you put your static components in template templates/, templates/.html.twig, templates/component/, templates/component/.html.twig

Does this mean you can use <twig:Form.Button.html.twig>?

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.

Getting very close now! I dropped some comments - but we also need documentation!

@@ -0,0 +1,5 @@
{% props label, primary = true %}

<button class="{{ primary ? 'primary' : 'secondary' }}">
Copy link
Member

Choose a reason for hiding this comment

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

We should use attributes here:

<button {{ attributes.defaults({ class: primary ? 'primary' : 'secondary' }) }}>

The reason is that, we need to test that anything that is a prop is NOT added as an attribute (and I think this may not be implemented yet?). So given this example:

<twig:Button label='Click me' :primary='false'/>

We need to assert that, even if we use {{ attributes }}, both label and primary don't show up as attributes.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hum, I am not sure we should have a difference between argument and prop. Why do you think we should differentiate theses two notions?

@WebMamba WebMamba force-pushed the matheo/static_twig_component branch from 13789f7 to b19c71a Compare July 20, 2023 14:35
@WebMamba
Copy link
Contributor Author

@weaverryan for the doc I plan to let a colleague do it, it's her first PR 😁

@kbond
Copy link
Member

kbond commented Jul 31, 2023

I think it's been decided that : is the dir separator? Do you mind updating the PR description?

/**
* @author Matheo Daninos <matheo.daninos@gmail.com>
*/
final class ComponentTemplateFinder implements ComponentTemplateFinderInterface
Copy link
Member

Choose a reason for hiding this comment

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

This feels a bit like a premature abstraction, what about in-lining in ComponentFactory for now?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@weaverryan ask me to do it in this comment: #802 (comment).
To not have to autowire the Environment service

Copy link
Member

Choose a reason for hiding this comment

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

Yea, it felt odd to inject ALL of Environment into ComponentFactory, which is otherwise unaware of Twig iirc

@WebMamba
Copy link
Contributor Author

WebMamba commented Aug 7, 2023

Hey all! I am back from holiday! And back to making this PR live 😎

@kbond PR description updated 👍

@weaverryan
Copy link
Member

Thank you for your hard work @WebMamba! Now, can you take a shot at writing the documentation?

@weaverryan weaverryan merged commit b1a46cd into symfony:2.x Aug 7, 2023
@WebMamba
Copy link
Contributor Author

WebMamba commented Aug 7, 2023

Woup Woup 🕺🏽 !! So happy to have this merge! 😁🎊🎉

@weaverryan I am on it 😎

@seb-jean
Copy link
Contributor

seb-jean commented Aug 8, 2023

Are arrays taken into account?

Example:

<twig:TabbedCodeBlocks :files="[
    'src/Twig/MealPlanner.php',
    'templates/components/MealPlanner.html.twig',
    'src/Form/MealPlannerForm.php',
]" />

I have an error
Error rendering "TabbedCodeBlocks" component: A "files" prop was passed when creating the "TabbedCodeBlocks" component. No matching files property or mount() argument was found, so we attempted to use this as an HTML attribute. But, the value is not a scalar (it's a array). Did you mean to pass this to your component or is there a typo on its name?

@seb-jean
Copy link
Contributor

seb-jean commented Aug 8, 2023

I tried to make a simple example

<twig:Button label='Click me' :primary='false'/>
{# templates/components/Button.html.twig #}
{% props label, primary = true %}

<button {{ attributes.defaults({class: primary ? 'primary' : 'secondary'}) }}>
    {{ label }}
</button>

The render is as follows but I have label="Click me" in button element. It’s too bad! Is there a way not to have the label present?

<button class="secondary" label="Click me">
    Click me
</button>

@WebMamba
Copy link
Contributor Author

WebMamba commented Aug 9, 2023

@seb-jean for the first issue you point out I open a fix: #1041

for the second issue the function attributes.only or attributes.without should do the fix.

@seb-jean
Copy link
Contributor

seb-jean commented Aug 9, 2023

thanks @WebMamba

weaverryan referenced this pull request Sep 7, 2023
This PR was squashed before being merged into the 2.x branch.

Discussion
----------

[Docs] Anonymous twig components

| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | no
| Tickets       | none
| License       | MIT

Add documentation for the recently added support of anonymous twig components [https://github.com/symfony/ux/pull/802](https://github.com/symfony/ux/pull/802)

Commits
-------

41f59f1 [Docs] Anonymous twig components
symfony-splitter referenced this pull request in symfony/ux-twig-component Sep 7, 2023
This PR was squashed before being merged into the 2.x branch.

Discussion
----------

[Docs] Anonymous twig components

| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | no
| Tickets       | none
| License       | MIT

Add documentation for the recently added support of anonymous twig components [https://github.com/symfony/ux/pull/802](https://github.com/symfony/ux/pull/802)

Commits
-------

41f59f18 [Docs] Anonymous twig components
@seb-jean
Copy link
Contributor

I would like to know if it is possible to have a dynamic component name.
See the code below:

{% set items = [
    {icon: 'Home'},
    {icon: 'MagnifyingGlass'},
    {icon: 'Bell'},
    {icon: 'Users'},
    {icon: 'Heart'},
] %}
{% for item in items %}
    <twig:Icon:{item.icon} class="h-6 w-6" />
{% endfor %}

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.

6 participants