Skip to content
Open
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
31 changes: 29 additions & 2 deletions src/LiveComponent/src/EventListener/DataModelPropsSubscriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,36 @@ public function onPreMount(PreMountEvent $event): void

foreach ($bindings as $binding) {
$childModel = $binding['child'];
$parentModel = $binding['parent'];
// strip the "[]" array notation (a JS-only convention, e.g. "selected[]")
// before resolving the property path, otherwise PropertyAccessor would throw
$parentModel = rtrim($binding['parent'], '[]');

$data[$childModel] = $this->propertyAccessor->getValue($parentMountedComponent->getComponent(), $parentModel);
$propValue = $this->propertyAccessor->getValue($parentMountedComponent->getComponent(), $parentModel);

// For radio/checkbox inputs the "value" attribute is per-input and must be preserved;
// the bound property only decides which input(s) are "checked". We cannot rely on a
// "type" prop to detect them (it may be hardcoded in the child template, see #3412),
// so we infer the case from the property value and the presence of an explicit "value".
if ('value' === $childModel) {
if (\is_bool($propValue)) {
// boolean checkbox: no explicit value, the property drives "checked"
$data['checked'] = $propValue;

continue;
}

if (\array_key_exists('value', $data)) {
// radio or checkbox group: keep the explicit per-input value and compute
// "checked" (loose comparison, to mirror the JS "setValueOnElement" behavior)
$data['checked'] = \is_array($propValue)
? \in_array($data['value'], $propValue, false)
: $data['value'] == $propValue;

continue;
}
}

$data[$childModel] = $propValue;
}

$event->setData($data);
Expand Down
19 changes: 19 additions & 0 deletions src/LiveComponent/tests/Fixtures/Component/CheckboxComponent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

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

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

use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;

#[AsTwigComponent('checkbox_component')]
final class CheckboxComponent
{
}
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 <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

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

use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveProp;
use Symfony\UX\LiveComponent\DefaultActionTrait;

#[AsLiveComponent('parent_component_data_model_inputs')]
final class ParentComponentDataModelInputs
{
use DefaultActionTrait;

#[LiveProp(writable: true)]
public string $choice = 'b';

/** @var string[] */
#[LiveProp(writable: true)]
public array $selected = ['paris'];

#[LiveProp(writable: true)]
public bool $active = true;

#[LiveProp(writable: true)]
public string $name = 'John';
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<input type="checkbox"{{ attributes }} />
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{% for option in ['a', 'b', 'c'] %}
{{ component('input_component', {'data-model': 'choice', type: 'radio', value: option}) }}
{% endfor %}
{% for city in ['paris', 'lyon'] %}
{{ component('input_component', {'data-model': 'selected[]', type: 'checkbox', value: city}) }}
{% endfor %}
{{ component('checkbox_component', {'data-model': 'active'}) }}
{{ component('input_component', {'data-model': 'name', type: 'text'}) }}
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,33 @@ public function testDataModelPropsAreAvailableInEmbeddedComponents()
$this->assertStringContainsString('<textarea data-model="content">default content on mount</textarea>', $html);
$this->assertStringContainsString('<input data-model="content" value="default content on mount" />', $html);
}

public function testDataModelOnRadioAndCheckboxInputs()
{
/** @var ComponentRenderer $renderer */
$renderer = self::getContainer()->get('ux.twig_component.component_renderer');

$html = $renderer->createAndRender('parent_component_data_model_inputs', [
'attributes' => ['id' => 'dummy-live-id'],
]);

// radio: each input keeps its own "value" (not overwritten with the prop value),
// and only the one matching the "choice" prop ("b") is checked
$this->assertStringContainsString('<input data-model="choice" type="radio" value="a" />', $html);
$this->assertStringContainsString('<input data-model="choice" type="radio" value="b" checked />', $html);
$this->assertStringContainsString('<input data-model="choice" type="radio" value="c" />', $html);

// checkbox group bound to an array prop: per-input "value" is kept and only the
// values contained in the array ("paris") are checked. The "selected[]" notation
// (a JS-only convention) must not crash the PropertyAccessor.
$this->assertStringContainsString('<input data-model="selected[]" type="checkbox" value="paris" checked />', $html);
$this->assertStringContainsString('<input data-model="selected[]" type="checkbox" value="lyon" />', $html);

// boolean checkbox whose "type" is hardcoded in the child template (so it never
// appears in the props): the bool prop drives "checked" without overwriting "value"
$this->assertStringContainsString('<input type="checkbox" data-model="active" checked />', $html);

// a regular text input is unaffected: its value is still set from the prop
$this->assertStringContainsString('<input data-model="name" type="text" value="John" />', $html);
}
}
Loading