Skip to content

Commit 9725943

Browse files
authored
Merge branch '2.x' into add-component-debug-command
2 parents d93c97b + 976b53f commit 9725943

File tree

148 files changed

+1267
-586
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

148 files changed

+1267
-586
lines changed

.github/workflows/doctor-rst.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
run: mkdir .cache
1717

1818
- name: Extract base branch name
19-
run: echo "##[set-output name=branch;]$(echo ${GITHUB_BASE_REF:=${GITHUB_REF##*/}})"
19+
run: echo "branch=$(echo ${GITHUB_BASE_REF:=${GITHUB_REF##*/}})" >> $GITHUB_OUTPUT
2020
id: extract_base_branch
2121

2222
- name: Cache DOCtor-RST

.github/workflows/test-turbo.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ jobs:
99
runs-on: ubuntu-latest
1010
steps:
1111
- name: Checkout
12-
uses: actions/checkout@v2
12+
uses: actions/checkout@v3
1313

1414
- name: Setup PHP
1515
uses: shivammathur/setup-php@v2
@@ -52,7 +52,7 @@ jobs:
5252

5353
steps:
5454
- name: Checkout
55-
uses: actions/checkout@v2
55+
uses: actions/checkout@v3
5656

5757
- name: Setup PHP
5858
uses: shivammathur/setup-php@v2
@@ -69,7 +69,7 @@ jobs:
6969
id: yarn-cache-dir-path
7070
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
7171

72-
- uses: actions/cache@v2
72+
- uses: actions/cache@v3
7373
id: yarn-cache
7474
with:
7575
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}

.github/workflows/test.yaml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ jobs:
88
coding-style-php:
99
runs-on: ubuntu-latest
1010
steps:
11-
- uses: actions/checkout@master
11+
- uses: actions/checkout@v3
1212
- uses: shivammathur/setup-php@v2
1313
with:
1414
php-version: '8.1'
@@ -20,11 +20,11 @@ jobs:
2020
name: JavaScript Coding Style
2121
runs-on: ubuntu-latest
2222
steps:
23-
- uses: actions/checkout@master
23+
- uses: actions/checkout@v3
2424
- name: Get yarn cache directory path
2525
id: yarn-cache-dir-path
2626
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
27-
- uses: actions/cache@v2
27+
- uses: actions/cache@v3
2828
id: yarn-cache
2929
with:
3030
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
@@ -39,11 +39,11 @@ jobs:
3939
name: Check for UnBuilt JS Dist Files
4040
runs-on: ubuntu-latest
4141
steps:
42-
- uses: actions/checkout@master
42+
- uses: actions/checkout@v3
4343
- name: Get yarn cache directory path
4444
id: yarn-cache-dir-path
4545
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
46-
- uses: actions/cache@v2
46+
- uses: actions/cache@v3
4747
id: yarn-cache
4848
with:
4949
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
@@ -124,11 +124,11 @@ jobs:
124124
tests-js:
125125
runs-on: ubuntu-latest
126126
steps:
127-
- uses: actions/checkout@master
127+
- uses: actions/checkout@v3
128128
- name: Get yarn cache directory path
129129
id: yarn-cache-dir-path
130130
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
131-
- uses: actions/cache@v2
131+
- uses: actions/cache@v3
132132
id: yarn-cache
133133
with:
134134
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}

