Skip to content

Props tag remove props from the attribute list #1041

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
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
13 changes: 13 additions & 0 deletions src/TwigComponent/src/ComponentAttributes.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ public function __toString(): string
function (string $carry, string $key) {
$value = $this->attributes[$key];

if (!\is_scalar($value) && null !== $value) {
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)));
}

if (null === $value) {
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.');
$value = true;
Expand Down Expand Up @@ -144,4 +148,13 @@ public function add($stimulusDto): self
// add the remaining attributes for values/classes
return $clone->defaults($controllersAttributes);
}

public function remove($key): self
{
$attributes = $this->attributes;

unset($attributes[$key]);

return new self($attributes);
}
}
4 changes: 1 addition & 3 deletions src/TwigComponent/src/ComponentFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,7 @@ public function mountFromObject(object $component, array $data, ComponentMetadat
continue;
}

if (!\is_scalar($value) && null !== $value) {
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)));
}
$data[$key] = $value;
}

return new MountedComponent(
Expand Down
30 changes: 23 additions & 7 deletions src/TwigComponent/src/Twig/PropsNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,21 @@ public function __construct(array $propsNames, array $values, $lineno = 0, strin

public function compile(Compiler $compiler): void
{
$compiler
->addDebugInfo($this)
->write('$propsNames = [];')
;

foreach ($this->getAttribute('names') as $name) {
$compiler
->addDebugInfo($this)
->write('if (!isset($context[\''.$name.'\'])) {')
;
->write('$propsNames[] = \''.$name.'\';')
->write('$context[\'attributes\'] = $context[\'attributes\']->remove(\''.$name.'\');')
->write('if (!isset($context[\''.$name.'\'])) {');

if (!$this->hasNode($name)) {
$compiler
->write('throw new \Twig\Error\RuntimeError("'.$name.' should be defined for component '.$this->getTemplateName().'");')
->write('}')
;
->write('}');

continue;
}
Expand All @@ -47,8 +51,20 @@ public function compile(Compiler $compiler): void
->write('$context[\''.$name.'\'] = ')
->subcompile($this->getNode($name))
->raw(";\n")
->write('}')
;
->write('}');
}

$compiler
->write('$attributesKeys = array_keys($context[\'attributes\']->all());')
->raw("\n")
->write('foreach ($context as $key => $value) {')
->raw("\n")
->write('if (in_array($key, $attributesKeys) && !in_array($key, $propsNames)) {')
->raw("\n")
->raw('unset($context[$key]);')
->raw("\n")
->write('}')
->write('}')
;
}
}
21 changes: 21 additions & 0 deletions src/TwigComponent/tests/Fixtures/User.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace Symfony\UX\TwigComponent\Tests\Fixtures;

class User
{
public function __construct(
private readonly string $name,
private readonly string $email,
) {}

public function getName(): string
{
return $this->name;
}

public function getEmail(): string
{
return $this->email;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<twig:UserCard :user='user' class='foo'/>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{% props user %}

<div {{ attributes }}>
<p>{{ user.name }}</p>
<p>{{ user.email }}</p>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also add and test for this:

<p>class variable defined? {{ class is defined ? 'yes': 'no' }}

This is the last thing to fix. I just tested locally. Everything works great (user is removed from attributes, {% props can be used to define defaults, etc) except that if something is NOT defined in {% props (i.e. class) is IT being made available as a variable, and it should not be. So, in the test, we want to assert that we see:

class variable defined? no

<p>class variable defined? {{ class is defined ? 'yes': 'no' }}</p>
</div>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought that props that were passed to anonymous components became attributes unless you used the {% props tag. Did I misunderstand that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No the props tag only allow to set default value on attributes or to required attributes, but all props are at first an attribute

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean that, if we added <div {{ attributes }}> in this component, it would break because it would try to "print" the user as an attribute?

Copy link
Member

@kbond kbond Aug 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also misunderstood. I think this could make {{attributes}} non-stringable like Ryan says(?)

I thought that props that were passed to anonymous components became attributes unless you used the {% props tag.

Feel like this should be the case as I don't think there's any other way to make the distinction.

13 changes: 13 additions & 0 deletions src/TwigComponent/tests/Integration/ComponentExtensionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
namespace Symfony\UX\TwigComponent\Tests\Integration;

use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\UX\TwigComponent\Tests\Fixtures\User;
use Twig\Environment;

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

public function testRenderAnonymousComponentWithNonScalarProps(): void
{
$user = new User('Fabien', 'test@test.com');

$output = self::getContainer()->get(Environment::class)->render('anonymous_component_none_scalar_prop.html.twig', ['user' => $user]);

$this->assertStringContainsString('class="foo"', $output);
$this->assertStringContainsString('Fabien', $output);
$this->assertStringContainsString('test@test.com', $output);
$this->assertStringContainsString('class variable defined? no', $output);
}

private function renderComponent(string $name, array $data = []): string
{
return self::getContainer()->get(Environment::class)->render('render_component.html.twig', [
Expand Down
8 changes: 0 additions & 8 deletions src/TwigComponent/tests/Integration/ComponentFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,14 +91,6 @@ public function testExceptionThrownIfRequiredMountParameterIsMissingFromPassedDa
$this->createComponent('component_c');
}

public function testExceptionThrownIfUnableToWritePassedDataToPropertyAndIsNotScalar(): void
{
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('But, the value is not a scalar (it\'s a stdClass)');

$this->createComponent('component_a', ['propB' => 'B', 'service' => new \stdClass()]);
}

public function testStringableObjectCanBePassedToComponent(): void
{
$attributes = $this->factory()->create('component_a', ['propB' => 'B', 'data-item-id-param' => new class() {
Expand Down