Skip to content

Commit e6c53fd

Browse files
kbondweaverryan
authored andcommitted
[Twig] add ExposeInTemplate attribute
1 parent b355eb9 commit e6c53fd

File tree

15 files changed

+207
-27
lines changed

15 files changed

+207
-27
lines changed

src/LiveComponent/src/ComponentWithFormTrait.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use Symfony\UX\LiveComponent\Attribute\BeforeReRender;
1919
use Symfony\UX\LiveComponent\Attribute\LiveProp;
2020
use Symfony\UX\LiveComponent\Util\LiveFormUtility;
21+
use Symfony\UX\TwigComponent\Attribute\ExposeInTemplate;
2122
use Symfony\UX\TwigComponent\Attribute\PostMount;
2223

2324
/**
@@ -27,6 +28,7 @@
2728
*/
2829
trait ComponentWithFormTrait
2930
{
31+
#[ExposeInTemplate(name: 'form', getter: 'getForm')]
3032
private ?FormView $formView = null;
3133
private ?FormInterface $formInstance = null;
3234

src/LiveComponent/src/Resources/doc/index.rst

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -813,7 +813,12 @@ recreate the *same* form, we pass in the ``Post`` object and set it as a
813813
``LiveProp``.
814814

815815
The template for this component will render the form, which is available
816-
as ``this.form`` thanks to the trait:
816+
as ``form`` thanks to the trait:
817+
818+
.. versionadded:: 2.1
819+
820+
The ability to access ``form`` directly in your component's template
821+
was added in LiveComponents 2.1. Previously ``this.form`` was required.
817822

818823
.. code-block:: twig
819824
@@ -833,13 +838,13 @@ as ``this.form`` thanks to the trait:
833838
#}
834839
data-action="change->live#update"
835840
>
836-
{{ form_start(this.form) }}
837-
{{ form_row(this.form.title) }}
838-
{{ form_row(this.form.slug) }}
839-
{{ form_row(this.form.content) }}
841+
{{ form_start(form) }}
842+
{{ form_row(form.title) }}
843+
{{ form_row(form.slug) }}
844+
{{ form_row(form.content) }}
840845
841846
<button>Save</button>
842-
{{ form_end(this.form) }}
847+
{{ form_end(form) }}
843848
</div>
844849
845850
Mostly, this is a pretty boring template! It includes the normal
@@ -1027,7 +1032,7 @@ Finally, tell the ``form`` element to use this action:
10271032
{# templates/components/post_form.html.twig #}
10281033
{# ... #}
10291034
1030-
{{ form_start(this.form, {
1035+
{{ form_start(form, {
10311036
attr: {
10321037
'data-action': 'live#action',
10331038
'data-action-name': 'prevent|save'
@@ -1148,11 +1153,11 @@ and ``removeComment()`` actions:
11481153
.. code-block:: twig
11491154
11501155
<div{{ attributes }}>
1151-
{{ form_start(this.form) }}
1152-
{{ form_row(this.form.title) }}
1156+
{{ form_start(form) }}
1157+
{{ form_row(form.title) }}
11531158
11541159
<h3>Comments:</h3>
1155-
{% for key, commentForm in this.form.comments %}
1160+
{% for key, commentForm in form.comments %}
11561161
<button
11571162
data-action="live#action"
11581163
data-action-name="removeComment(index={{ key }})"
@@ -1164,7 +1169,7 @@ and ``removeComment()`` actions:
11641169
</div>
11651170
11661171
{# avoid an extra label for this field #}
1167-
{% do this.form.comments.setRendered %}
1172+
{% do form.comments.setRendered %}
11681173
11691174
<button
11701175
data-action="live#action"
@@ -1173,7 +1178,7 @@ and ``removeComment()`` actions:
11731178
>+ Add Comment</button>
11741179
11751180
<button type="submit" >Save</button>
1176-
{{ form_end(this.form) }}
1181+
{{ form_end(form) }}
11771182
</div>
11781183
11791184
Done! Behind the scenes, it works like this:
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
<div{{ attributes }}>
2-
{{ form(this.form) }}
2+
{{ form(form) }}
33
</div>

src/TwigComponent/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313

1414
- Add `PreRenderEvent` to intercept/manipulate twig template/variables before rendering.
1515

16+
- Add `ExposeInTemplate` attribute to make non-public properties available in component
17+
templates directly.
18+
1619
## 2.0.0
1720

1821
- Support for `stimulus` version 2 was removed and support for `@hotwired/stimulus`
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\UX\TwigComponent\Attribute;
13+
14+
/**
15+
* Use to expose private/protected properties as variables directly
16+
* in a component template (`someProp` vs `this.someProp`). These
17+
* properties must be "accessible" (have a getter).
18+
*
19+
* @author Kevin Bond <[email protected]>
20+
*
21+
* @experimental
22+
*/
23+
#[\Attribute(\Attribute::TARGET_PROPERTY)]
24+
final class ExposeInTemplate
25+
{
26+
/**
27+
* @param string|null $name The variable name to expose. Leave as null
28+
* to default to property name.
29+
* @param string|null $getter The getter method to use. Leave as null
30+
* to default to PropertyAccessor logic.
31+
*/
32+
public function __construct(public ?string $name = null, public ?string $getter = null)
33+
{
34+
}
35+
}

src/TwigComponent/src/ComponentRenderer.php

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111

1212
namespace Symfony\UX\TwigComponent;
1313

14+
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
1415
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
16+
use Symfony\UX\TwigComponent\Attribute\ExposeInTemplate;
1517
use Symfony\UX\TwigComponent\EventListener\PreRenderEvent;
1618
use Twig\Environment;
1719
use Twig\Extension\EscaperExtension;
@@ -28,7 +30,8 @@ final class ComponentRenderer
2830
public function __construct(
2931
private Environment $twig,
3032
private EventDispatcherInterface $dispatcher,
31-
private ComponentFactory $factory
33+
private ComponentFactory $factory,
34+
private PropertyAccessorInterface $propertyAccessor
3235
) {
3336
}
3437

@@ -40,10 +43,42 @@ public function render(MountedComponent $mounted): string
4043
$this->safeClassesRegistered = true;
4144
}
4245

43-
$event = new PreRenderEvent($mounted, $this->factory->metadataFor($mounted->getName()));
46+
$component = $mounted->getComponent();
47+
$variables = array_merge(
48+
// add the component as "this"
49+
['this' => $component],
50+
51+
// add attributes
52+
['attributes' => $mounted->getAttributes()],
53+
54+
// expose all public properties
55+
get_object_vars($component),
56+
57+
// expose non-public properties marked with ExposeInTemplate attribute
58+
iterator_to_array($this->exposedVariables($component)),
59+
);
60+
$event = new PreRenderEvent($mounted, $this->factory->metadataFor($mounted->getName()), $variables);
4461

4562
$this->dispatcher->dispatch($event);
4663

4764
return $this->twig->render($event->getTemplate(), $event->getVariables());
4865
}
66+
67+
private function exposedVariables(object $component): \Iterator
68+
{
69+
$class = new \ReflectionClass($component);
70+
71+
foreach ($class->getProperties() as $property) {
72+
if (!$attribute = $property->getAttributes(ExposeInTemplate::class)[0] ?? null) {
73+
continue;
74+
}
75+
76+
$attribute = $attribute->newInstance();
77+
78+
/** @var ExposeInTemplate $attribute */
79+
$value = $attribute->getter ? $component->{rtrim($attribute->getter, '()')}() : $this->propertyAccessor->getValue($component, $property->name);
80+
81+
yield $attribute->name ?? $property->name => $value;
82+
}
83+
}
4984
}

src/TwigComponent/src/DependencyInjection/TwigComponentExtension.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ class_exists(AbstractArgument::class) ? new AbstractArgument(sprintf('Added in %
5757
new Reference('twig'),
5858
new Reference('event_dispatcher'),
5959
new Reference('ux.twig_component.component_factory'),
60+
new Reference('property_accessor'),
6061
])
6162
;
6263

src/TwigComponent/src/EventListener/PreRenderEvent.php

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,16 @@
2323
final class PreRenderEvent extends Event
2424
{
2525
private string $template;
26-
private array $variables;
2726

2827
/**
2928
* @internal
3029
*/
31-
public function __construct(private MountedComponent $mounted, private ComponentMetadata $metadata)
32-
{
30+
public function __construct(
31+
private MountedComponent $mounted,
32+
private ComponentMetadata $metadata,
33+
private array $variables
34+
) {
3335
$this->template = $this->metadata->getTemplate();
34-
$this->variables = $this->mounted->getVariables();
3536
}
3637

3738
/**

src/TwigComponent/src/MountedComponent.php

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,4 @@ public function getAttributes(): ComponentAttributes
4141
{
4242
return $this->attributes;
4343
}
44-
45-
public function getVariables(): array
46-
{
47-
return array_merge(
48-
['this' => $this->component, 'attributes' => $this->attributes],
49-
get_object_vars($this->component)
50-
);
51-
}
5244
}

src/TwigComponent/src/Resources/doc/index.rst

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,60 @@ If an option name matches an argument name in ``mount()``, the option is
219219
passed as that argument and the component system will *not* try to set
220220
it directly on a property.
221221

222+
ExposeInTemplate Attribute
223+
~~~~~~~~~~~~~~~~~~~~~~~~~~
224+
225+
.. versionadded:: 2.1
226+
227+
The ``ExposeInTemplate`` attribute was added in TwigComponents 2.1.
228+
229+
All public component properties are available directly in your component
230+
template. You can use the ``ExposeInTemplate`` attribute to expose
231+
private/protected properties directly in a component template (``someProp``
232+
vs ``this.someProp``). These properties must be *accessible* (have a getter).
233+
234+
// ...
235+
use Symfony\UX\TwigComponent\Attribute\ExposeInTemplate;
236+
237+
#[AsTwigComponent('alert')]
238+
class AlertComponent
239+
{
240+
#[ExposeInTemplate]
241+
private string $message; // available as `{{ message }}` in the template
242+
243+
#[ExposeInTemplate('alert_type')]
244+
private string $type = 'success'; // available as `{{ alert_type }}` in the template
245+
246+
#[ExposeInTemplate(name: 'ico', getter: 'fetchIcon')]
247+
private string $icon = 'ico-warning'; // available as `{{ ico }}` in the template using `fetchIcon()` as the getter
248+
249+
/**
250+
* Required to access $this->message
251+
*/
252+
public function getMessage(): string
253+
{
254+
return $this->message;
255+
}
256+
257+
/**
258+
* Required to access $this->type
259+
*/
260+
public function getType(): string
261+
{
262+
return $this->type;
263+
}
264+
265+
/**
266+
* Required to access $this->icon
267+
*/
268+
public function fetchIcon(): string
269+
{
270+
return $this->icon;
271+
}
272+
273+
// ...
274+
}
275+
222276
PreMount Hook
223277
~~~~~~~~~~~~~
224278

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
namespace Symfony\UX\TwigComponent\Tests\Fixtures\Component;
4+
5+
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
6+
use Symfony\UX\TwigComponent\Attribute\ExposeInTemplate;
7+
8+
/**
9+
* @author Kevin Bond <[email protected]>
10+
*/
11+
#[AsTwigComponent('with_exposed_variables')]
12+
final class WithExposedVariables
13+
{
14+
#[ExposeInTemplate]
15+
private string $prop1 = 'prop1 value';
16+
17+
#[ExposeInTemplate('customProp2')]
18+
private string $prop2 = 'prop2 value';
19+
20+
#[ExposeInTemplate('customProp3', getter: 'customGetter()')]
21+
private string $prop3 = 'prop3 value';
22+
23+
public function getProp1(): string
24+
{
25+
return $this->prop1;
26+
}
27+
28+
public function getProp2(): string
29+
{
30+
return $this->prop2;
31+
}
32+
33+
public function customGetter(): string
34+
{
35+
return $this->prop3;
36+
}
37+
}

src/TwigComponent/tests/Fixtures/Kernel.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use Symfony\Component\Config\Loader\LoaderInterface;
1818
use Symfony\Component\DependencyInjection\ContainerBuilder;
1919
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
20+
use Symfony\UX\TwigComponent\Tests\Fixtures\Component\WithExposedVariables;
2021
use Symfony\UX\TwigComponent\Tests\Fixtures\Component\ComponentA;
2122
use Symfony\UX\TwigComponent\Tests\Fixtures\Component\ComponentB;
2223
use Symfony\UX\TwigComponent\Tests\Fixtures\Component\ComponentC;
@@ -59,6 +60,7 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load
5960
'key' => 'component_d',
6061
'template' => 'components/custom2.html.twig',
6162
]);
63+
$c->register(WithExposedVariables::class)->setAutoconfigured(true)->setAutowired(true);
6264

6365
if ('missing_key' === $this->environment) {
6466
$c->register('missing_key', ComponentB::class)->setAutowired(true)->addTag('twig.component');
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Prop1: {{ prop1 }}
2+
Prop2: {{ customProp2 }}
3+
Prop3: {{ customProp3 }}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{{ component('with_exposed_variables') }}

src/TwigComponent/tests/Integration/ComponentExtensionTest.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,13 @@ public function testCanRenderComponentWithAttributes(): void
6464
$this->assertStringContainsString('Component Content (prop value 2)', $output);
6565
$this->assertStringContainsString('<button class="foo baz" type="submit" style="color:red;">', $output);
6666
}
67+
68+
public function testRenderComponentWithExposedVariables(): void
69+
{
70+
$output = self::getContainer()->get(Environment::class)->render('exposed_variables.html.twig');
71+
72+
$this->assertStringContainsString('Prop1: prop1 value', $output);
73+
$this->assertStringContainsString('Prop2: prop2 value', $output);
74+
$this->assertStringContainsString('Prop3: prop3 value', $output);
75+
}
6776
}

0 commit comments

Comments
 (0)