Skip to content

Commit

Permalink
Merge branch '2.x' into add-component-debug-command
Browse files Browse the repository at this point in the history
  • Loading branch information
StevenRenaux authored Sep 12, 2023
2 parents 26129bf + 55a716b commit b43c18b
Show file tree
Hide file tree
Showing 100 changed files with 2,524 additions and 1,721 deletions.
27 changes: 14 additions & 13 deletions src/Autocomplete/src/Form/AutocompleteEntityTypeSubscriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,7 @@

namespace Symfony\UX\Autocomplete\Form;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Query\Parameter;
use Doctrine\ORM\Utility\PersisterHelper;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
Expand Down Expand Up @@ -54,13 +52,15 @@ public function preSubmit(FormEvent $event)
$form = $event->getForm();
$options = $form->get('autocomplete')->getConfig()->getOptions();

/** @var EntityManagerInterface $em */
$em = $options['em'];
$repository = $em->getRepository($options['class']);
$queryBuilder = $options['query_builder'] ?: $repository->createQueryBuilder('o');
$rootAlias = $queryBuilder->getRootAliases()[0];

if (!isset($data['autocomplete']) || '' === $data['autocomplete']) {
$options['choices'] = [];
} else {
/** @var EntityManagerInterface $em */
$em = $options['em'];
$repository = $em->getRepository($options['class']);

$idField = $options['id_reader']->getIdField();
$idType = PersisterHelper::getTypeOfField($idField, $em->getClassMetadata($options['class']), $em)[0];

Expand All @@ -69,22 +69,23 @@ public function preSubmit(FormEvent $event)
$idx = 0;

foreach ($data['autocomplete'] as $id) {
$params[":id_$idx"] = new Parameter("id_$idx", $id, $idType);
$params[":id_$idx"] = [$id, $idType];
++$idx;
}

$queryBuilder = $repository->createQueryBuilder('o');

if ($params) {
$queryBuilder
->where(sprintf("o.$idField IN (%s)", implode(', ', array_keys($params))))
->setParameters(new ArrayCollection($params));
->andWhere(sprintf("$rootAlias.$idField IN (%s)", implode(', ', array_keys($params))))
;
foreach ($params as $key => $param) {
$queryBuilder->setParameter($key, $param[0], $param[1]);
}
}

$options['choices'] = $queryBuilder->getQuery()->getResult();
} else {
$options['choices'] = $repository->createQueryBuilder('o')
->where("o.$idField = :id")
$options['choices'] = $queryBuilder
->andWhere("$rootAlias.$idField = :id")
->setParameter('id', $data['autocomplete'], $idType)
->getQuery()
->getResult();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,21 +46,34 @@ public function testCategoryFieldSubmitsCorrectly()
$firstCat = CategoryFactory::createOne(['name' => 'First cat']);
CategoryFactory::createOne(['name' => 'in space']);
CategoryFactory::createOne(['name' => 'ate pizza']);
$fooCat = CategoryFactory::createOne(['name' => 'I contain "foo" which CategoryAutocompleteType uses its query_builder option.']);

$this->browser()
->throwExceptions()
->get('/test-form')
// the field renders empty (but the placeholder is there)
->assertElementCount('#product_category_autocomplete option', 1)
->assertNotContains('First cat')

->post('/test-form', [
'body' => [
'product' => ['category' => ['autocomplete' => $firstCat->getId()]],
],
])
// the option does NOT match something returned by query_builder
// so ONLY the placeholder shows up
->assertElementCount('#product_category_autocomplete option', 1)
->assertNotContains('First cat')

->assertNotContains('First cat')
->post('/test-form', [
'body' => [
'product' => ['category' => ['autocomplete' => $fooCat->getId()]],
],
])
// the one option + placeholder now shows up
->assertElementCount('#product_category_autocomplete option', 2)
->assertContains('First cat')
->assertContains('which CategoryAutocompleteType uses')
;
}

Expand Down
1 change: 1 addition & 0 deletions src/LiveComponent/assets/dist/live_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -2154,6 +2154,7 @@ class RequestBuilder {
const fetchOptions = {};
fetchOptions.headers = {
Accept: 'application/vnd.live-component+html',
'X-Requested-With': 'XMLHttpRequest',
};
const totalFiles = Object.entries(files).reduce((total, current) => total + current.length, 0);
const hasFingerprints = Object.keys(children).length > 0;
Expand Down
1 change: 1 addition & 0 deletions src/LiveComponent/assets/src/Backend/RequestBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export default class {
const fetchOptions: RequestInit = {};
fetchOptions.headers = {
Accept: 'application/vnd.live-component+html',
'X-Requested-With': 'XMLHttpRequest',
};

const totalFiles = Object.entries(files).reduce(
Expand Down
5 changes: 5 additions & 0 deletions src/LiveComponent/assets/test/Backend/RequestBuilder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ describe('buildRequest', () => {
expect(fetchOptions.method).toEqual('GET');
expect(fetchOptions.headers).toEqual({
Accept: 'application/vnd.live-component+html',
'X-Requested-With': 'XMLHttpRequest',
});
});

Expand All @@ -38,6 +39,7 @@ describe('buildRequest', () => {
expect(fetchOptions.headers).toEqual({
Accept: 'application/vnd.live-component+html',
'X-CSRF-TOKEN': '_the_csrf_token',
'X-Requested-With': 'XMLHttpRequest',
});
const body = <FormData>fetchOptions.body;
expect(body).toBeInstanceOf(FormData);
Expand Down Expand Up @@ -100,6 +102,7 @@ describe('buildRequest', () => {
expect(fetchOptions.headers).toEqual({
// no token
Accept: 'application/vnd.live-component+html',
'X-Requested-With': 'XMLHttpRequest',
});
const body = <FormData>fetchOptions.body;
expect(body).toBeInstanceOf(FormData);
Expand Down Expand Up @@ -180,6 +183,7 @@ describe('buildRequest', () => {
expect(fetchOptions.headers).toEqual({
Accept: 'application/vnd.live-component+html',
'X-CSRF-TOKEN': '_the_csrf_token',
'X-Requested-With': 'XMLHttpRequest',
});
const body = <FormData>fetchOptions.body;
expect(body).toBeInstanceOf(FormData);
Expand All @@ -204,6 +208,7 @@ describe('buildRequest', () => {
expect(fetchOptions.headers).toEqual({
Accept: 'application/vnd.live-component+html',
'X-CSRF-TOKEN': '_the_csrf_token',
'X-Requested-With': 'XMLHttpRequest',
});
const body = <FormData>fetchOptions.body;
expect(body).toBeInstanceOf(FormData);
Expand Down
18 changes: 9 additions & 9 deletions src/LiveComponent/doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1087,7 +1087,7 @@ the ``#[LiveArg()]`` attribute::
}

Normally, the argument name in PHP - e.g. ``$id`` - should match the
argument named used in Twig ``id={{ item.id }}``. But if they don't
argument name used in Twig ``id={{ item.id }}``. But if they don't
match, you can pass an argument to ``LiveArg``, like we did with ``itemName``.

Actions and CSRF Protection
Expand Down Expand Up @@ -1153,7 +1153,7 @@ the component now extends ``AbstractController``! That is totally
allowed, and gives you access to all of your normal controller
shortcuts. We even added a flash message!

.. _files
.. _files:

Uploading files
---------------
Expand All @@ -1172,7 +1172,7 @@ to handle the files and tell the component when the file should be sent:
<button data-action="live#action" data-action-name="files|my_action" />
</p>

To send a file (or files) with an action use `files` modifier.
To send a file (or files) with an action use ``files`` modifier.
Without an argument it will send all pending files to your action.
You can also specify a modifier parameter to choose which files should be upload.

Expand All @@ -1191,7 +1191,7 @@ You can also specify a modifier parameter to choose which files should be upload
<button data-action="live#action" data-action-name="files|myAction" />
</p>

The files will be available in a regular `$request->files` files bag::
The files will be available in a regular ``$request->files`` files bag::

// src/Components/FileUpload.php
namespace App\Components;
Expand Down Expand Up @@ -1219,8 +1219,8 @@ The files will be available in a regular `$request->files` files bag::
.. tip::

Remember that in order to send multiple files from a single input you
need to specify `multiple` attribute on HTML element and end `name`
with `[]`.
need to specify ``multiple`` attribute on HTML element and end ``name``
with ``[]``.

.. _forms:

Expand Down Expand Up @@ -2266,7 +2266,7 @@ There are three ways to emit an event:
Listen to Events
~~~~~~~~~~~~~~~~

To listen to an event, add a method with a `#[LiveListener]` above it::
To listen to an event, add a method with a ``#[LiveListener]`` above it::

#[LiveProp]
public int $productCount = 0;
Expand All @@ -2280,7 +2280,7 @@ To listen to an event, add a method with a `#[LiveListener]` above it::
Thanks to this, when any other component emits the ``productAdded`` event, an Ajax
call will be made to call this method and re-render the component.

Behind the scenes, event listeners are also `LiveActions <actions>`, so you can
Behind the scenes, event listeners are also ``LiveActions <actions>``, so you can
autowire any services you need.

Passing Data to Listeners
Expand All @@ -2299,7 +2299,7 @@ You can also pass extra (scalar) data to the listeners::
}

In your listeners, you can access this by adding a matching argument
name with `#[LiveArg]` in front::
name with ``#[LiveArg]`` in front::

#[LiveListener('productAdded')]
public function incrementProductCount(#[LiveArg] int $product)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
use Symfony\UX\LiveComponent\Twig\TemplateMap;
use Symfony\UX\LiveComponent\Util\ChildComponentPartialRenderer;
use Symfony\UX\LiveComponent\Util\FingerprintCalculator;
use Symfony\UX\LiveComponent\Util\LiveComponentStack;
use Symfony\UX\LiveComponent\Util\LiveControllerAttributesCreator;
use Symfony\UX\LiveComponent\Util\TwigAttributeHelperFactory;
use Symfony\UX\TwigComponent\ComponentFactory;
Expand Down Expand Up @@ -113,7 +114,7 @@ function (ChildDefinition $definition, AsLiveComponent $attribute) {
$container->register('ux.live_component.event_listener.data_model_props_subscriber', DataModelPropsSubscriber::class)
->addTag('kernel.event_subscriber')
->setArguments([
new Reference('ux.twig_component.component_stack'),
new Reference('ux.twig_component.live_component_stack'),
new Reference('property_accessor'),
])
;
Expand All @@ -130,10 +131,16 @@ function (ChildDefinition $definition, AsLiveComponent $attribute) {
$container->register('ux.live_component.live_responder', LiveResponder::class);
$container->setAlias(LiveResponder::class, 'ux.live_component.live_responder');

$container->register('ux.live_component.intercept_child_component_render_subscriber', InterceptChildComponentRenderSubscriber::class)
$container->register('ux.twig_component.live_component_stack', LiveComponentStack::class)
->setArguments([
new Reference('ux.twig_component.component_stack'),
])
;

$container->register('ux.live_component.intercept_child_component_render_subscriber', InterceptChildComponentRenderSubscriber::class)
->setArguments([
new Reference('ux.twig_component.live_component_stack'),
])
->addTag('container.service_subscriber', ['key' => DeterministicTwigIdCalculator::class, 'id' => 'ux.live_component.deterministic_id_calculator'])
->addTag('container.service_subscriber', ['key' => ChildComponentPartialRenderer::class, 'id' => 'ux.live_component.child_component_partial_renderer'])
->addTag('kernel.event_subscriber');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\UX\LiveComponent\Util\LiveComponentStack;
use Symfony\UX\LiveComponent\Util\ModelBindingParser;
use Symfony\UX\TwigComponent\ComponentStack;
use Symfony\UX\TwigComponent\Event\PreMountEvent;

/**
Expand All @@ -31,7 +31,7 @@ final class DataModelPropsSubscriber implements EventSubscriberInterface
{
private ModelBindingParser $modelBindingParser;

public function __construct(private ComponentStack $componentStack, private PropertyAccessorInterface $propertyAccessor)
public function __construct(private LiveComponentStack $componentStack, private PropertyAccessorInterface $propertyAccessor)
{
$this->modelBindingParser = new ModelBindingParser();
}
Expand All @@ -54,8 +54,9 @@ public function onPreMount(PreMountEvent $event): void
unset($data['dataModel']);
$data['data-model'] = $dataModel;

// the parent is still listed as the "current" component at this point
$parentMountedComponent = $this->componentStack->getCurrentComponent();
// find the first parent of the component about to be rendered that is a Live Component
// only those can have properties controlled via the data-model attribute
$parentMountedComponent = $this->componentStack->getCurrentLiveComponent();
if (null === $parentMountedComponent) {
throw new \LogicException('You can only pass "data-model" when rendering a component when you\'re rendering inside of a parent component.');
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
use Symfony\Contracts\Service\ServiceSubscriberInterface;
use Symfony\UX\LiveComponent\Twig\DeterministicTwigIdCalculator;
use Symfony\UX\LiveComponent\Util\ChildComponentPartialRenderer;
use Symfony\UX\LiveComponent\Util\LiveComponentStack;
use Symfony\UX\LiveComponent\Util\LiveControllerAttributesCreator;
use Symfony\UX\TwigComponent\ComponentStack;
use Symfony\UX\TwigComponent\Event\PreCreateForRenderEvent;

/**
Expand All @@ -34,15 +34,15 @@ class InterceptChildComponentRenderSubscriber implements EventSubscriberInterfac
public const CHILDREN_FINGERPRINTS_METADATA_KEY = 'children_fingerprints';

public function __construct(
private ComponentStack $componentStack,
private LiveComponentStack $componentStack,
private ContainerInterface $container,
) {
}

public function preComponentCreated(PreCreateForRenderEvent $event): void
{
// if there is already a component, that's a parent. Else, this is not a child.
if (null === $parentComponent = $this->componentStack->getCurrentComponent()) {
if (null === $parentComponent = $this->componentStack->getCurrentLiveComponent()) {
return;
}

Expand Down
54 changes: 54 additions & 0 deletions src/LiveComponent/src/Util/LiveComponentStack.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

declare(strict_types=1);

namespace Symfony\UX\LiveComponent\Util;

use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\TwigComponent\ComponentStack;
use Symfony\UX\TwigComponent\MountedComponent;

/**
* This class decorates the TwigComponent\ComponentStack adding specific Live component functionalities.
*
* @author Bart Vanderstukken <[email protected]>
*
* @internal
*/
final class LiveComponentStack extends ComponentStack
{
public function __construct(private readonly ComponentStack $componentStack)
{
}

public function getCurrentLiveComponent(): ?MountedComponent
{
foreach ($this->componentStack as $mountedComponent) {
if ($this->isLiveComponent($mountedComponent->getComponent()::class)) {
return $mountedComponent;
}
}

return null;
}

private function isLiveComponent(string $classname): bool
{
return [] !== (new \ReflectionClass($classname))->getAttributes(AsLiveComponent::class);
}

public function getCurrentComponent(): ?MountedComponent
{
return $this->componentStack->getCurrentComponent();
}

public function getParentComponent(): ?MountedComponent
{
return $this->componentStack->getParentComponent();
}

public function hasParentComponent(): bool
{
return $this->componentStack->hasParentComponent();
}
}
22 changes: 22 additions & 0 deletions src/LiveComponent/tests/Fixtures/Component/InputComponent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?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\LiveComponent\Tests\Fixtures\Component;

use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;

/**
* @author Bart Vanderstukken <[email protected]>
*/
#[AsTwigComponent('input_component')]
final class InputComponent
{
}
Loading

0 comments on commit b43c18b

Please sign in to comment.