mcpField
L
live-component

live-component

github.com/smnandre開発18090 回インストール19 週間前に更新
リポジトリ

live-component について

Symfony UX LiveComponent で PHP と Twig のリアクティブな UI を構築する開発者向けのリファレンススキルです。AJAX によるサーバー側の再レンダリング、状態バインディング、アクション、フォーム、コンポーネント間通信を扱い、これらの操作に JavaScript を書かずに済みます。

主な機能

  1. 01AJAX によるコンポーネント再描画
  2. 02状態の保持と書き込み
  3. 03入力の双方向バインディング
  4. 04サーバー側アクションとフォーム連携
  5. 05コンポーネント間のイベント通信

活用シーン

  1. 01入力に応じた検索結果の更新
  2. 02フォーム項目のリアルタイム検証
  3. 03入力によるリストの絞り込み
  4. 04サーバー側ロジックを使うインタラクティブなコンポーネントの構築

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/smnandre/symfony-ux-skills --skill live-component

インストール後、Claude Code で/live-componentを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

references/
api.md
gotchas.md
patterns.md

詳細

LiveComponent

AJAX 経由で動的に再レンダリングする TwigComponent です。JavaScript を一切使わず、PHP + Twig でリアクティブな UI を構築できます。ユーザーが操作するたびにサーバーとの通信が発生し、コンポーネントが再レンダリングされて DOM がモーフィングされます。

LiveComponent を使用する場面

コンポーネントの出力がユーザーの操作に依存する場合は、LiveComponent を使用してください。入力中に更新される検索結果、リアルタイム検証付きのフォーム、リストを絞り込むフィルターなど、ユーザーの入力に応じて UI を変更する必要があり、その変更にサーバー側のデータやロジックが必要となるあらゆる場面が該当します。

初回読み込み後にコンポーネントが再レンダリングされない場合は、代わりに TwigComponent を使用してください(オーバーヘッドが少なく、AJAX も不要です)。操作がクライアント側だけで完結する場合(切り替え、アニメーションなど)は、代わりに Stimulus を使用してください。

インストール

composer require symfony/ux-live-component

クイックリファレンス

#[AsLiveComponent]           Make component live (re-renderable via AJAX)
#[LiveProp]                  State that persists across re-renders
#[LiveProp(writable: true)]  State that the frontend can modify
#[LiveAction]                Server method callable from frontend
data-model="prop"            Two-way bind input to LiveProp
data-action="live#action"    Call LiveAction on event
data-loading="..."           Show/hide/style elements during AJAX
{{ attributes }}             REQUIRED on root element (wires the Stimulus controller)

基本的な例

// src/Twig/Components/Counter.php
namespace App\Twig\Components;

use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveProp;
use Symfony\UX\LiveComponent\Attribute\LiveAction;
use Symfony\UX\LiveComponent\DefaultActionTrait;

#[AsLiveComponent]
final class Counter
{
    use DefaultActionTrait;

    #[LiveProp]
    public int $count = 0;

    #[LiveAction]
    public function increment(): void
    {
        $this->count++;
    }

