Skip to content

Commit aaca52b

Browse files
committed
Adding test to verify that data-model takes precedence over name attribute
1 parent ba3d740 commit aaca52b

File tree

2 files changed

+122
-0
lines changed

2 files changed

+122
-0
lines changed

src/LiveComponent/README.md

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1135,3 +1135,100 @@ You can also trigger a specific "action" instead of a normal re-render:
11351135
#}
11361136
>
11371137
```
1138+
1139+
## Embedded Components
1140+
1141+
It's totally possible to embed one live component into another. But when
1142+
you do, there are a few things to know:
1143+
1144+
* When the parent component is re-rendered, the child component is *not*
1145+
automatically re-rendered. Basically, each component operates 100%
1146+
independently of each other.
1147+
1148+
For example,
1149+
suppose you have an `EditPostComponent`:
1150+
1151+
```php
1152+
<?php
1153+
1154+
namespace App\Twig\Components;
1155+
1156+
use App\Entity\Post;
1157+
use Doctrine\ORM\EntityManagerInterface;
1158+
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
1159+
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
1160+
use Symfony\UX\LiveComponent\Attribute\LiveAction;
1161+
use Symfony\UX\LiveComponent\Attribute\LiveProp;
1162+
1163+
#[AsLiveComponent('edit_post')]
1164+
final class EditPostComponent extends AbstractController
1165+
{
1166+
#[LiveProp(exposed: ['title', 'content'])]
1167+
public Post $post;
1168+
1169+
#[LiveAction]
1170+
public function save(EntityManagerInterface $entityManager)
1171+
{
1172+
$entityManager->flush();
1173+
1174+
return $this->redirectToRoute('some_route');
1175+
}
1176+
}
1177+
```
1178+
1179+
And a `MarkdownTextareaComponent`:
1180+
1181+
```php
1182+
<?php
1183+
1184+
namespace App\Twig\Components;
1185+
1186+
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
1187+
use Symfony\UX\LiveComponent\Attribute\LiveProp;
1188+
1189+
#[AsLiveComponent('markdown_textarea')]
1190+
final class MarkdownTextareaComponent
1191+
{
1192+
#[LiveProp]
1193+
public string $label;
1194+
1195+
#[LiveProp]
1196+
public string $name;
1197+
1198+
#[LiveProp(writable: true)]
1199+
public string $value = '';
1200+
}
1201+
```
1202+
1203+
In the `EditPostComponent` template, you render the `MarkdownTextareaComponent`:
1204+
1205+
```twig
1206+
{# templates/components/edit_post.html.twig #}
1207+
<div {{ init_live_component(this) }}>
1208+
<input
1209+
type="text"
1210+
name="post[title]"
1211+
data-action="live#updateDefer"
1212+
value="{{ this.post.title }}"
1213+
>
1214+
1215+
{{ component('markdown_textarea', {
1216+
name: 'post[content]',
1217+
label: 'Content',
1218+
value: this.post.content
1219+
}) }}
1220+
1221+
<button
1222+
data-action="live#action"
1223+
data-action-name="save"
1224+
>Save</button>
1225+
</div>
1226+
```
1227+
1228+
The `MarkdownTextareaComponent`
1229+
1230+
When you do
1231+
this, there's nothing special to know: both components render independently.
1232+
If you're using [Live Components](https://github.com/symfony/ux-live-component),
1233+
then there *are* some guidelines related to how the re-rendering of parent
1234+
and child components works. Read [Live Embedded Components](https://github.com/symfony/ux-live-component#embedded-components).

src/LiveComponent/assets/test/controller/model.test.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,31 @@ describe('LiveController data-model Tests', () => {
138138
fetchMock.done();
139139
});
140140

141+
it('uses data-model when both name and data-model is present', async () => {
142+
const data = { name: 'Ryan' };
143+
const { element, controller } = await startStimulus(
144+
template(data),
145+
data
146+
);
147+
148+
// give element data-model="name" and name="first_name"
149+
const inputElement = getByLabelText(element, 'Name:');
150+
inputElement.setAttribute('name', 'first_name');
151+
152+
// ?name should be what's sent to the server
153+
fetchMock.getOnce('http://localhost/?name=Ryan+WEAVER', {
154+
html: template({ name: 'Ryan Weaver' }),
155+
data: { name: 'Ryan Weaver' }
156+
});
157+
158+
await userEvent.type(inputElement, ' WEAVER');
159+
160+
await waitFor(() => expect(inputElement).toHaveValue('Ryan Weaver'));
161+
expect(controller.dataValue).toEqual({name: 'Ryan Weaver'});
162+
163+
fetchMock.done();
164+
});
165+
141166
it('standardizes user[firstName] style models into post.name', async () => {
142167
const deeperModelTemplate = (data) => `
143168
<div

0 commit comments

Comments
 (0)