Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

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-turbo

After 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-component

A 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-component

A 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.

Thanks for reading!

Donate to the site — we work hard for you.