关于 live-component
面向 Symfony UX LiveComponent 开发者的参考技能,用于构建 PHP 和 Twig 响应式界面。内容涵盖基于 AJAX 的服务端重新渲染、状态绑定、操作、表单和组件通信,无需为这些交互编写 JavaScript。
主要功能
- 01通过 AJAX 重新渲染组件
- 02持久化及可写组件状态
- 03输入双向绑定
- 04服务端操作与表单集成
- 05组件事件通信
适用场景
- 01随输入更新搜索结果
- 02实时验证表单字段
- 03根据输入筛选列表
- 04构建由服务端逻辑驱动的交互组件
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ smnandre/ symfony-ux-skills --skill live-component安装后在 Claude Code 中输入/live-component确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/api.mdgotchas.mdpatterns.md详细介绍
LiveComponent
通过 AJAX 动态重新渲染的 TwigComponents。使用 PHP + Twig 构建响应式 UI,无需 JavaScript。每次用户交互都会触发一次服务器往返,重新渲染组件并对 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(双向绑定)
只有可写 props 才能通过 data-model 从前端修改:
#[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 调用 Actions
{# 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 访问。在单次渲染周期内使用 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 }} 不可省略。 没有它,就不会附加 live behavior Stimulus 控制器,任何功能都无法正常工作。
参考资料
- 完整 API(props、actions、表单、事件、所有选项):references/api.md
- 模式(搜索、CRUD、模态框、验证、实际示例):references/patterns.md
- 注意事项(props、水合、性能、常见错误):references/gotchas.md
另请参阅
- UX Map 提供
ComponentWithMapTrait,用于在 LiveComponents 中创建响应式地图。当 LiveProps 发生变化时,地图会自动更新。 - UX Icons 可在 LiveComponent 模板中使用,无需特殊设置——图标会像其他 Twig 标记一样,在每次服务器往返时重新渲染。