src/Autocomplete/tests/Functional/AutocompleteFormRenderingTest.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,6 @@ public function testCategoryFieldSubmitsCorrectly()
6363
// the option does NOT match something returned by query_builder
6464
// so ONLY the placeholder shows up
6565
->assertElementCount('#product_category_autocomplete option', 1)
66-
->assertNotContains('First cat')
67-
6866
->assertNotContains('First cat')
6967
->post('/test-form', [
7068
'body' => [

src/LiveComponent/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# CHANGELOG
22

3+
## 2.12.0
4+
5+
- Add `onUpdated` hook for `LiveProp`
6+
37
## 2.11.0
48

59
- Add helper for testing live components.

src/LiveComponent/doc/index.rst

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1158,9 +1158,9 @@ shortcuts. We even added a flash message!
11581158
Uploading files
11591159
---------------
11601160

1161-
.. versionadded:: 2.9
1161+
.. versionadded:: 2.11
11621162

1163-
The ability to upload files to actions was added in version 2.9.
1163+
The ability to upload files to actions was added in version 2.11.
11641164

11651165
Files aren't sent to the component by default. You need to use a live action
11661166
to handle the files and tell the component when the file should be sent:
@@ -3063,6 +3063,62 @@ Then specify this new route on your component:
30633063
use DefaultActionTrait;
30643064
}
30653065
3066+
Add a Hook on LiveProp Update
3067+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3068+
3069+
.. versionadded:: 2.12
3070+
3071+
The ``onUpdated`` option was added in LiveComponents 2.12.
3072+
3073+
If you want to run custom code after a specific LiveProp is updated,
3074+
you can do it by adding an ``onUpdated`` option set to a public method name
3075+
on the component:
3076+
3077+
.. code-block:: diff
3078+
3079+
// ...
3080+
3081+
#[AsLiveComponent]
3082+
class ProductSearch
3083+
{
3084+
- #[LiveProp(writable: true)]
3085+
+ #[LiveProp(writable: true, onUpdated: 'onQueryUpdated')]
3086+
public string $query = '';
3087+
3088+
// ...
3089+
3090+
public function onQueryUpdated($previousValue): void
3091+
{
3092+
// $this->query already contains a new value
3093+
// and its previous value is passed as an argument
3094+
}
3095+
}
3096+
}
3097+
3098+
As soon as the `query` LiveProp is updated, the ``onQueryUpdated()`` method
3099+
will be called. The previous value is passed there as the first argument.
3100+
3101+
If you're allowing object properties to be writable, you can also listen to
3102+
the change of one specific key:
3103+
3104+
.. code-block::
3105+
3106+
// ...
3107+
3108+
#[AsLiveComponent]
3109+
class EditPost
3110+
{
3111+
#[LiveProp(writable: ['title', 'content'], onUpdated: ['title' => 'onTitleUpdated'])]
3112+
public Post $post;
3113+
3114+
// ...
3115+
3116+
public function onTitleUpdated($previousValue): void
3117+
{
3118+
// ...
3119+
}
3120+
}
3121+
30663122
Debugging Components
30673123
--------------------
30683124

@@ -3129,6 +3185,20 @@ uses Symfony's test client to render and make requests to your components::
31293185
$response = $testComponent->call('redirect')->response(); // Symfony\Component\HttpFoundation\Response
31303186

31313187
$this->assertSame(302, $response->getStatusCode());
3188+
3189+
// authenticate a user ($user is instance of UserInterface)
3190+
$testComponent->actingAs($user);
3191+
3192+
// customize the test client
3193+
$client = self::getContainer()->get('test.client');
3194+
3195+
// do some stuff with the client (ie login user via form)
3196+
3197+
$testComponent = $this->createLiveComponent(
3198+
name: 'MyComponent',
3199+
data: ['foo' => 'bar'],
3200+
client: $client,
3201+
);
31323202
}
31333203
}
31343204

src/LiveComponent/src/Attribute/LiveProp.php

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,15 @@ final class LiveProp
4949

5050
private bool $acceptUpdatesFromParent;
5151

