Skip to content

[Twig] Allow nested attributes #1405

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 1 commit into from
Mar 23, 2024
Merged

Conversation

kbond
Copy link
Member

@kbond kbond commented Jan 18, 2024

Q A
Bug fix? no
New feature? yes
Issues Ref #1047 (possible solution)
License MIT

This PR adds a syntax for nested attributes: attributes that aren't meant to be used on the root element but one of its descendants.

Here's an example:

{# MyDialog #}
<div{{ attributes }}>
    <div{{ attributes.nested('title') }}>
        {% block title %}Default Title{% endblock %}
    </div>
    <div{{ attributes.nested('body') }}>
        {% block content %}{% endblock %}
    </div>
    <div{{ attributes.nested('footer') }}>
        {% block title %}Default Footer{% endblock %}
    </div>
</div>

{# render #}
<twig:MyDialog class="foo" body:class="bar" footer:class="baz">
   Some content
</twig:MyDialog>

{# output #}
<div class="foo">
    <div>
        Default Title
    </div>
    <div class="bar">
        Some content
    </div>
    <div class="baz">
        Default Footer
    </div>
</div>

The nesting is recursive so you could potentially do something like this:

<twig:Form
    :form="form"
    class="ui-form"
    row:class="ui-form-row"
    row:label:class="ui-form-label"
    row:widget:class="ui-form-widget"
/>

@kbond
Copy link
Member Author

kbond commented Jan 18, 2024

I'm still thinking about how we could pass nested attributes down to nested components:

Something like:

{# Form #}
<form{{ attributes }}>
    <twig:Input :_attributes="attributes.nested('input')" />

    <!-- or -->

    <twig:Input ...attributes.nested('input') />
</form>

@smnandre
Copy link
Member

Oh that's a really nice feature ! 👏

Does it work with live components ? (please say no hahaha)

@kbond
Copy link
Member Author

kbond commented Jan 18, 2024

Does it work with live components ?

I think it would actually, only the rendering of the attributes takes the "nested" syntax into account.

@kbond
Copy link
Member Author

kbond commented Jan 18, 2024

I'm still thinking about how we could pass nested attributes down to nested components:

Solved!

<twig:Input {{ ...attributes.nested('input') }} />

@smnandre
Copy link
Member

Little question just to be curious, nothing about "right now" ;)

Would this be a lot of work to handle (same thing but in "json" notation instead of "html") ?

<twig:Form
    :form="form"
    class="ui-form"
    :row:="{class: "ui-form-row", label: .., widget: .{class: ......}"
/>

@kbond kbond force-pushed the feat/twig-nested-attributes branch from 35e65c7 to 57dc323 Compare January 18, 2024 23:05
@kbond
Copy link
Member Author

kbond commented Jan 18, 2024

Little question just to be curious, nothing about "right now" ;)

I think the current syntax looks cleaner (just standard html attributes).

@kbond kbond force-pushed the feat/twig-nested-attributes branch from 57dc323 to 4bda8ed Compare January 18, 2024 23:15
@kbond
Copy link
Member Author

kbond commented Jan 18, 2024

Regarding the CI failure, it's because I'm using the twig spread operator in a test. I either need to bump the min twig version or skip this test if not supported. Thoughts?

@smnandre
Copy link
Member

Skip for now ? We will probably drop Twig < 3.something when Twig 4.0 is released right ?

@kbond kbond force-pushed the feat/twig-nested-attributes branch 2 times, most recently from cdcb87f to 84839d2 Compare January 19, 2024 16:07
@weaverryan
Copy link
Member

I've wondered about how to do this a bunch of times 😅. I had imagined it working via blocks someday:

<twig:MyDialog class="foo">
    <twig:block name="footer" class="baz">Footer</twig:block>
</twig:MyDialog>

That's a bit inspired by Blade, because our blocks == Blade slots, and they have slot attributes - https://laravel.com/docs/10.x/blade#slot-attributes

However, 3 issues I see are:

A) What if I wanted to define a class on the footer but not override the content? <twig:block name="footer" class="baz" />? And is that enough to differentiate between "I do not want to override this block content" vs other cases where "I want to override this block content to an empty string?"'.

B) Do we want to require a block in order to render an element with "nested" attributes?

C) The more obvious issue is: blocks are a core concept... so this may not even be possible unless we modify Twig itself. Though, I had imagined that we might cheat by doing a translation:

<twig:MyDialog class="foo">
    <twig:block name="footer" class="baz">Footer</twig:block>
</twig:MyDialog>

becomes (line breaks added for clarity)

