Skip to content

[TwigComponent]: Add the ability to spread attributes from the ... attribute #2923

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

Open
wants to merge 1 commit into
base: 2.x
Choose a base branch
from

Conversation

LexAgone
Copy link

[TwigComponent]: Add the ability to spread attributes from the ... attribute

Q A
Bug fix? no
New feature? yes
Docs? no
Issues
License MIT

As a front-end developer working primarily with Twig, I frequently create components that require nested attributes. However, there's currently no way to pass an array of attributes with a prefix.

For example, given a component:

<twig:Card>[...]</twig:Card>

I can currently write:

<twig:Card {{ ...myAttributes }}>[...]</twig:Card>

or:

<twig:Card attributes="{{ myAttributes }}">[...]</twig:Card>

But I can't do:

<twig:Card nested:{{ ...myAttributes }}>[...]</twig:Card>

or:

<twig:Card nested:attributes="{{ myAttributes }}">[...]</twig:Card>

That's why I'm proposing support for a new spread-like syntax using ... to allow passing an array of attributes (array, Traversable, or StimulusAttributes) with a prefix.

Real-world use case:

I have a Twig extension (App\Twig\Extension\ConfirmExtension) that provides a confirm() function returning a StimulusAttributes object.

I also have a component <twig:TableRow/> that contains a delete button.

Currently, I cannot write:

<twig:TableRow btn:{{ ...confirm() }} />

With this PR, my proposed syntax would allow the following:

<twig:TableRow btn:...="{{ confirm() }}" />

This improves flexibility and developer experience when working with prefixed dynamic attributes in Twig components.

@carsonbot carsonbot added Feature New Feature TwigComponent Status: Needs Review Needs to be reviewed labels Jul 14, 2025
Comment on lines -116 to -120
foreach ($data as $key => $value) {
if ($value instanceof \Stringable) {
$data[$key] = (string) $value;
}
}
Copy link
Member

Choose a reason for hiding this comment

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

why this removal ?

Copy link
Author

Choose a reason for hiding this comment

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

Because this loop force attributes to be strings if they are Stringable. However, StimulusAttributes implements Stringable so I can't pass recursively the object in the ComponentAttributes. Maybe I can exclude StimulusAttributes like this :
if (!$value instanceof \StimulusAttributes && $value instanceof \Stringable) { ?

@@ -103,6 +109,31 @@ public function __clone(): void
$this->rendered = [];
}

private function handleAttributes(array $attributes): array
{
$spreadAttributes = $attributes[self::SPREADABLE_ATTRIBUTE] ?? null;
Copy link
Member

Choose a reason for hiding this comment

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

This check should be done in constructor to avoid method call for everyone not using this feature

Copy link
Author

Choose a reason for hiding this comment

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

I didn't want to break the code guideline but ok, if it's allowed


unset($attributes[self::SPREADABLE_ATTRIBUTE]);

return [...$attributes, ...$spreadAttributes];
Copy link
Member

Choose a reason for hiding this comment

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

This is something that can be done userland, no ? Wether in PHP or Twig

Copy link
Author

Choose a reason for hiding this comment

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

What do you mean ? The interest is to be recursive. Currently, for the "primary" attributes it's not that usefull but for nested attributes, we need that

@smnandre
Copy link
Member

I'm not a bin fan of hard-coding something that uses a Twig construct (the ...) to create another behaviour.

<twig:TableRow btn:...="{{ confirm() }}" />

I'm very surprised we allow "..." as attribute name to be honest, and think this is something we should fix.

For your initial need, why don't you prefix your keys in your function ? Or pass it as prop instead as attribute ? Anything not working as expected here ?

@LexAgone
Copy link
Author

LexAgone commented Jul 15, 2025

I'm very surprised we allow "..." as attribute name to be honest, and think this is something we should fix.

My first thought was to use the "*" attribute but I had to edit the PreLexer to allow this character, but I saw the "." was allowed so I was like... Ok, why not...

For your initial need, why don't you prefix your keys in your function ? Or pass it as prop instead as attribute ? Anything not working as expected here ?

Here is my ConfirmExtension function :

readonly class ConfirmExtension
{
    public function __construct(
        #[Autowire(service: 'stimulus.helper')]
        private StimulusHelper $stimulusHelper,
    ) {
    }

    #[AsTwigFunction(name: 'confirm', isSafe: ['html_attr'])]
    public function getConfirm(
        string $question = 'Êtes-vous sûr de vouloir effectuer cette action ?',
        string $confirmLabel = 'Confirmer',
        string $cancelLabel = 'Annuler',
        string $title = 'Confirmation',
    ): StimulusAttributes {
        $attributes = $this->stimulusHelper->createStimulusAttributes();
        $attributes->addController('admin--confirm');
        $attributes->addAction(
            'admin--confirm',
            'showConfirm',
            'click',
            [
                'message' => $question,
                'confirm-label' => $confirmLabel,
                'cancel-label' => $cancelLabel,
                'title' => $title,
            ]
        );

        return $attributes;
    }
}

There is no way to pass prefix in the StimulusHelper. Maybe I could transform the StimulusAttributes to array to add prefix passed in props of the function and prefix all the keys... But It could be so much easier and convenient to be allowed to pass the object directly in the spreadable attribute. I have several exemples in my projects where It would be nice to have this.

And also, it's possible to have the functionality in the first instance of attributes, why not for the nested ones ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Feature New Feature Status: Needs Review Needs to be reviewed TwigComponent
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants