Skip to content

Commit 53d04cb

Browse files
committed
Send live action arguments to backend
1 parent 39d4e49 commit 53d04cb

File tree

7 files changed

+164
-5
lines changed

7 files changed

+164
-5
lines changed

src/LiveComponent/assets/dist/live_controller.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1070,7 +1070,7 @@ class default_1 extends Controller {
10701070
directives.forEach((directive) => {
10711071
const _executeAction = () => {
10721072
this._clearWaitingDebouncedRenders();
1073-
this._makeRequest(directive.action);
1073+
this._makeRequest(directive.action, directive.named);
10741074
};
10751075
let handled = false;
10761076
directive.modifiers.forEach((modifier) => {
@@ -1162,11 +1162,14 @@ class default_1 extends Controller {
11621162
}, this.debounceValue || DEFAULT_DEBOUNCE);
11631163
}
11641164
}
1165-
_makeRequest(action) {
1165+
_makeRequest(action, values) {
11661166
const splitUrl = this.urlValue.split('?');
11671167
let [url] = splitUrl;
11681168
const [, queryString] = splitUrl;
11691169
const params = new URLSearchParams(queryString || '');
1170+
if (typeof values === 'object' && Object.keys(values).length > 0) {
1171+
params.set('values', new URLSearchParams(values).toString());
1172+
}
11701173
const fetchOptions = {};
11711174
fetchOptions.headers = {
11721175
'Accept': 'application/vnd.live-component+json',

src/LiveComponent/assets/src/live_controller.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ export default class extends Controller {
144144
// taking precedence
145145
this._clearWaitingDebouncedRenders();
146146

147-
this._makeRequest(directive.action);
147+
this._makeRequest(directive.action, directive.named);
148148
}
149149

150150
let handled = false;
@@ -296,12 +296,16 @@ export default class extends Controller {
296296
}
297297
}
298298

299-
_makeRequest(action: string|null) {
299+
_makeRequest(action: string|null, values: Record<string,unknown>) {
300300
const splitUrl = this.urlValue.split('?');
301301
let [url] = splitUrl
302302
const [, queryString] = splitUrl;
303303
const params = new URLSearchParams(queryString || '');
304304

305+
if (typeof values === 'object' && Object.keys(values).length > 0) {
306+
params.set('values', new URLSearchParams(values).toString());
307+
}
308+
305309
const fetchOptions: RequestInit = {};
306310
fetchOptions.headers = {
307311
'Accept': 'application/vnd.live-component+json',

src/LiveComponent/assets/test/controller/action.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ describe('LiveController Action Tests', () => {
3535
data-action="live#action"
3636
data-action-name="save"
3737
>Save</button>
38+
39+
<button data-action="live#action" data-action-name="sendNamedArgs(a=1, b=2, c=3)">Send named args</button>
3840
</div>
3941
`;
4042

@@ -64,4 +66,15 @@ describe('LiveController Action Tests', () => {
6466

6567
expect(postMock.lastOptions().body.get('comments')).toEqual('hi WEAVER');
6668
});
69+
70+
it('Sends action named args', async () => {
71+
const data = { comments: 'hi' };
72+
const { element } = await startStimulus(template(data));
73+
74+
fetchMock.postOnce('http://localhost/_components/my_component/sendNamedArgs?values=a%3D1%26b%3D2%26c%3D3', {
75+
html: template({ comments: 'hi' }),
76+
});
77+
78+
getByText(element, 'Send named args').click();
79+
});
6780
});
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\UX\LiveComponent\ArgumentResolver;
13+
14+
use Symfony\Component\HttpFoundation\Request;
15+
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface;
16+
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
17+
18+
/**
19+
* @author Tomas Norkūnas <norkunas.tom@gmail.com>
20+
*
21+
* @experimental
22+
*/
23+
class LiveActionArgumentValueResolver implements ArgumentValueResolverInterface
24+
{
25+
/**
26+
* {@inheritdoc}
27+
*/
28+
public function supports(Request $request, ArgumentMetadata $argument): bool
29+
{
30+
if ($argument->isVariadic() || !$request->attributes->has('_live_values')) {
31+
return false;
32+
}
33+
34+
return isset($request->attributes->get('_live_values')[$argument->getName()]);
35+
}
36+
37+
public function resolve(Request $request, ArgumentMetadata $argument): iterable
38+
{
39+
yield $request->attributes->get('_live_values')[$argument->getName()];
40+
}
41+
}

src/LiveComponent/src/DependencyInjection/LiveComponentExtension.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use Symfony\Component\DependencyInjection\ContainerBuilder;
1717
use Symfony\Component\DependencyInjection\Extension\Extension;
1818
use Symfony\Component\DependencyInjection\Reference;
19+
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\LiveActionArgumentValueResolver;
1920
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
2021
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
2122
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
@@ -93,5 +94,8 @@ function (ChildDefinition $definition, AsLiveComponent $attribute) {
9394
;
9495

9596
$container->setAlias(ComponentValidatorInterface::class, ComponentValidator::class);
97+
98+
$container->register(LiveActionArgumentValueResolver::class)
99+
->addTag('controller.argument_value_resolver', ['priority' => -200]);
96100
}
97101
}

src/LiveComponent/src/EventListener/LiveComponentSubscriber.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ public function onKernelController(ControllerEvent $event): void
144144
// extra variables to be made available to the controller
145145
// (for "actions" only)
146146
parse_str($request->query->get('values'), $values);
147-
$request->attributes->add($values);
147+
$request->attributes->set('_live_values', $values);
148148
$request->attributes->set('_component', $component);
149149
}
150150

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\UX\LiveComponent\Tests\Unit\ArgumentResolver;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\HttpFoundation\Request;
16+
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
17+
use Symfony\UX\LiveComponent\ArgumentResolver\LiveActionArgumentValueResolver;
18+
19+
final class LiveActionArgumentValueResolverTest extends TestCase
20+
{
21+
/**
22+
* @dataProvider provideTestSupportsData
23+
*/
24+
public function testSupports(Request $request, ArgumentMetadata $metadata, bool $expectedSupport)
25+
{
26+
$resolver = new LiveActionArgumentValueResolver();
27+
28+
self::assertSame($expectedSupport, $resolver->supports($request, $metadata));
29+
}
30+
31+
public function provideTestSupportsData(): iterable
32+
{
33+
yield 'unsupported no values' => [
34+
self::createRequest(),
35+
self::createArgumentMetadata('test', 'string'),
36+
false,
37+
];
38+
39+
yield 'unsupported empty values' => [
40+
self::createRequest(['_live_values' => []]),
41+
self::createArgumentMetadata('test', 'string'),
42+
false,
43+
];
44+
45+
yield 'unsupported variadic' => [
46+
self::createRequest(['_live_values' => ['test' => '1']]),
47+
self::createArgumentMetadata('test', 'string', true),
48+
false,
49+
];
50+
51+
yield 'supported argument' => [
52+
self::createRequest(['_live_values' => ['test' => '1']]),
53+
self::createArgumentMetadata('test', 'string'),
54+
true,
55+
];
56+
}
57+
58+
/**
59+
* @dataProvider provideTestResolveData
60+
*/
61+
public function testResolve(Request $request, ArgumentMetadata $metadata, $expected)
62+
{
63+
$resolver = new LiveActionArgumentValueResolver();
64+
/** @var \Generator $results */
65+
$results = $resolver->resolve($request, $metadata);
66+
67+
self::assertSame($expected, iterator_to_array($results));
68+
}
69+
70+
public function provideTestResolveData(): iterable
71+
{
72+
yield 'resolves string value' => [
73+
self::createRequest(['_live_values' => ['test' => '1']]),
74+
self::createArgumentMetadata('test', 'string'),
75+
['1'],
76+
];
77+
78+
yield 'resolves array value' => [
79+
self::createRequest(['_live_values' => ['test' => ['inner' => '1']]]),
80+
self::createArgumentMetadata('test', 'array'),
81+
[['inner' => '1']],
82+
];
83+
}
84+
85+
private static function createRequest(array $attributes = []): Request
86+
{
87+
return new Request([], [], $attributes);
88+
}
89+
90+
private static function createArgumentMetadata(string $name, string $type, bool $variadic = false): ArgumentMetadata
91+
{
92+
return new ArgumentMetadata($name, $type, $variadic, false, null);
93+
}
94+
}

0 commit comments

Comments
 (0)