Turbo and live components
A live search, a form without a reload, updating part of a page usually mean a lot of JavaScript. The Symfony UX family of packages gives the same with Twig and PHP: the page stays server-rendered but behaves like an app.
Turbo
composer require symfony/ux-turboAfter installation, following links and submitting forms stop reloading the whole page: Turbo fetches the new page in the background and replaces the contents of body. No code changes are needed.
One condition: after a successful form submission the controller must redirect, and on validation errors answer with a 422 code. A Symfony form does that by itself when passed to render().
Turbo Frames
A frame updates only its own part of the page:
<turbo-frame id="comments">
{% for comment in comments %}
<p>{{ comment.text }}</p>
{% endfor %}
<a href="{{ path('post_comments', {id: post.id, page: page + 1}) }}">More</a>
</turbo-frame>A click on a link inside the frame loads a new page but replaces only the element with the same id. The rest of the page is untouched.
Twig components
composer require symfony/ux-twig-componentA component is a class and a template that together describe a piece of the interface:
// src/Twig/Components/Alert.php
namespace App\Twig\Components;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
#[AsTwigComponent]
class Alert
{
public string $type = 'info';
public string $message;
}{# templates/components/Alert.html.twig #}
<div class="alert alert-{{ type }}">{{ message }}</div><twig:Alert type="success" message="The post was saved" />A live component
composer require symfony/ux-live-componentA live component is re-rendered on the server when its data changes:
// src/Twig/Components/ProductSearch.php
namespace App\Twig\Components;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveProp;
use Symfony\UX\LiveComponent\DefaultActionTrait;
#[AsLiveComponent]
class ProductSearch
{
use DefaultActionTrait;
#[LiveProp(writable: true)]
public string $query = '';
public function __construct(private ProductRepository $products) {}
public function getProducts(): array
{
return $this->products->search($this->query);
}
}{# templates/components/ProductSearch.html.twig #}
<div {{ attributes }}>
<input type="search" data-model="query" placeholder="Search">
<ul>
{% for product in this.products %}
<li>{{ product.title }}</li>
{% endfor %}
</ul>
</div><twig:ProductSearch />The visitor types — the component sends a request, the server runs the search and returns new markup. Not a line of your own JavaScript.
Actions
A method with the #[LiveAction] attribute can be called from the template:
use Symfony\UX\LiveComponent\Attribute\LiveAction;
#[LiveProp]
public int $count = 0;
#[LiveAction]
public function increment(): void
{
$this->count++;
}<button data-action="live#action" data-live-action-param="increment">+1</button>
<span>{{ count }}</span>Common mistake
A property with writable: true can be changed by the visitor — including to a value the interface does not offer. Do not make identifiers, prices or roles writable, and check rights in actions just as you would in a controller. Properties without writable are protected by a signature and cannot be forged.