Skip to content

[TwigComponent] Support passing blocks to nested embedded components #920

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
Jun 23, 2023

Conversation

sneakyvv
Copy link
Contributor

@sneakyvv sneakyvv commented Jun 2, 2023

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

The fix for #844, and a better solution then a manual passthrough workaround, is literally just passing the blocks from the "host Template" to the embedded Template, so that the blocks are merged with the embedded template's blocks, overriding them but altering their name and using a special outerBloocks variable to map the block name to the new name (see #920 (comment)).

This means that the example from #844 (comment) (tweaked a bit)

{# anywhere #}
{% set name = 'Fabien' %}
<twig:DivComponent>
    Hello {{ name }}!
</twig:DivComponent>
{# DivComponent.html.twig #}
<twig:GenericElement element="div" class="divComponent">
    {{ block(outerBlocks.content) }}
</twig:GenericElement>
{# GenericElement.html.twig #}
<{{ element }}{{ attributes }}>
  {%- block content -%}{%- endblock -%}
</{{ element }}>

Results in

<div class="divComponent">Hello Fabien!</div>

See the tests for more elaborate examples showing the access to context variables, multi-level overrides, etc.

@sneakyvv
Copy link
Contributor Author

sneakyvv commented Jun 2, 2023

Note that I had some tests locally to address the ability to access the host/parent component from within an (nested) embedded component, but since that depends on #863 I didn't add them in this PR.
I'll add those tests either in that PR or in this one, depending on which one might get merged first.

@sneakyvv sneakyvv changed the title Support passing blocks to nested embedded components [TwigComponent] Support passing blocks to nested embedded components Jun 2, 2023
@sneakyvv
Copy link
Contributor Author

sneakyvv commented Jun 2, 2023

To fix the failing tests #918 should be merged. Done, and rebased

@sneakyvv
Copy link
Contributor Author

sneakyvv commented Jun 8, 2023

For the record: as discussed on Slack, this PR currently can cause a recursive loop when a block (for example content) for a component has a(nother) component which also has a content block. The last component will be used in the actual content block for the first component. Then when the latter component is rendered it also will be using that same content definition from the first component. So it will render that first content block which renders the last's content blocks which refers again to the first and so on...

I have a fix for this ready, and will update the PR soon after I've updated/extended the tests.

@sneakyvv
Copy link
Contributor Author

I pushed an improved mechanism to pass blocks down to nested components.

As said in my previous comment, just passing along blocks to the embedded templates will quickly end up in recursive loops, not using the correct blocks etc.

Now, blocks are still passed along but their name is being randomized.
A BlockStack object is introduced which keeps track of the location of a block definition, the relation to each embedded component, and that randomized name, so that when the component instance is rendered, the correct block definition can be accessed via the special outerBlocks variable (which refers to the BlockStack).

The example from the description now becomes (I've adjusted it there as well):

{# anywhere #}
{% set name = 'Fabien' %}
<twig:DivComponent>
    Hello {{ name }}!
</twig:DivComponent>
{# DivComponent.html.twig #}
<twig:GenericElement element="div" class="divComponent">
    {{ block(outerBlocks.content) }}
</twig:GenericElement>
{# GenericElement.html.twig #}
<{{ element }}{{ attributes }}>
  {%- block content -%}{%- endblock -%}
</{{ element }}>

I added a LOT more test cases, which deal with all kind of problems that would not be workable with just passing blocks down like:

  • nesting the same component instances in each other, or simply components which are using the same block names
  • skipping blocks for some component instances
  • adding extra content to an outer block at some level of the component stack

See this test which lists all "rules" that blocks abide to within the context of embedded components.

@weaverryan
Copy link
Member

This is QUITE impressive and also complex. You've done an excellent job annotating the complex code, but still, this is a BEAST. However, as this is important, the feature is experimental and its well-covered in tests, I think we need to merge it and try to start using it in the wild. Thank you for your hard work on this @sneakyvv!

@weaverryan weaverryan merged commit b06640e into symfony:2.x Jun 23, 2023
symfony-splitter pushed a commit to symfony/ux-twig-component that referenced this pull request Jun 23, 2023
…d components (sneakyvv)

This PR was squashed before being merged into the 2.x branch.

Discussion
----------

[TwigComponent] Support passing blocks to nested embedded components

| Q             | A
| ------------- | ---
| Bug fix?      | yes
| New feature?  | no
| Tickets       | Fix #844
| License       | MIT

The fix for #844, and a better solution then a manual passthrough workaround, is literally just passing the blocks from the "host Template" to the embedded Template, ~~so that the blocks are merged with the embedded template's blocks, overriding them~~ but altering their name and using a special `outerBloocks` variable to map the block name to the new name (see symfony/ux#920 (comment)).

This means that the example from symfony/ux#844 (comment) (tweaked a bit)

```twig
{# anywhere #}
{% set name = 'Fabien' %}
<twig:DivComponent>
    Hello {{ name }}!
</twig:DivComponent>
```

```twig
{# DivComponent.html.twig #}
<twig:GenericElement element="div" class="divComponent">
    {{ block(outerBlocks.content) }}
</twig:GenericElement>
```

```twig
{# GenericElement.html.twig #}
<{{ element }}{{ attributes }}>
  {%- block content -%}{%- endblock -%}
</{{ element }}>
```

Results in

```twig
<div class="divComponent">Hello Fabien!</div>
```

See the tests for more elaborate examples showing the access to context variables, multi-level overrides, etc.

Commits
-------

f4b064ca [TwigComponent] Support passing blocks to nested embedded components
weaverryan added a commit that referenced this pull request Jul 11, 2023
…o embedded components (sneakyvv, weaverryan)

This PR was merged into the 2.x branch.

Discussion
----------

[TwigComponent] Add documentation about passing blocks to embedded components

| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | no
| Tickets       | Fix #...
| License       | MIT

This adds documentation of the feature added in #920

Commits
-------

0bbb2b8 Fixing rst syntax
acaa41a proofing
ae5a458 Add simple example of outerBlocks without nested components to ease in
111ac3a Allow referring to outerblocks from non-nested components
b0913d4 Fix typos
8e1d94b Moving away from "embedded component" wording in docs
305661c Added suggestions of PR feedback
1e85030 Add version info
444686c Add documentation about passing blocks to embedded components and their context
weaverryan added a commit that referenced this pull request Sep 27, 2023
…endering embedded components (sneakyvv)

This PR was squashed before being merged into the 2.x branch.

Discussion
----------

[TwigComponent] Add variable to refer outer scope when rendering embedded components

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

This PR's is an alternative approach to the problem stated in #863.

Due to the changes proposed in #920, which deals with passing blocks down to nested embedded components, I realized that just being able to access the host (or parent) component isn't sufficient if blocks can be passed down multiple levels deep (as already mentioned in #863 (comment)).

This PR adds a proxy object `Hierarchy` which allows this syntax:

`Component.php`
```php
final class Component
{
    public function scream(int $screamHowHard = 1): string
    {
        return 'sf-ux rules' . str_repeat('!', $screamHowHard);
    }
}
```

```twig
{# component.html.twig #}

<twig:Foo :someProp="this.something(1)">
    {{ someProp }}
    {{ outerScope.this.something(3) }}
</twig:Foo>
```

```twig
{# Foo.html.twig #}

<div>{% block content %}{% endblock %}</div>
```

Resulting in

```twig
<div>
    sf-ux rules!
    sf-ux rules!!!
</div>
```

Adding another layer below Foo, would ask for something like

```twig
{# component.html.twig #}

<twig:Foo>
    {{ outerScope.outerScope.this.something(3) }}
</twig:Foo>
```

to still point to Foo's function.

---

### Questions

There's also a `ComputedPropertiesProxy` object. Should we have a cached version of the `Hierarchy` proxy?

---

### NOTE

This PR continues on the changes of #920, because having a hierarchy only makes sense if blocks can be passed down multiple times. So #920 may have to be merged first, but if needed, and the `outerScope.this` syntax is preferred over the syntax of #863, I could push my branch that starts of the main branch instead.

Edited: using outerScope variable now instead Hierarchy proxy

Commits
-------

e618111 [TwigComponent] Add variable to refer outer scope when rendering embedded components
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.

[TwigComponent] Impossible to declare higher level component content block
2 participants