    #[LiveAction]
    public function decrement(): void
    {
        $this->count--;
    }
}
{# templates/components/Counter.html.twig #}
<div {{ attributes }}>
    <button data-action="live#action" data-live-action-param="decrement">-</button>
    <span>{{ count }}</span>
    <button data-action="live#action" data-live-action-param="increment">+</button>
</div>

重要: ルート要素では {{ attributes }} をレンダリングする必要があります。これにより、システム全体の動作に必要な Stimulus の data-controller="live" 属性が挿入されます。これがないと、何も再レンダリングされません。

LiveProp

AJAX による再レンダリングの間も保持される状態です。Props はフロントエンドにシリアライズされ、リクエストのたびに返送されます。

基本的な Props
#[LiveProp]
public string $query = '';

#[LiveProp]
public int $page = 1;

#[LiveProp]
public ?User $user = null;  // Entities auto-hydrate by ID
書き込み可能な Props(双方向バインディング)

data-model を通じてフロントエンドから変更できるのは、書き込み可能な Props のみです。

#[LiveProp(writable: true)]
public string $search = '';

// Writable with specific fields for objects
#[LiveProp(writable: ['email', 'name'])]
public User $user;
URL バインディング

Prop を URL クエリパラメーターと同期します。これにより、状態をブックマークしたり共有したりできます。

#[LiveProp(writable: true, url: true)]
public string $query = '';
// URL becomes: ?query=search+term

// Custom parameter name
use Symfony\UX\LiveComponent\Metadata\UrlMapping;

#[LiveProp(writable: true, url: new UrlMapping(as: 'q'))]
public string $query = '';
// URL becomes: ?q=search+term
ハイドレーション

Doctrine エンティティは ID によって自動的にハイドレーションされます。カスタム型の場合は次のようにします。

#[LiveProp(hydrateWith: 'hydrateStatus', dehydrateWith: 'dehydrateStatus')]
public Status $status;

public function hydrateStatus(string $value): Status
{
    return Status::from($value);
}

public function dehydrateStatus(Status $status): string
{
    return $status->value;
}

データバインディング(data-model)

入力を、書き込み可能な LiveProps にバインドします。入力が変更されると、新しい値でコンポーネントが再レンダリングされます。

{# Re-render on change (default) #}
<input type="text" data-model="search">

{# Debounced -- wait 300ms after last keystroke #}
<input type="text" data-model="debounce(300)|search">

{# Only update on blur #}
<input type="text" data-model="on(blur)|search">

{# Update model but don't re-render yet #}
<input type="text" data-model="norender|search">

{# Checkbox, radio, select #}
<input type="checkbox" data-model="enabled">
<select data-model="category">
    <option value="1">Category 1</option>
</select>
検証モディファイア
{# Only re-render when input meets criteria #}
<input data-model="minlength(3)|search">
<input data-model="maxlength(100)|bio">
<input data-model="min(0)|quantity">
<input data-model="max(999)|price">

LiveAction

フロントエンドから呼び出せるサーバーメソッドです。

#[LiveAction]
public function save(): void
{
    // Called via data-action="live#action" data-live-action-param="save"
}

#[LiveAction]
public function delete(#[LiveArg] int $id): void
{
    // With typed argument via data-live-id-param="123"
}
Twig からアクションを呼び出す
{# Button click #}
<button data-action="live#action" data-live-action-param="save">Save</button>

{# With arguments #}
<button
    data-action="live#action"
    data-live-action-param="delete"
    data-live-id-param="{{ item.id }}"
>Delete</button>

{# Form submit (prevent default) #}
<form data-action="live#action:prevent" data-live-action-param="submit">

検索の例(完全版)

#[AsLiveComponent]
final class ProductSearch
{
    use DefaultActionTrait;

    #[LiveProp(writable: true, url: true)]
    public string $query = '';

    #[LiveProp(writable: true)]
    public string $category = '';

    public function __construct(
        private readonly ProductRepository $products,
    ) {}

    public function getProducts(): array
    {
        return $this->products->search($this->query, $this->category);
    }
}
<div {{ attributes }}>
    <input type="search" data-model="debounce(300)|query" placeholder="Search...">

    <select data-model="category">
        <option value="">All Categories</option>
        {% for cat in categories %}
            <option value="{{ cat.id }}">{{ cat.name }}</option>
        {% endfor %}
    </select>

    <div data-loading="addClass(opacity-50)">
        {% for product in this.products %}
            <div>{{ product.name }}</div>
        {% endfor %}
    </div>
</div>

読み込み状態

AJAX による再レンダリング中に、視覚的なフィードバックを表示します。

{# Add/remove class while loading #}
<div data-loading="addClass(opacity-50)">
<div data-loading="removeClass(hidden)">

{# Show/hide element while loading #}
<span data-loading="show">Loading...</span>
<div data-loading="hide">Content</div>

{# Disable button while loading #}
<button data-loading="attr(disabled)">Submit</button>

{# Scoped to specific action or model #}
<span data-loading="action(save)|show">Saving...</span>
<span data-loading="model(query)|show">Searching...</span>

{# Delay before showing (avoid flicker on fast responses) #}
<span data-loading="delay(300)|show">Loading...</span>

フォームとの統合

use Symfony\Component\Form\FormInterface;
use Symfony\UX\LiveComponent\ComponentWithFormTrait;

#[AsLiveComponent]
final class RegistrationForm extends AbstractController
{
    use DefaultActionTrait;
    use ComponentWithFormTrait;

    #[LiveProp]
    public ?User $initialFormData = null;

    protected function instantiateForm(): FormInterface
    {
        return $this->createForm(UserType::class, $this->initialFormData);
    }

    #[LiveAction]
    public function save(EntityManagerInterface $em): Response
    {
        $this->submitForm();
        $user = $this->getForm()->getData();
        $em->persist($user);
        $em->flush();

        return $this->redirectToRoute('app_success');
    }
}
<div {{ attributes }}>
    {{ form_start(form, {
        attr: {
            'data-action': 'live#action:prevent',
            'data-live-action-param': 'save'
        }
    }) }}

    {{ form_row(form.email) }}
    {{ form_row(form.password) }}

    <button type="submit" data-loading="attr(disabled)">Register</button>

    {{ form_end(form) }}
</div>
リアルタイム検証
{{ form_row(form.email, {
    attr: {'data-model': 'on(blur)|validatedFields'}
}) }}

コンポーネント間の通信

イベントの発行(子から親へ)
use Symfony\UX\LiveComponent\ComponentToolsTrait;

#[AsLiveComponent]
final class ChildComponent
{
    use DefaultActionTrait;
    use ComponentToolsTrait;

    #[LiveAction]
    public function save(): void
    {
        // ... save logic
        $this->emit('itemSaved', ['id' => $this->item->getId()]);
    }
}
イベントのリッスン(親)
use Symfony\UX\LiveComponent\Attribute\LiveListener;

#[AsLiveComponent]
final class ParentComponent
{
    use DefaultActionTrait;

    #[LiveListener('itemSaved')]
    public function onItemSaved(#[LiveArg] int $id): void
    {
        // Component re-renders automatically after this method
    }
}
ブラウザーイベント(LiveComponent から Stimulus へ)
$this->dispatchBrowserEvent('modal:close');

<div data-action="modal:close@window->modal#close">

ポーリング

タイマーでコンポーネントを自動更新します。

{# Default: every 2 seconds #}
<div {{ attributes }} data-poll>

{# Custom interval #}
<div {{ attributes }} data-poll="delay(5000)">

{# Call specific action on each poll #}
<div {{ attributes }} data-poll="action(refresh)">

遅延/後回しの読み込み

{# Load component after page renders (deferred AJAX call) #}
<twig:HeavyComponent defer />

{# Load when element scrolls into viewport (IntersectionObserver) #}
<twig:HeavyComponent lazy />

{# Placeholder while loading #}
<twig:HeavyComponent lazy>
    <div>Loading...</div>
</twig:HeavyComponent>

データの保持

{# Prevent re-render from modifying this subtree #}
<div data-live-ignore>
    {# Third-party widget, contenteditable, etc. #}
</div>

{# Preserve specific attribute during DOM morph #}
<input data-live-preserve="value">

算出プロパティ

TwigComponent と同様に、getXxx() メソッドには this.xxx としてアクセスできます。1 回のレンダリングサイクル内でキャッシュするには computed.xxx を使用してください(ループ内でメソッドが複数回呼び出されるのを防げます)。

public function getFilteredItems(): array
{
    return array_filter($this->items, fn($i) => $i->isActive());
}
{# Uncached -- called each time #}
{% for item in this.filteredItems %}

{# Cached within this render #}
{% for item in computed.filteredItems %}

重要な原則

すべての操作でサーバーとの通信が発生します。 LiveComponent はクライアント側のフレームワークではありません。再レンダリングのたびに、コンポーネントの状態全体がサーバーに送信され、PHP が再実行されて DOM がモーフィングされます。ドラッグ&ドロップやリアルタイム描画など、頻繁に発生する操作には、代わりに Stimulus を使用してください。

コンポーネントは小さく保ってください。 LiveProps が多く、テンプレートが複雑な大規模コンポーネントは再レンダリングが遅くなります。イベントの発行/リッスンを通じて連携する、小さくて目的を絞ったコンポーネントに分割してください。

norender と on(blur) を使ってリクエストを減らしてください。 キー入力のたびにサーバーを呼び出す必要はありません。テキスト入力にはデバウンスを設定し、即時のフィードバックが不要なフィールドでは、フォーカスが外れたときのイベントにバインディングを遅延させてください。

ルート要素の {{ attributes }} は必須です。 これがないと、ライブ動作を担う Stimulus コントローラーがアタッチされず、何も機能しません。

参考資料

  • 完全な API(props、アクション、フォーム、イベント、すべてのオプション):references/api.md
  • パターン(検索、CRUD、モーダル、検証、実用例):references/patterns.md
  • 注意点(props、ハイドレーション、パフォーマンス、よくある間違い):references/gotchas.md

関連項目

  • UX Map は、LiveComponent 内でリアクティブなマップを使用するための ComponentWithMapTrait を提供します。LiveProps が変更されると、マップは自動的に更新されます。
  • UX Icons は、特別な設定なしで LiveComponent のテンプレート内で動作します。アイコンは、ほかの Twig マークアップと同様に、サーバーとの往復ごとに再レンダリングされます。

コメント