Skip to content

Commit 2609c67

Browse files
committed
feature #1041 Props tag remove props from the attribute list (matheo, WebMamba)
This PR was merged into the 2.x branch. Discussion ---------- Props tag remove props from the attribute list | Q | A | ------------- | --- | Bug fix? | yes | New feature? | no | Tickets | none | License | MIT An assertion in ComponentFactory is blocking us to use non-scalar values for anonymous components. `@weaverryan` do you know why this check was made for? Commits ------- 6f693d6 Remove .idea 58fce65 remove .idea f6ac821 remove attributes from context 416c46a make remove method immutable and test if attributes are still render 66b7f58 remove props from the attribute in the PropsNode 2555d26 Revert "props tag remove props from attributes" a9d422e props tag remove props from attributes 2d9f300 Fix non scalar value to anonymous component
2 parents 5fd8da1 + 6f693d6 commit 2609c67

File tree

8 files changed

+79
-18
lines changed

8 files changed

+79
-18
lines changed

src/TwigComponent/src/ComponentAttributes.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ public function __toString(): string
3535
function (string $carry, string $key) {
3636
$value = $this->attributes[$key];
3737

38+
if (!\is_scalar($value) && null !== $value) {
39+
throw new \LogicException(sprintf('A "%s" prop was passed when creating the component. No matching "%s" property or mount() argument was found, so we attempted to use this as an HTML attribute. But, the value is not a scalar (it\'s a %s). Did you mean to pass this to your component or is there a typo on its name?', $key, $key, get_debug_type($value)));
40+
}
41+
3842
if (null === $value) {
3943
trigger_deprecation('symfony/ux-twig-component', '2.8.0', 'Passing "null" as an attribute value is deprecated and will throw an exception in 3.0.');
4044
$value = true;
@@ -144,4 +148,13 @@ public function add($stimulusDto): self
144148
// add the remaining attributes for values/classes
145149
return $clone->defaults($controllersAttributes);
146150
}
151+
152+
public function remove($key): self
153+
{
154+
$attributes = $this->attributes;
155+
156+
unset($attributes[$key]);
157+
158+
return new self($attributes);
159+
}
147160
}

src/TwigComponent/src/ComponentFactory.php

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,7 @@ public function mountFromObject(object $component, array $data, ComponentMetadat
102102
continue;
103103
}
104104

105-
if (!\is_scalar($value) && null !== $value) {
106-
throw new \LogicException(sprintf('A "%s" prop was passed when creating the "%s" component. No matching %s property or mount() argument was found, so we attempted to use this as an HTML attribute. But, the value is not a scalar (it\'s a %s). Did you mean to pass this to your component or is there a typo on its name?', $key, $componentMetadata->getName(), $key, get_debug_type($value)));
107-
}
105+
$data[$key] = $value;
108106
}
109107

110108
return new MountedComponent(

src/TwigComponent/src/Twig/PropsNode.php

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,21 @@ public function __construct(array $propsNames, array $values, $lineno = 0, strin
2828

2929
public function compile(Compiler $compiler): void
3030
{
31+
$compiler
32+
->addDebugInfo($this)
33+
->write('$propsNames = [];')
34+
;
35+
3136
foreach ($this->getAttribute('names') as $name) {
3237
$compiler
33-
->addDebugInfo($this)
34-
->write('if (!isset($context[\''.$name.'\'])) {')
35-
;
38+
->write('$propsNames[] = \''.$name.'\';')
39+
->write('$context[\'attributes\'] = $context[\'attributes\']->remove(\''.$name.'\');')
40+
->write('if (!isset($context[\''.$name.'\'])) {');
3641

3742
if (!$this->hasNode($name)) {
3843
$compiler
3944
->write('throw new \Twig\Error\RuntimeError("'.$name.' should be defined for component '.$this->getTemplateName().'");')
40-
->write('}')
41-
;
45+
->write('}');
4246

4347
continue;
4448
}
@@ -47,8 +51,20 @@ public function compile(Compiler $compiler): void
4751
->write('$context[\''.$name.'\'] = ')
4852
->subcompile($this->getNode($name))
4953
->raw(";\n")
50-
->write('}')
51-
;
54+
->write('}');
5255
}
56+
57+
$compiler
58+
->write('$attributesKeys = array_keys($context[\'attributes\']->all());')
59+
->raw("\n")
60+
->write('foreach ($context as $key => $value) {')
61+
->raw("\n")
62+
->write('if (in_array($key, $attributesKeys) && !in_array($key, $propsNames)) {')
63+
->raw("\n")
64+
->raw('unset($context[$key]);')
65+
->raw("\n")
66+
->write('}')
67+
->write('}')
68+
;
5369
}
5470
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace Symfony\UX\TwigComponent\Tests\Fixtures;
4+
5+
class User
6+
{
7+
public function __construct(
8+
private readonly string $name,
9+
private readonly string $email,
10+
) {}
11+
12+
public function getName(): string
13+
{
14+
return $this->name;
15+
}
16+
17+
public function getEmail(): string
18+
{
19+
return $this->email;
20+
}
21+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<twig:UserCard :user='user' class='foo'/>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{% props user %}
2+
3+
<div {{ attributes }}>
4+
<p>{{ user.name }}</p>
5+
<p>{{ user.email }}</p>
6+
<p>class variable defined? {{ class is defined ? 'yes': 'no' }}</p>
7+
</div>

src/TwigComponent/tests/Integration/ComponentExtensionTest.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\UX\TwigComponent\Tests\Integration;
1313

1414
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
15+
use Symfony\UX\TwigComponent\Tests\Fixtures\User;
1516
use Twig\Environment;
1617

1718
/**
@@ -182,6 +183,18 @@ public function testRenderAnonymousComponentInNestedDirectory(): void
182183
$this->assertStringContainsString('class="primary"', $output);
183184
}
184185

186+
public function testRenderAnonymousComponentWithNonScalarProps(): void
187+
{
188+
$user = new User('Fabien', '[email protected]');
189+
190+
$output = self::getContainer()->get(Environment::class)->render('anonymous_component_none_scalar_prop.html.twig', ['user' => $user]);
191+
192+
$this->assertStringContainsString('class="foo"', $output);
193+
$this->assertStringContainsString('Fabien', $output);
194+
$this->assertStringContainsString('[email protected]', $output);
195+
$this->assertStringContainsString('class variable defined? no', $output);
196+
}
197+
185198
private function renderComponent(string $name, array $data = []): string
186199
{
187200
return self::getContainer()->get(Environment::class)->render('render_component.html.twig', [

src/TwigComponent/tests/Integration/ComponentFactoryTest.php

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -91,14 +91,6 @@ public function testExceptionThrownIfRequiredMountParameterIsMissingFromPassedDa
9191
$this->createComponent('component_c');
9292
}
9393

94-
public function testExceptionThrownIfUnableToWritePassedDataToPropertyAndIsNotScalar(): void
95-
{
96-
$this->expectException(\LogicException::class);
97-
$this->expectExceptionMessage('But, the value is not a scalar (it\'s a stdClass)');
98-
99-
$this->createComponent('component_a', ['propB' => 'B', 'service' => new \stdClass()]);
100-
}
101-
10294
public function testStringableObjectCanBePassedToComponent(): void
10395
{
10496
$attributes = $this->factory()->create('component_a', ['propB' => 'B', 'data-item-id-param' => new class() {

0 commit comments

Comments
 (0)