Skip to content

[Twig] add ExposeInTemplate attribute #259

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
Feb 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/LiveComponent/src/ComponentWithFormTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use Symfony\UX\LiveComponent\Attribute\BeforeReRender;
use Symfony\UX\LiveComponent\Attribute\LiveProp;
use Symfony\UX\LiveComponent\Util\LiveFormUtility;
use Symfony\UX\TwigComponent\Attribute\ExposeInTemplate;
use Symfony\UX\TwigComponent\Attribute\PostMount;

/**
Expand All @@ -27,6 +28,7 @@
*/
trait ComponentWithFormTrait
{
#[ExposeInTemplate(name: 'form', getter: 'getForm')]
private ?FormView $formView = null;
private ?FormInterface $formInstance = null;

Expand Down
29 changes: 17 additions & 12 deletions src/LiveComponent/src/Resources/doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -813,7 +813,12 @@ recreate the *same* form, we pass in the ``Post`` object and set it as a
``LiveProp``.

The template for this component will render the form, which is available
as ``this.form`` thanks to the trait:
as ``form`` thanks to the trait:

.. versionadded:: 2.1

The ability to access ``form`` directly in your component's template
was added in LiveComponents 2.1. Previously ``this.form`` was required.

.. code-block:: twig

Expand All @@ -833,13 +838,13 @@ as ``this.form`` thanks to the trait:
#}
data-action="change->live#update"
>
{{ form_start(this.form) }}
{{ form_row(this.form.title) }}
{{ form_row(this.form.slug) }}
{{ form_row(this.form.content) }}
{{ form_start(form) }}
{{ form_row(form.title) }}
{{ form_row(form.slug) }}
{{ form_row(form.content) }}

<button>Save</button>
{{ form_end(this.form) }}
{{ form_end(form) }}
</div>

Mostly, this is a pretty boring template! It includes the normal
Expand Down Expand Up @@ -1027,7 +1032,7 @@ Finally, tell the ``form`` element to use this action:
{# templates/components/post_form.html.twig #}
{# ... #}

{{ form_start(this.form, {
{{ form_start(form, {
attr: {
'data-action': 'live#action',
'data-action-name': 'prevent|save'
Expand Down Expand Up @@ -1148,11 +1153,11 @@ and ``removeComment()`` actions:
.. code-block:: twig

<div{{ attributes }}>
{{ form_start(this.form) }}
{{ form_row(this.form.title) }}
{{ form_start(form) }}
{{ form_row(form.title) }}

<h3>Comments:</h3>
{% for key, commentForm in this.form.comments %}
{% for key, commentForm in form.comments %}
<button
data-action="live#action"
data-action-name="removeComment(index={{ key }})"
Expand All @@ -1164,7 +1169,7 @@ and ``removeComment()`` actions:
</div>

{# avoid an extra label for this field #}
{% do this.form.comments.setRendered %}
{% do form.comments.setRendered %}

<button
data-action="live#action"
Expand All @@ -1173,7 +1178,7 @@ and ``removeComment()`` actions:
>+ Add Comment</button>

<button type="submit" >Save</button>
{{ form_end(this.form) }}
{{ form_end(form) }}
</div>

Done! Behind the scenes, it works like this:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
<div{{ attributes }}>
{{ form(this.form) }}
{{ form(form) }}
</div>
3 changes: 3 additions & 0 deletions src/TwigComponent/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@

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

- Add `ExposeInTemplate` attribute to make non-public properties available in component
templates directly.

## 2.0.0

- Support for `stimulus` version 2 was removed and support for `@hotwired/stimulus`
Expand Down
35 changes: 35 additions & 0 deletions src/TwigComponent/src/Attribute/ExposeInTemplate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\UX\TwigComponent\Attribute;

/**
* Use to expose private/protected properties as variables directly
* in a component template (`someProp` vs `this.someProp`). These
* properties must be "accessible" (have a getter).
*
* @author Kevin Bond <[email protected]>
*
* @experimental
*/
#[\Attribute(\Attribute::TARGET_PROPERTY)]
final class ExposeInTemplate
{
/**
* @param string|null $name The variable name to expose. Leave as null
* to default to property name.
* @param string|null $getter The getter method to use. Leave as null
* to default to PropertyAccessor logic.
*/
public function __construct(public ?string $name = null, public ?string $getter = null)
{
}
}
39 changes: 37 additions & 2 deletions src/TwigComponent/src/ComponentRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@

namespace Symfony\UX\TwigComponent;

use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use Symfony\UX\TwigComponent\Attribute\ExposeInTemplate;
use Symfony\UX\TwigComponent\EventListener\PreRenderEvent;
use Twig\Environment;
use Twig\Extension\EscaperExtension;
Expand All @@ -28,7 +30,8 @@ final class ComponentRenderer
public function __construct(
private Environment $twig,
private EventDispatcherInterface $dispatcher,
private ComponentFactory $factory
private ComponentFactory $factory,
private PropertyAccessorInterface $propertyAccessor
) {
}

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

$event = new PreRenderEvent($mounted, $this->factory->metadataFor($mounted->getName()));
$component = $mounted->getComponent();
$variables = array_merge(
// add the component as "this"
['this' => $component],

// add attributes
['attributes' => $mounted->getAttributes()],

// expose all public properties
get_object_vars($component),

// expose non-public properties marked with ExposeInTemplate attribute
iterator_to_array($this->exposedVariables($component)),
);
$event = new PreRenderEvent($mounted, $this->factory->metadataFor($mounted->getName()), $variables);

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

return $this->twig->render($event->getTemplate(), $event->getVariables());
}

private function exposedVariables(object $component): \Iterator
{
$class = new \ReflectionClass($component);

foreach ($class->getProperties() as $property) {
if (!$attribute = $property->getAttributes(ExposeInTemplate::class)[0] ?? null) {
continue;
}

$attribute = $attribute->newInstance();

/** @var ExposeInTemplate $attribute */
$value = $attribute->getter ? $component->{rtrim($attribute->getter, '()')}() : $this->propertyAccessor->getValue($component, $property->name);

yield $attribute->name ?? $property->name => $value;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ class_exists(AbstractArgument::class) ? new AbstractArgument(sprintf('Added in %
new Reference('twig'),
new Reference('event_dispatcher'),
new Reference('ux.twig_component.component_factory'),
new Reference('property_accessor'),
])
;

Expand Down
9 changes: 5 additions & 4 deletions src/TwigComponent/src/EventListener/PreRenderEvent.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,16 @@
final class PreRenderEvent extends Event
{
private string $template;
private array $variables;

/**
* @internal
*/
public function __construct(private MountedComponent $mounted, private ComponentMetadata $metadata)
{
public function __construct(
private MountedComponent $mounted,
private ComponentMetadata $metadata,
private array $variables
) {
$this->template = $this->metadata->getTemplate();
$this->variables = $this->mounted->getVariables();
}

/**
Expand Down
8 changes: 0 additions & 8 deletions src/TwigComponent/src/MountedComponent.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,4 @@ public function getAttributes(): ComponentAttributes
{
return $this->attributes;
}

public function getVariables(): array
{
return array_merge(
['this' => $this->component, 'attributes' => $this->attributes],
get_object_vars($this->component)
);
}
}
54 changes: 54 additions & 0 deletions src/TwigComponent/src/Resources/doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,60 @@ If an option name matches an argument name in ``mount()``, the option is
passed as that argument and the component system will *not* try to set
it directly on a property.

ExposeInTemplate Attribute
~~~~~~~~~~~~~~~~~~~~~~~~~~

.. versionadded:: 2.1

The ``ExposeInTemplate`` attribute was added in TwigComponents 2.1.

All public component properties are available directly in your component
template. You can use the ``ExposeInTemplate`` attribute to expose
private/protected properties directly in a component template (``someProp``
vs ``this.someProp``). These properties must be *accessible* (have a getter).

// ...
use Symfony\UX\TwigComponent\Attribute\ExposeInTemplate;

#[AsTwigComponent('alert')]
class AlertComponent
{
#[ExposeInTemplate]
private string $message; // available as `{{ message }}` in the template

#[ExposeInTemplate('alert_type')]
private string $type = 'success'; // available as `{{ alert_type }}` in the template

#[ExposeInTemplate(name: 'ico', getter: 'fetchIcon')]
private string $icon = 'ico-warning'; // available as `{{ ico }}` in the template using `fetchIcon()` as the getter

/**
* Required to access $this->message
*/
public function getMessage(): string
{
return $this->message;
}

/**
* Required to access $this->type
*/
public function getType(): string
{
return $this->type;
}

/**
* Required to access $this->icon
*/
public function fetchIcon(): string
{
return $this->icon;
}

// ...
}

PreMount Hook
~~~~~~~~~~~~~

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

namespace Symfony\UX\TwigComponent\Tests\Fixtures\Component;

use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
use Symfony\UX\TwigComponent\Attribute\ExposeInTemplate;

/**
* @author Kevin Bond <[email protected]>
*/
#[AsTwigComponent('with_exposed_variables')]
final class WithExposedVariables
{
#[ExposeInTemplate]
private string $prop1 = 'prop1 value';

#[ExposeInTemplate('customProp2')]
private string $prop2 = 'prop2 value';

#[ExposeInTemplate('customProp3', getter: 'customGetter()')]
private string $prop3 = 'prop3 value';

public function getProp1(): string
{
return $this->prop1;
}

public function getProp2(): string
{
return $this->prop2;
}

public function customGetter(): string
{
return $this->prop3;
}
}
2 changes: 2 additions & 0 deletions src/TwigComponent/tests/Fixtures/Kernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
use Symfony\UX\TwigComponent\Tests\Fixtures\Component\WithExposedVariables;
use Symfony\UX\TwigComponent\Tests\Fixtures\Component\ComponentA;
use Symfony\UX\TwigComponent\Tests\Fixtures\Component\ComponentB;
use Symfony\UX\TwigComponent\Tests\Fixtures\Component\ComponentC;
Expand Down Expand Up @@ -59,6 +60,7 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load
'key' => 'component_d',
'template' => 'components/custom2.html.twig',
]);
$c->register(WithExposedVariables::class)->setAutoconfigured(true)->setAutowired(true);

if ('missing_key' === $this->environment) {
$c->register('missing_key', ComponentB::class)->setAutowired(true)->addTag('twig.component');
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Prop1: {{ prop1 }}
Prop2: {{ customProp2 }}
Prop3: {{ customProp3 }}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{ component('with_exposed_variables') }}
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,13 @@ public function testCanRenderComponentWithAttributes(): void
$this->assertStringContainsString('Component Content (prop value 2)', $output);
$this->assertStringContainsString('<button class="foo baz" type="submit" style="color:red;">', $output);
}

public function testRenderComponentWithExposedVariables(): void
{
$output = self::getContainer()->get(Environment::class)->render('exposed_variables.html.twig');

$this->assertStringContainsString('Prop1: prop1 value', $output);
$this->assertStringContainsString('Prop2: prop2 value', $output);
$this->assertStringContainsString('Prop3: prop3 value', $output);
}
}