52+
/**
53+
* @var string|string[]|null
54+
*
55+
* A hook that will be called after the property is updated.
56+
* Set it to a method name on the Live Component that should be called.
57+
* The old value of the property will be passed as an argument to it.
58+
*/
59+
private null|string|array $onUpdated;
60+
5261
/**
5362
* @param bool|array $writable If true, this property can be changed by the frontend.
5463
* Or set to an array of paths within this object/array
@@ -73,7 +82,8 @@ public function __construct(
7382
array $serializationContext = [],
7483
string $fieldName = null,
7584
string $format = null,
76-
bool $updateFromParent = false
85+
bool $updateFromParent = false,
86+
string|array $onUpdated = null,
7787
) {
7888
$this->writable = $writable;
7989
$this->hydrateWith = $hydrateWith;
@@ -83,6 +93,7 @@ public function __construct(
8393
$this->fieldName = $fieldName;
8494
$this->format = $format;
8595
$this->acceptUpdatesFromParent = $updateFromParent;
96+
$this->onUpdated = $onUpdated;
8697

8798
if ($this->useSerializerForHydration && ($this->hydrateWith || $this->dehydrateWith)) {
8899
throw new \InvalidArgumentException('Cannot use useSerializerForHydration with hydrateWith or dehydrateWith.');
@@ -172,4 +183,9 @@ public function acceptUpdatesFromParent(): bool
172183
{
173184
return $this->acceptUpdatesFromParent;
174185
}
186+
187+
public function onUpdated(): null|string|array
188+
{
189+
return $this->onUpdated;
190+
}
175191
}

src/LiveComponent/src/LiveComponentHydrator.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
2121
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
2222
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
23+
use Symfony\UX\LiveComponent\Attribute\LiveProp;
2324
use Symfony\UX\LiveComponent\Exception\HydrationException;
2425
use Symfony\UX\LiveComponent\Hydration\HydrationExtensionInterface;
2526
use Symfony\UX\LiveComponent\Metadata\LiveComponentMetadata;
@@ -208,6 +209,10 @@ public function hydrate(object $component, array $props, array $updatedProps, Li
208209
// unexpected and can't be set - e.g. a string field for an `int` property.
209210
// We ignore this, and allow the original value to remain set.
210211
}
212+
213+
if ($propMetadata->onUpdated()) {
214+
$this->processOnUpdatedHook($component, $frontendName, $propMetadata, $dehydratedUpdatedProps, $dehydratedOriginalProps);
215+
}
211216
}
212217

213218
foreach (AsLiveComponent::postHydrateMethods($component) as $method) {
@@ -559,4 +564,52 @@ private function recursiveKeySort(array &$data): void
559564
}
560565
ksort($data);
561566
}
567+
568+
private function ensureOnUpdatedMethodExists(object $component, string $methodName): void
569+
{
570+
if (method_exists($component, $methodName)) {
571+
return;
572+
}
573+
574+
throw new \Exception(sprintf('Method "%s:%s()" specified as LiveProp "onUpdated" hook does not exist.', $component::class, $methodName));
575+
}
576+
577+
/**
578+
* A special hook that will be called if the LiveProp was changed
579+
* and $onUpdated argument is set on its attribute.
580+
*/
581+
private function processOnUpdatedHook(object $component, string $frontendName, LivePropMetadata $propMetadata, DehydratedProps $dehydratedUpdatedProps, DehydratedProps $dehydratedOriginalProps): void
582+
{
583+
$onUpdated = $propMetadata->onUpdated();
584+
if (\is_string($onUpdated)) {
585+
$onUpdated = [LiveProp::IDENTITY => $onUpdated];
586+
}
587+
588+
foreach ($onUpdated as $propName => $funcName) {
589+
if (LiveProp::IDENTITY === $propName) {
590+
if (!$dehydratedUpdatedProps->hasPropValue($frontendName)) {
591+
continue;
592+
}
593+
594+
$this->ensureOnUpdatedMethodExists($component, $funcName);
595+
$propertyOldValue = $this->hydrateValue(
596+
$dehydratedOriginalProps->getPropValue($frontendName),
597+
$propMetadata,
598+
$component,
599+
);
600+
$component->{$funcName}($propertyOldValue);
601+
602+
continue;
603+
}
604+
605+
$key = sprintf('%s.%s', $frontendName, $propName);
606+
if (!$dehydratedUpdatedProps->hasPropValue($key)) {
607+
continue;
608+
}
609+
610+
$this->ensureOnUpdatedMethodExists($component, $funcName);
611+
$propertyOldValue = $dehydratedOriginalProps->getPropValue($key);
612+
$component->{$funcName}($propertyOldValue);
613+
}
614+
}
562615
}

src/LiveComponent/src/Metadata/LivePropMetadata.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,4 +105,9 @@ public function getFormat(): ?string
105105
{
106106
return $this->liveProp->format();
107107
}
108+
109+
public function onUpdated(): null|string|array
110+
{
111+
return $this->liveProp->onUpdated();
112+
}
108113
}

src/LiveComponent/src/Test/InteractsWithLiveComponents.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\UX\LiveComponent\Test;
1313

14+
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
1415
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
1516
use Symfony\UX\TwigComponent\ComponentFactory;
1617

@@ -19,7 +20,7 @@
1920
*/
2021
trait InteractsWithLiveComponents
2122
{
22-
protected function createLiveComponent(string $name, array $data = []): TestLiveComponent
23+
protected function createLiveComponent(string $name, array $data = [], KernelBrowser $client = null): TestLiveComponent
2324
{
2425
if (!$this instanceof KernelTestCase) {
2526
throw new \LogicException(sprintf('The "%s" trait can only be used on "%s" classes.', __TRAIT__, KernelTestCase::class));
@@ -37,7 +38,7 @@ protected function createLiveComponent(string $name, array $data = []): TestLive
3738
$metadata,
3839
$data,
3940
$factory,
40-
self::getContainer()->get('test.client'),
41+
$client ?? self::getContainer()->get('test.client'),
4142
self::getContainer()->get('ux.live_component.component_hydrator'),
4243
self::getContainer()->get('ux.live_component.metadata_factory'),
4344
self::getContainer()->get('router'),

0 commit comments

Comments
 (0)