mcpField
L
live-component

live-component

github.com/smnandre开发18090 次安装19 周前更新
代码仓库

关于 live-component

面向 Symfony UX LiveComponent 开发者的参考技能,用于构建 PHP 和 Twig 响应式界面。内容涵盖基于 AJAX 的服务端重新渲染、状态绑定、操作、表单和组件通信,无需为这些交互编写 JavaScript。

主要功能

  1. 01通过 AJAX 重新渲染组件
  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 动态重新渲染的 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 控制器,任何功能都无法正常工作。

参考资料

另请参阅

  • UX Map 提供 ComponentWithMapTrait,用于在 LiveComponents 中创建响应式地图。当 LiveProps 发生变化时,地图会自动更新。
  • UX Icons 可在 LiveComponent 模板中使用,无需特殊设置——图标会像其他 Twig 标记一样,在每次服务器往返时重新渲染。

评论

支持 Markdown