symfony/ux-live-component
Build interactive UIs in Symfony with Live Components: stateful Twig components that update via Ajax without writing custom JavaScript. Handle actions, validation, and form binding, with predictable server-side rendering and smooth partial updates.
## Getting Started
### Minimal Setup
1. **Installation**:
```bash
composer require symfony/ux-live-component
npm install @symfony/stimulus-bridge @symfony/ux-live-component
npm run build
Ensure StimulusBundle and UxLiveComponentBundle are enabled in config/bundles.php.
Basic Component:
// src/Components/MyComponent.php
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveProp;
#[AsLiveComponent]
class MyComponent
{
#[LiveProp]
public string $name = 'World';
public function greet(): string
{
return "Hello, {$this->name}!";
}
}
Template:
{# templates/components/my_component.html.twig #}
<h1>{{ greet() }}</h1>
<input data-model="name">
Register in Twig:
{% import '@MyComponent' as MyComponent %}
{{ MyComponent\MyComponent() }}
Trigger Live Updates:
Add the Stimulus controller to your layout (e.g., base.html.twig):
{{ stimulus_controller('live-component') }}
Create a form where fields update without full page reloads:
#[AsLiveComponent]
class UserFormComponent
{
#[LiveProp]
public User $user;
public function save(): void
{
// Save logic (e.g., $this->user->save())
$this->dispatchBrowserEvent('user-saved');
}
}
{# templates/components/user_form.html.twig #}
<form data-action="submit->live#submit">
<input data-model="user.firstName">
<input data-model="user.lastName">
<button type="submit">Save</button>
</form>
LivePropScalar Values:
#[LiveProp(writable: ['email'])]
public string $email;
<input data-model="email">
Objects/Entities:
#[LiveProp(writable: ['user'])]
public User $user;
<input data-model="user.firstName">
Arrays:
#[LiveProp(writable: ['tags'])]
public array $tags = [];
<input data-model="tags.0">
Custom Hydration:
#[LiveProp(useSerializerForHydration: true)]
public array $complexData;
Trigger Actions:
<button data-action="click->live#update--action(save)">Save</button>
public function save(): void
{
// Logic
$this->dispatchBrowserEvent('user-saved');
}
Browser Events:
public function __construct()
{
$this->dispatchBrowserEvent('custom-event', ['data' => 'value']);
}
#[AsLiveComponent]
class ParentComponent {
#[LiveProp]
public string $message;
public function notifyChild(): void
{
$this->dispatchBrowserEvent('child-update', ['message' => $this->message]);
}
}
{{ childComponent({ message: message }) }}
// In child component's Stimulus controller
this.component.on('child-update', (event) => {
this.message = event.detail.message;
});
ValidatableComponentTrait:
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\ValidatableComponentTrait;
#[AsLiveComponent]
class UserFormComponent
{
use ValidatableComponentTrait;
#[LiveProp]
public User $user;
public function submit(): void
{
if ($this->validate()) {
$this->user->save();
}
}
}
<form data-action="submit->live#submit">
<input data-model="user.email" data-error="errors.email">
{{ errors|render }}
</form>
#[AsLiveComponent(template: FromMethod('getTemplate'))]
class DynamicComponent {
public function getTemplate(): string
{
return $this->isAdmin() ? 'admin_template' : 'user_template';
}
}
#[LiveProp]
public UserType $form;
public function __construct()
{
$this->form = $this->createForm(UserType::class);
}
{{ form_start(form) }}
{{ form_widget(form) }}
{{ form_end(form) }}
#[LiveProp(writable: ['user'])]
public User $user;
public function save(): void
{
$entityManager = $this->getEntityManager();
$entityManager->persist($this->user);
$entityManager->flush();
}
// assets/controllers/my_component_controller.js
import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
connect() {
this.component.on('user-saved', () => {
alert('User saved!');
});
}
}
{{ stimulus_controller('my-component', { target: '#my-component' }) }}
#[AsLiveComponent]
class AssetComponent {
#[LiveProp]
public string $assetPath;
public function getAssetUrl(): string
{
return $this->assetMapper->getUrl($this->assetPath);
}
}
use Symfony\UX\LiveComponent\Test\InteractsWithLiveComponents;
class MyComponentTest extends TestCase {
use InteractsWithLiveComponents;
public function testComponent()
{
$component = $this->createLiveComponent(MyComponent::class);
$rendered = $component->render();
$this->assertSelectorTextContains('h1', 'Hello, World!');
$this->assertComponentDispatchBrowserEvent($rendered, 'user-saved');
}
}
X-Requested-With:
Ensure your CORS configuration includes X-Requested-With:
# config/packages/nextras_cors.yaml
nextras_cors:
paths:
'^/api/*':
allow_origin: ['*']
allow_methods: ['GET', 'POST', 'PUT', 'DELETE']
allow_headers: ['Content-Type', 'X-Requested-With']
expose_headers: ['X-Requested-With']
value attribute for boolean binding:
<input type="checkbox" data-model="isActive">
#[LiveProp(useSerializerForHydration: true)]
public array $nestedData;
#[LiveProp(writable: false)] for read-only props.
#[LiveProp(writable: false)]
public string $readonlyValue;
<button data-action="click->live#update--action(save)|norender">Save (no re-render)</button>
#[LiveProp(acceptUpdatesFromParent: true)]
public string $parentData;
How can I help you explore Laravel packages today?