{% component 'MyDialog' with { class: 'foo' } %}
    {% block footer %}
        {% do attributes.nested('footer', { class: 'baz' }) %}
        Footer
    {% endblock %}
{% endblock %}

The usage would be:

{# Modal.html.twig #}
<div{{ attributes }}>
    {% block footer %}
    <div{{ attributes.nested('footer') }}>
        {% block footer %}Default Footer{% endblock %}
    </div>
</div>

Not sure this is better, tbh. But I wanted to throw out this alternate universe before we decide on a syntax.

@smnandre
Copy link
Member

We could totally use a intermediary tag like "slot"

@kbond
Copy link
Member Author

kbond commented Jan 19, 2024

We could totally use a intermediary tag like "slot"

Wasn't the issue with this: we won't have the benefits of blocks? I think this was the reason we ended up abandoning a new slot type.

But I wanted to throw out this alternate universe before we decide on a syntax.

The block attributes system is interesting but it wouldn't solve the initial reason for me creating this PR: passing nested attributes to subcomponents.

What I like about this implementation is it doesn't require any new twig fanciness.

@smnandre
Copy link
Member

Wasn't the issue with this: we won't have the benefits of blocks? I think this was the reason we ended up abandoning a new slot type.

Was'nt this before the HTML syntax ? I feel we have learned a lot about "hacking" things into Twig since, maybe there is more things "possible" than before ?

Did you remember what specifically in the "benefits of blocks" was laking ? Or list some of those you count more on ?

@kbond
Copy link
Member Author

kbond commented Jan 19, 2024

Was'nt this before the HTML syntax ?

Possibly, I can't quite remember.

Did you remember what specifically in the "benefits of blocks" was laking ? Or list some of those you count more on ?

I just remember Ryan and I realizing how powerful the out of the box block system was and all the benefits we'd be missing by doing our own thing.

@weaverryan
Copy link
Member

I just remember Ryan and I realizing how powerful the out of the box block system was and all the benefits we'd be missing by doing our own thing.

Yup, the block system is massively powerful and we should keep using it.

I'm leaning towards "let's merge this & we could always deprecate it later). But <twig:Foo body:class="bar" isn't valid HTML... and so it sucks to land on a solution that's going to look wrong to editors (my editor also yells about <twig:Foo, but that is just because it's unaware of this twig namespace: it IS valid HTML).

The block attributes system is interesting but it wouldn't solve the initial reason for me creating this PR: #1405 (comment).

<twig:Input {{ ...attributes.nested('input') }} />

That problem occurred to me also:

A) What if I wanted to define a class on the footer but not override the content? <twig:block name="footer" class="baz" />? And is that enough to differentiate between "I do not want to override this block content" vs other cases where "I want to override this block content to an empty string?"'.

It could possibly work via:

<twig:Form>
    <twig:block name="widget" class="ui-form-widget" />
</twig:Form>

It would require defining a self-closing block tag as meaning "Do not actually override this block" vs "Override with null". The above would result in:

{% component 'Form' %}
    {% do attributes.nested('widget', { class: 'ui-form-widget' }) %}
{% endcomponent %}

Inside Form.html.twig, you'd have:

{% block widget %}<twig:Input {{ ...attributes.nested('widget') }} />{% endblock %}

On the one hand, this feels much crazier than your solution. On the other hand, this is a hard problem because it's not solved in Twig. And that's what we're trying to do here - ideally with the best API possible.

@weaverryan weaverryan added Status: Waiting Feedback Needs feedback from the author Feature New Feature labels Jan 29, 2024
weaverryan added a commit that referenced this pull request Jan 29, 2024
… (kbond)

This PR was merged into the 2.x branch.

Discussion
----------

[Twig] Make `ComponentAttributes` traversable/countable

| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | yes
| Issues        | n/a
| License       | MIT

This will help with #1405 and #1414.

Commits
-------

8040d9c feat(twig): make `ComponentAttributes` traversable/countable
@kbond
Copy link
Member Author

kbond commented Jan 29, 2024

But <twig:Foo body:class="bar" isn't valid HTML...

Do you mean body:class="bar" here? Isn't :class="bar" just as invalid?

@kbond kbond force-pushed the feat/twig-nested-attributes branch from 84839d2 to 534b9f5 Compare January 29, 2024 16:26
@weaverryan
Copy link
Member

Do you mean body:class="bar" here? Isn't :class="bar" just as invalid?

Fair point, but at least in the eyes of PhpStorm, :class is ok. This may be the Symfony plugin at work (I have it disabled on this project, but still possible), but :class is at least a known syntax from other languages.

