ux icon indicating copy to clipboard operation
ux copied to clipboard

[TwigComponent] Easier composability of components.

Open veewee opened this issue 3 years ago • 14 comments

We're building a large set of twig components. Currently, it is possible to pass the attributes from one component into another one. This works, as long as the attributes in the parent component are no real properties inside the twig component PHP class:

Use-case:

#[AsTwigComponent('button')]
class Button {
    public string $type = 'submit';
}

#[AsTwigComponent('primary-button')]
class PrimaryButton extends Button {

}

Now the template of the Primary button could look like this:

{% set children = block('children') %}
{% component 'button' with attributes.defaults({
    type: type,
    class: 'primary',
}).all() %}
    {% block children %}{{ children|raw }}{% endblock %}
{% endcomponent %}

And button:

<button type="{{type}}" {{attributes.defaults({class: 'button'})}}>
    {% block children %}{% endblock %}
</button>

This works, but we had to do some strange things in order to make it work:

  • Primary-button needs to pass down all public properties manually. It would be nice if there was a way to just pass all attributes - even if they are placed in properties.
    • See twig of primary button above: type: type,
  • Primary-button needs to serialize to all(). It would be nice if the component took both an array and the Twig Attributes model. For the example above, this is still managable, but you might have 10 props or more in some places.
  • passing down blocks from parent into children is a bit cumbersome as well. Not sure if this is fixable
    • See twig component of primary button above: {% set children = block('children') %}

It would be nice if we could improve DX a bit here.

veewee avatar Dec 02 '22 15:12 veewee

I would really love to make this "just work" and work awesomely :). Do you have any ideas on how you would like the final code to look? To give a better impression, I'd need to do some research into composition on existing systems - components are not new, so build on the ideas of giants :).

weaverryan avatar Dec 02 '22 16:12 weaverryan

Well ... I do love the way react handles this ... :)

interface ButtonProps {   /* Not to mention extends ButtonHTMLAttributes<HTMLButtonElement> */
    readonly type : string;
    readonly chlildren: ReactNode;
}

const PrimaryButton = (props : ButtonProps) => (
    <Button className="primary" {...props} />
)

const Button = (props : ButtonProps) => (
    <button  {...props} />
)

But twig sure ain't no react.

A few possibilities which are more in line with twig maybe:

all() problem

This should be the easiest one to fix: support both array and the twig attributes class as parameter.

Passing down props and blocks

Here are a few ideas

A dedicated syntax

{% component 'button' from this %}
 {# this would give the possibility to override blocks. If you do not override a block, it falls back to the one in the child #}
 {% block children %}This one is overridden but can {{ parent() }}{% endblock children %}
{% endcomponent %}

I'm not sure about the this in this syntax. it feels natural and might be possible to compose components from other components. E.g.:

{% set peerComponent = component('peer', peerAttrs) %}
{% component 'button' from peerComponent %}{% endcomponent %}

a macro or twig function

{{ composeComponent('button', this) }}

I'm not sure how far you can go with this. Passing down the props sure is possible, but I am not sure at this moment how twig blocks work internally and if you can pass them down like this. This would pretty much work like a Higher order component in react that reconstructs this (the parent component) back into attributes and passes them down to the child button component

These are just brainfarts at the moment. I can try to think of a few other possibilities if you like. I'm a bit busier the coming week, but maybe I can also try to create a POC out of this if I can find some time.

WDYT?

veewee avatar Dec 02 '22 18:12 veewee

all() problem

This should be the easiest one to fix: support both array and the twig attributes class as parameter.

That seems reasonable. To repeat what you're referring to for later clarity, you would want the following to be allowed:

{% component 'button' with attributes.defaults({
    type: type,
    class: 'primary',
}) %}

Currently, you would need to add a .all() on the end to convert the ComponentAttributes into an array.

Passing down props and blocks

This is super helpful. But it'll require some time to bend my mind around it a bit and think what might be possible @kbond might have some thoughts about what might be possible - he did the work for the current {% component syntax, which deals with blocks.

weaverryan avatar Dec 05 '22 14:12 weaverryan

Is this a bit related to #542?

While I did work on the {% component syntax, I don't have a great knowledge of twig internals. I copied most of the code from Twig's {% embed tag and https://github.com/giorgiopogliani/twig-components.

kbond avatar Dec 05 '22 16:12 kbond

Hello @kbond

I wouldnt say those 2 are very related. Only this would be easier with the PR linked to that issue:

{% set mountedComponent = mount_component('mount_for_change') %}
{% do mountedComponent.setName('Mr. Smith') %}

{% component 'button' from mountedComponent %}{% endcomponent %}

I think the use-case for mount_component is more as in the example of that specific issue. Eg you have a table and want to tell how table cells should be rendered, but want to pass in the data on the moment you render that component.

veewee avatar Dec 05 '22 17:12 veewee

This should be the easiest one to fix: support both array and the twig attributes class as parameter.

Agreed (but I'll have to dig into twig internals), couple questions:

  1. If passing attributes directly, what should the parameter name be in the child component? It will already have it's own attributes parameter, right? Or are you thinking this should override it?
  2. Would you want to pass additional parameters down in addition to attributes?

kbond avatar Dec 05 '22 18:12 kbond

Related to this issue it would be super cool if we could avoid using {% block %} without multiple slots :) Because its cumbersome to write {% component Super %} {% block ... %} {% endblock %} {% endcomponent %}

norkunas avatar Dec 05 '22 19:12 norkunas

I know... I'd love this as well but it's pretty advanced stuff. https://github.com/giorgiopogliani/twig-components has this but I couldn't figure out how to port that code into this library.

kbond avatar Dec 05 '22 19:12 kbond

Yup, I would LOVE that syntax, in general, with twig components... 😇

weaverryan avatar Dec 05 '22 21:12 weaverryan

1. If passing attributes directly, what should the parameter name be in the child component? It will already have it's own `attributes` parameter, right? Or are you thinking this should override it?

Good question. I don't know... I'dd say: merge(attributes, this.publicProps) and pass that down to the attributes of the child component?

2. Would you want to pass additional parameters down _in addition_ to attributes?

If you choose between component with and component from, then I'dd say that if you want to explicitely specify attributes, you'dd use the with form. If you want to copy it all you use from? I don't see a lot of value in adding other attributes atm. You can always use parent's attributes.with().without() as well.

Yeah, maybe blade's slots principle is better in this specific situation than the block system... I don't mind the twig syntax that much, but the HTML-ish version would indeed be super nice indeed! However, it might complicate things like passing down multiple attributes at once etc.

veewee avatar Dec 06 '22 07:12 veewee

The idea of an default block was proposed for twig's embed tag - looks like it is complex!

kbond avatar Dec 06 '22 14:12 kbond

Thank you for this suggestion. There has not been a lot of activity here for a while. Would you still like to see this feature?

carsonbot avatar Apr 25 '24 12:04 carsonbot

Friendly ping? Should this still be open? I will close if I don't hear anything.

carsonbot avatar May 11 '24 07:05 carsonbot

Hey,

I didn't hear anything so I'm going to close it. Feel free to comment if this is still relevant, I can always reopen!

carsonbot avatar May 25 '24 14:05 carsonbot

Thank you for this suggestion. There has not been a lot of activity here for a while. Would you still like to see this feature? Every feature is developed by the community. Perhaps someone would like to try? You can read how to contribute to get started.

carsonbot avatar Nov 26 '24 12:11 carsonbot

Could I get an answer? If I do not hear anything I will assume this issue is resolved or abandoned. Please get back to me <3

carsonbot avatar Dec 10 '24 12:12 carsonbot

Hey,

I didn't hear anything so I'm going to close it. Feel free to comment if this is still relevant, I can always reopen!

carsonbot avatar Dec 24 '24 12:12 carsonbot

Thank you for this suggestion. There has not been a lot of activity here for a while. Would you still like to see this feature? Every feature is developed by the community. Perhaps someone would like to try? You can read how to contribute to get started.

carsonbot avatar Jul 06 '25 12:07 carsonbot