Skip to content

Commit df6388e

Browse files
committed
[TwigComponents] Fixing inconsistency with how {% component rendered vs {{ component()
1 parent 2991b06 commit df6388e

File tree

7 files changed

+140
-79
lines changed

7 files changed

+140
-79
lines changed

src/LiveComponent/src/EventListener/DeferLiveComponentSubscriber.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,17 @@ public function onPostMount(PostMountEvent $event): void
2828
$data = $event->getData();
2929
if (\array_key_exists('defer', $data)) {
3030
$event->addExtraMetadata('defer', true);
31-
unset($event->getData()['defer']);
31+
unset($data['defer']);
3232
}
3333

3434
if (\array_key_exists('loading-template', $data)) {
3535
$event->addExtraMetadata('loading-template', $data['loading-template']);
36-
unset($event->getData()['loading-template']);
36+
unset($data['loading-template']);
3737
}
3838

3939
if (\array_key_exists('loading-tag', $data)) {
4040
$event->addExtraMetadata('loading-tag', $data['loading-tag']);
41-
unset($event->getData()['loading-tag']);
41+
unset($data['loading-tag']);
4242
}
4343

4444
$event->setData($data);

src/TwigComponent/src/ComponentRenderer.php

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -64,16 +64,23 @@ public function render(MountedComponent $mounted): string
6464

6565
$event = $this->preRender($mounted);
6666

67+
$variables = $event->getVariables();
68+
// see ComponentNode. When rendering an individual embedded component,
69+
// *not* through its parent, we need to set the parent template.
70+
if ($event->getTemplateIndex()) {
71+
$variables['__parent__'] = $event->getParentTemplateForEmbedded();
72+
}
73+
6774
try {
6875
if ($this->twig::MAJOR_VERSION < 3) {
69-
return $this->twig->loadTemplate($event->getTemplate(), $event->getTemplateIndex())->render($event->getVariables());
76+
return $this->twig->loadTemplate($event->getTemplate(), $event->getTemplateIndex())->render($variables);
7077
}
7178

7279
return $this->twig->loadTemplate(
7380
$this->twig->getTemplateClass($event->getTemplate()),
7481
$event->getTemplate(),
7582
$event->getTemplateIndex(),
76-
)->render($event->getVariables());
83+
)->render($variables);
7784
} finally {
7885
$mounted = $this->componentStack->pop();
7986

@@ -82,7 +89,7 @@ public function render(MountedComponent $mounted): string
8289
}
8390
}
8491

85-
public function embeddedContext(string $name, array $props, array $context, string $hostTemplateName, int $index): array
92+
public function startEmbeddedComponentRender(string $name, array $props, array $context, string $hostTemplateName, int $index): PreRenderEvent
8693
{
8794
$context[PreRenderEvent::EMBEDDED] = true;
8895

@@ -92,13 +99,7 @@ public function embeddedContext(string $name, array $props, array $context, stri
9299

93100
$this->componentStack->push($mounted);
94101

95-
$embeddedContext = $this->preRender($mounted, $context)->getVariables();
96-
97-
if (!isset($embeddedContext['outerBlocks'])) {
98-
$embeddedContext['outerBlocks'] = new BlockStack();
99-
}
100-
101-
return $embeddedContext;
102+
return $this->preRender($mounted, $context);
102103
}
103104

104105
public function finishEmbeddedComponentRender(): void

src/TwigComponent/src/DependencyInjection/TwigComponentExtension.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,6 @@ class_exists(AbstractArgument::class) ? new AbstractArgument(sprintf('Added in %
102102
$container->register('ux.twig_component.twig.component_extension', ComponentExtension::class)
103103
->addTag('twig.extension')
104104
->addTag('container.service_subscriber', ['key' => ComponentRenderer::class, 'id' => 'ux.twig_component.component_renderer'])
105-
->addTag('container.service_subscriber', ['key' => ComponentFactory::class, 'id' => 'ux.twig_component.component_factory'])
106105
;
107106

108107
$container->register('ux.twig_component.twig.lexer', ComponentLexer::class);

src/TwigComponent/src/Event/PreRenderEvent.php

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,13 @@ final class PreRenderEvent extends Event
2323
/** @internal */
2424
public const EMBEDDED = '__embedded';
2525

26+
/**
27+
* Only relevant when rendering a specific embedded component.
28+
* This is the "component template" that the embedded component
29+
* should extend.
30+
*/
31+
private string $parentTemplateForEmbedded;
2632
private string $template;
27-
2833
private ?int $templateIndex = null;
2934

3035
/**
@@ -36,6 +41,7 @@ public function __construct(
3641
private array $variables
3742
) {
3843
$this->template = $this->metadata->getTemplate();
44+
$this->parentTemplateForEmbedded = $this->template;
3945
}
4046

4147
public function isEmbedded(): bool
@@ -58,6 +64,10 @@ public function setTemplate(string $template, int $index = null): self
5864
{
5965
$this->template = $template;
6066
$this->templateIndex = $index;
67+
// only if we are *not* targeting an embedded component, change the parent template
68+
if (null === $index) {
69+
$this->parentTemplateForEmbedded = $template;
70+
}
6171

6272
return $this;
6373
}
@@ -70,6 +80,11 @@ public function getTemplateIndex(): ?int
7080
return $this->templateIndex;
7181
}
7282

83+
public function getParentTemplateForEmbedded(): string
84+
{
85+
return $this->parentTemplateForEmbedded;
86+
}
87+
7388
public function getComponent(): object
7489
{
7590
return $this->mounted->getComponent();

src/TwigComponent/src/Twig/ComponentExtension.php

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313

1414
use Psr\Container\ContainerInterface;
1515
use Symfony\Contracts\Service\ServiceSubscriberInterface;
16-
use Symfony\UX\TwigComponent\ComponentFactory;
1716
use Symfony\UX\TwigComponent\ComponentRenderer;
17+
use Symfony\UX\TwigComponent\Event\PreRenderEvent;
1818
use Twig\Error\RuntimeError;
1919
use Twig\Extension\AbstractExtension;
2020
use Twig\TwigFunction;
@@ -34,7 +34,6 @@ public static function getSubscribedServices(): array
3434
{
3535
return [
3636
ComponentRenderer::class,
37-
ComponentFactory::class,
3837
];
3938
}
4039

@@ -48,7 +47,7 @@ public function getFunctions(): array
4847
public function getTokenParsers(): array
4948
{
5049
return [
51-
new ComponentTokenParser(fn () => $this->container->get(ComponentFactory::class)),
50+
new ComponentTokenParser(),
5251
new PropsTokenParser(),
5352
];
5453
}
@@ -62,7 +61,7 @@ public function render(string $name, array $props = []): string
6261
}
6362
}
6463

65-
public function preRender(string $name, array $props): ?string
64+
public function extensionPreCreateForRender(string $name, array $props): ?string
6665
{
6766
try {
6867
return $this->container->get(ComponentRenderer::class)->preCreateForRender($name, $props);
@@ -71,10 +70,10 @@ public function preRender(string $name, array $props): ?string
7170
}
7271
}
7372

74-
public function embeddedContext(string $name, array $props, array $context, string $hostTemplateName, int $index): array
73+
public function startEmbeddedComponentRender(string $name, array $props, array $context, string $hostTemplateName, int $index): PreRenderEvent
7574
{
7675
try {
77-
return $this->container->get(ComponentRenderer::class)->embeddedContext($name, $props, $context, $hostTemplateName, $index);
76+
return $this->container->get(ComponentRenderer::class)->startEmbeddedComponentRender($name, $props, $context, $hostTemplateName, $index);
7877
} catch (\Throwable $e) {
7978
$this->throwRuntimeError($name, $e);
8079
}

src/TwigComponent/src/Twig/ComponentNode.php

Lines changed: 93 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,37 +11,53 @@
1111

1212
namespace Symfony\UX\TwigComponent\Twig;
1313

14+
use Symfony\UX\TwigComponent\BlockStack;
1415
use Twig\Compiler;
15-
use Twig\Node\EmbedNode;
1616
use Twig\Node\Expression\AbstractExpression;
17+
use Twig\Node\Node;
1718

1819
/**
1920
* @author Fabien Potencier <fabien@symfony.com>
2021
* @author Kevin Bond <kevinbond@gmail.com>
2122
*
2223
* @internal
2324
*/
24-
final class ComponentNode extends EmbedNode
25+
final class ComponentNode extends Node
2526
{
26-
public function __construct(string $component, string $template, int $index, AbstractExpression $variables, bool $only, int $lineno, string $tag)
27+
public function __construct(string $component, string $embeddedTemplateName, int $embeddedTemplateIndex, ?AbstractExpression $props, bool $only, int $lineno, string $tag)
2728
{
28-
parent::__construct($template, $index, $variables, $only, false, $lineno, $tag);
29+
$nodes = [];
30+
if (null !== $props) {
31+
$nodes['props'] = $props;
32+
}
2933

34+
parent::__construct($nodes, [], $lineno, $tag);
35+
36+
$this->setAttribute('only', $only);
37+
$this->setAttribute('embedded_template', $embeddedTemplateName);
38+
$this->setAttribute('embedded_index', $embeddedTemplateIndex);
3039
$this->setAttribute('component', $component);
3140
}
3241

3342
public function compile(Compiler $compiler): void
3443
{
3544
$compiler->addDebugInfo($this);
3645

46+
/*
47+
* Block 1) PreCreateForRender handling
48+
*
49+
* We call code to trigger the PreCreateForRender event. If the event returns
50+
* a string, we return that string and skip the rest of the rendering process.
51+
*/
3752
$compiler
3853
->write('$preRendered = $this->extensions[')
3954
->string(ComponentExtension::class)
40-
->raw(']->preRender(')
55+
->raw(']->extensionPreCreateForRender(')
4156
->string($this->getAttribute('component'))
4257
->raw(', ')
4358
->raw('twig_to_array(')
44-
->subcompile($this->getNode('variables'))
59+
;
60+
$this->writeProps($compiler)
4561
->raw(')')
4662
->raw(");\n")
4763
;
@@ -58,32 +74,85 @@ public function compile(Compiler $compiler): void
5874
->indent()
5975
;
6076

77+
/*
78+
* Block 2) Create the component & return render info
79+
*
80+
* We call code that creates the component and dispatches the
81+
* PreRender event. The result $preRenderEvent variable holds
82+
* the final template, template index & variables.
83+
*/
6184
$compiler
62-
->write('$embeddedContext = $this->extensions[')
85+
->write('$preRenderEvent = $this->extensions[')
6386
->string(ComponentExtension::class)
64-
->raw(']->embeddedContext(')
87+
->raw(']->startEmbeddedComponentRender(')
6588
->string($this->getAttribute('component'))
6689
->raw(', twig_to_array(')
67-
->subcompile($this->getNode('variables'))
90+
;
91+
$this->writeProps($compiler)
6892
->raw('), ')
6993
->raw($this->getAttribute('only') ? '[]' : '$context')
7094
->raw(', ')
71-
->string(TemplateNameParser::parse($this->getAttribute('name')))
95+
->string(TemplateNameParser::parse($this->getAttribute('embedded_template')))
7296
->raw(', ')
73-
->raw($this->getAttribute('index'))
97+
->raw($this->getAttribute('embedded_index'))
7498
->raw(");\n")
7599
;
100+
$compiler
101+
->write('$embeddedContext = $preRenderEvent->getVariables();')
102+
->raw("\n")
103+
// Add __parent__ to the embedded context: this is used in its extends
104+
// Note: PreRenderEvent::getTemplateIndex() is not used here. This is
105+
// only used during "normal" {{ component() }} rendering, which allows
106+
// you to target rendering a specific "embedded template" that originally
107+
// came from a {% component %} tag. This is used by LiveComponents to
108+
// allow an "embedded component" syntax live component to be re-rendered.
109+
// In this case, we are obviously rendering an entire template, which
110+
// happens to contain a {% component %} tag. So we don't need to worry
111+
// about trying to allow a specific embedded template to be targeted.
112+
->write('$embeddedContext["__parent__"] = $preRenderEvent->getTemplate();')
113+
->raw("\n")
114+
;
115+
116+
/*
117+
* Block 3) Add & update the block stack
118+
*
119+
* We add the outerBlock to the context if it doesn't exist yet.
120+
* Then add them to the block stack and get the converted embedded blocks.
121+
*/
122+
$compiler->write('if (!isset($embeddedContext["outerBlocks"])) {')
123+
->raw("\n")
124+
->indent()
125+
->write(sprintf('$embeddedContext["outerBlocks"] = new \%s();', BlockStack::class))
126+
->raw("\n")
127+
->outdent()
128+
->write('}')
129+
->raw("\n");
76130

77131
$compiler->write('$embeddedBlocks = $embeddedContext[')
78132
->string('outerBlocks')
79133
->raw(']->convert($blocks, ')
80-
->raw($this->getAttribute('index'))
134+
->raw($this->getAttribute('embedded_index'))
81135
->raw(");\n")
82136
;
83137

84-
$this->addGetTemplate($compiler);
85-
$compiler->raw('->display($embeddedContext, $embeddedBlocks);');
86-
$compiler->raw("\n");
138+
/*
139+
* Block 4) Render the component template
140+
*
141+
* This will actually render the child component template.
142+
*/
143+
$compiler
144+
->write('$this->loadTemplate(')
145+
->string($this->getAttribute('embedded_template'))
146+
->raw(', ')
147+
->repr($this->getTemplateName())
148+
->raw(', ')
149+
->repr($this->getTemplateLine())
150+
->raw(', ')
151+
->string($this->getAttribute('embedded_index'))
152+
->raw(')')
153+
->raw('->display($embeddedContext, $embeddedBlocks);')
154+
->raw("\n")
155+
;
87156

88157
$compiler->write('$this->extensions[')
89158
->string(ComponentExtension::class)
@@ -97,4 +166,13 @@ public function compile(Compiler $compiler): void
97166
->raw("\n")
98167
;
99168
}
169+
170+
private function writeProps(Compiler $compiler): Compiler
171+
{
172+
if ($this->hasNode('props')) {
173+
return $compiler->subcompile($this->getNode('props'));
174+
}
175+
176+
return $compiler->raw('[]');
177+
}
100178
}

0 commit comments

Comments
 (0)