symfony / ux

Symfony UX initiative: a JavaScript ecosystem for Symfony
https://ux.symfony.com/
MIT License
820 stars 298 forks source link

[TwigComponent] Overwriting component and template paths #563

Open tacman opened 1 year ago

tacman commented 1 year ago

I'm restructuring my theme bundle to accommodate swapping out specific stylesheets and javascript, and am not sure how to configure the twig components.

What I'm shooting for:

The goal is that free/open source projects can require sneat-bundle (which depends on bootstrap-bundle), and other projects that need another theme can do the same thing. Ideally, switching out themes would be as simply as uninstalling one and installing the other.

For example, imagine a CardComponent, which generates the HTML for https://getbootstrap.com/docs/5.0/components/card/ if bootstrap-bundle is installed, or fancier cards like https://demos.themeselection.com/sneat-bootstrap-html-admin-template-free/html/cards-basic.html if another theme is loaded. Obviously, the base component would take all the options available, and silently fail or display a warning if a particular feature wasn't implemented for that theme.

So the goal is that the developer (me) can simply

{% component card  with {title: 'my card title' %}
      {% block body %}
           The card body goes here.
     {% endblock %}
{% endcomponent %}

In theory this sounds doable, that it might be able to work the way symfony templates do, cascading through various directories until it finds the right component and template.

So in the example above, I'd like to first look for the component in velzon, my paid theme, and if it's not there, look in sneat, the free theme, and if it's not there, look in bootstrap, which will always been installed. Similarly, when looking for the card.html.twig template defined in the component

namespace Survos\BootstrapBundle\Components;
#[AsTwigComponent('card', template: '@SurvosBootstrap/components/card.html.twig')]
class CardComponent
{

I'd like to cascade the templates.

Perhaps this isn't the right approach, or isn't worth the complexity. But I figured I'd ask. Thanks

kbond commented 1 year ago

Hmm, possible to only enable the components in the container under certain conditions? (So only one say menu component is ever available)

tacman commented 1 year ago

yes, or prioritize the components, so if there are multiple ones. Or namespace them?

Just brainstorming.

Again, I love Twig Components. I've converted all my macros (except the knp_menu) and most of my non-trivial twig functions/filters to Twig Components, and can't imagine going back.

I do wish PHPStorm was aware of them though!

kbond commented 1 year ago

Your idea does sound cool (using without namespacing). In principal, it allows switching "themes" easily.

Does adding a priority work with service locators?

tacman commented 1 year ago

Does adding a priority work with service locators?

I still struggle with service locators, the compiler pass, etc.

On a related note (but I can open this in a new issue if that's appropriate), is there a way to implement a render call that bypasses the twig template altogether? I was playing around with rewriting the knp menu render twig template as a component:

https://github.com/KnpLabs/KnpMenu/blob/master/src/Knp/Menu/Resources/views/knp_menu.html.twig

Here's a block, for example.

{% block item %}
{% if item.displayed %}
{# building the class of the item #}
    {%- set classes = item.attribute('class') is not empty ? [item.attribute('class')] : [] %}
    {%- if matcher.isCurrent(item) %}
        {%- set classes = classes|merge([options.currentClass]) %}
    {%- elseif matcher.isAncestor(item, options.matchingDepth) %}
        {%- set classes = classes|merge([options.ancestorClass]) %}
    {%- endif %}
    {%- if item.actsLikeFirst %}
        {%- set classes = classes|merge([options.firstClass]) %}
    {%- endif %}
    {%- if item.actsLikeLast %}
        {%- set classes = classes|merge([options.lastClass]) %}
    {%- endif %}

Whenever I see lots of twig logic, I wonder if it can be a component, and leverage using PHP. In this particular template, there's a trivial about of HTML, some open and close list tags (ul, li, div, span), and LOTS of logic for maintaining attributes.

In this case, a component might not even be the right answer, probably a custom menu renderer would be correct, but I was curious if there's a way to override the render call. If so, that could also be a spot to look for the other twig templates.

Again, just brainstorming. In my particular project, I gave up on theme switching and copies all the components into the new bundle. Eventually, I clean up the demo repo and my bundle enough so I can share it, I'm really pleased with using components in it.

kbond commented 1 year ago

I still struggle with service locators, the compiler pass, etc.

Ok, I will have to play with this.

is there a way to implement a render call that bypasses the twig template altogether

At one point we played with the idea of self-rendering components. We didn't implement because we couldn't think of a valid use-case but maybe this is it?

tacman commented 1 year ago

@kbond , any changes that would allow a custom template to be passed in? It looks like the template is stored private $template property of Attribute and set in the constructor, but there's no setter. If there were, maybe I could pass the template in as a property and set it in preMount?

kbond commented 1 year ago

What about having a $template property on the component? Then in your component's configured template, do:

{{ include(template) }}

This could be injected into the component based on some kind of config or, if the property is public, using component('my_component', {template: 'custom_template.html.twig'})

tacman commented 1 year ago

hmm, interesting idea. I'll play around with that. Thanks.

WebMamba commented 1 year ago

I had a similar issue. I think it can be great to have the ability to set a template dynamically for a component. For example to have the ability to choose a template based on some configuration. What do you think about adding a function like withTemplate(): string in the Component class to be able to choose dynamically a template?

kbond commented 1 year ago

What do you think about adding a function like withTemplate(): string in the Component class to be able to choose dynamically a template?

What class would the method be on?

WebMamba commented 1 year ago

In the Component class

#[AsTwigComponent]
 class MyComponent
{
      public function __construct(
         // binded config
          string $templateBundlePath
      ) {}

      #[WithTemplate]
     public function getTemplate(): string
     {
          return $templateBundlePath . '/path/my_component.html.twig' 
     }
}
tacman commented 11 months ago

Revisiting this issue.

<twig:Alert theme='bootstrap5' type='danger'>Alert message</twig>
<twig:Alert theme='bootstrap4' type='danger'>Alert message</twig>
<twig:Alert theme='tailwind' type='warning'>Warning message</twig>

I think like the AlertComponent to render "templates/components/$theme/Alert.html.twig", rather than adding the theme logic to the single template, where I would be able to control the template to be rendered by setting a property.

As mentioned above, I could add a template property, but that moves a lot of logic to the call.

<twig:Alert template="@Mybundle/component/alert/tailwind.html.twig" type='warning'>Warning message</twig>

And realistically, the theme is going to be set somewhere else, injected into the component, then the component will figure out which template to use (like Symfony form themes).

Still just brainstorming. But I think dynamic templates would be useful at the component level, maybe something like the #[#WithTemplate] that @WebMamba suggests.

carsonbot commented 5 months ago

Thank you for this issue. There has not been a lot of activity here for a while. Has this been resolved?

carsonbot commented 4 months ago

Hello? This issue is about to be closed if nobody replies.

tacman commented 4 months ago

I still like the idea of dynamically defining the template name. Twig is great for rendering, but some things are complicated to do when adding conditional logic.