Screenshot 2024-01-30 at 8 39 51 AM

@smnandre
Copy link
Member

:class="bar" may be invalid.... But on custom elements, class:="bar" is not ;)

<!DOCTYPE html>
<html>
<head>
<title>Hello</title>
</head>
<body>
<twig-bar class:="foo"></twig-bar>
</body>
</html>

This is perfectly valid HTML code ;)

@kbond
Copy link
Member Author

kbond commented Jan 30, 2024

For me, it looks like PhpStorm marks attributes with : in their name as invalid for standard html elements but not "custom components"

image

class autocompletion only appears to work for class (not :class or widget:class) which is a bit of a bummer.

Edit:

The official tailwindcss plugin allows adding custom class names (I can add "widget:class" to the "classAttributes" config and it works). There is an experimental regex option that I think we could make work for *:class but I haven't figured that out.

image

@smnandre
Copy link
Member

I think all this can be handled with IDE, syntax rules, Symfony plugin... and i personnaly prefer a syntax non final (different from the HTML produced)

The only question should be: what is at risk if something not intended to is displayed in HTML.
And for that we could also "just" add some Listener (or a ux:component:lint) to check final output.

--

In a (not-so) distant future, i'm more and more convinced by using "custom web components" syntax instead of our current "grammar-free custom XML namespace") ....

That would allow us reserved attribute, custom syntax rules, templates,... and would offer incredible possibilities in term of lazy load too .... while beeing entirely supported in HTML/Twig editors

@kbond kbond force-pushed the feat/twig-nested-attributes branch from 534b9f5 to 2b699a9 Compare March 22, 2024 20:59
@kbond kbond force-pushed the feat/twig-nested-attributes branch from 2b699a9 to 8ee104a Compare March 22, 2024 21:10
@kbond kbond force-pushed the feat/twig-nested-attributes branch from 8ee104a to e5b3896 Compare March 22, 2024 21:47
Copy link
Member

@smnandre smnandre left a comment

Choose a reason for hiding this comment

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

I approve for you to merge :) But still nitpicking doc things we can send later

@carsonbot carsonbot added the Status: Reviewed Has been reviewed by a maintainer label Mar 22, 2024
Comment on lines +1068 to +1072
You can have attributes that aren't meant to be used on the *root* element
but one of its *descendants*. This is useful for, say, a dialog component where
you want to allow customizing the attributes of the dialog's content, title,
and footer. Here's an example of this:

Copy link
Member

Choose a reason for hiding this comment

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

You can pass attributes from the outside directly to the descendants of a component. This approach is especially useful for components like dialogs, to configure the rendering of inner parts like content, title, and footer. Here's an example:

</div>
</div>

The nesting is recursive so you could potentially do something like this:
Copy link
Member

Choose a reason for hiding this comment

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

I'm on very small details but this is not really recursive and more a tree here (in the meaning a parent call child, but never call itself back)

This paragraph and your following example make me want to do two small TwigComponent demos

  • one litterally recursive where you pass to the child a level max and something a bit dynamic (oh i think i have n idea, poke me if your'e around) and the recursion goes until the level is reached
  • a form demo with a cool theme, just like you did.

WDYT ?

(and we need to start the TwigComponent demo)

@@ -19,8 +19,10 @@
*
* @immutable
*/
final class ComponentAttributes implements \IteratorAggregate, \Countable
final class ComponentAttributes implements \Stringable, \IteratorAggregate, \Countable
Copy link
Member

Choose a reason for hiding this comment

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

<3

Copy link
Member

@smnandre smnandre left a comment

Choose a reason for hiding this comment

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

(Later on, that'be nice if we could add some more tests on this.. to ensure non-regressive later PR, and maybe one or two on "non-happy-path". I'd be glad to help if you want)

:)

Copy link
Contributor

@WebMamba WebMamba left a comment

Choose a reason for hiding this comment

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

LGTM! Thanks @kbond!

@kbond kbond merged commit ede48bc into symfony:2.x Mar 23, 2024
@kbond kbond deleted the feat/twig-nested-attributes branch March 23, 2024 14:39
@kbond
Copy link
Member Author

kbond commented Mar 23, 2024

Thanks! @smnandre, when time permits, mind making a followup PR with some of the doc changes/tests you'd like?

@smnandre
Copy link
Member

Will do in April... but won't mind a bit of help on this :))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Feature New Feature Status: Reviewed Has been reviewed by a maintainer Status: Waiting Feedback Needs feedback from the author
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants