跳到内容

Live Components

编辑此页

Live components 构建于 TwigComponent 库之上,让您能够随着用户与 Twig 组件的交互,在前端自动更新它们。灵感来自 LivewirePhoenix LiveView

如果您还不熟悉 Twig 组件,值得花几分钟时间在 TwigComponent 文档中熟悉一下。

一个实时的产品搜索组件可能看起来像这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 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 $productRepository)
    {
    }

    public function getProducts(): array
    {
        // example method that returns an array of Products
        return $this->productRepository->search($this->query);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{# templates/components/ProductSearch.html.twig #}
{# for the Live Component to work, there must be a single root element
   (e.g. a <div>) where the attributes are applied to #}
<div {{ attributes }}>
    <input
        type="search"
        data-model="query"
    >

    <ul>
        {% for product in this.products %}
            <li>{{ product.name }}</li>
        {% endfor %}
    </ul>
</div>

完成!现在在任何你想要的地方渲染它

1
{{ component('ProductSearch') }}

当用户在框中键入时,组件将自动重新渲染并显示新的结果!

想要一些演示?查看 http://ux.symfony.ac.cn/live-component#demo

安装

使用 Composer 和 Symfony Flex 安装 bundle

1
$ composer require symfony/ux-live-component

如果您正在使用 WebpackEncore,请安装您的 assets 并重启 Encore(如果您正在使用 AssetMapper,则不需要)

1
2
$ npm install --force
$ npm run watch

如果你的项目本地化为不同的语言(通过 locale 路由参数 或通过 在请求中设置 locale),请将 {_locale} 属性添加到 UX Live Components 路由定义中,以在重新渲染之间保持 locale

1
2
3
4
5
# config/routes/ux_live_component.yaml
  live_component:
      resource: '@LiveComponentBundle/config/routes.php'
-     prefix: /_components
+     prefix: /{_locale}/_components

就是这样!我们准备好了!

让你的组件 “实时”

如果您还没有这样做,请查看 Twig Component 文档,以了解 Twig 组件的基础知识。

假设你已经构建了一个基本的 Twig 组件

1
2
3
4
5
6
7
8
9
10
11
12
13
// src/Twig/Components/RandomNumber.php
namespace App\Twig\Components;

use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;

#[AsTwigComponent]
class RandomNumber
{
    public function getRandomNumber(): int
    {
        return rand(0, 1000);
    }
}
1
2
3
4
{# templates/components/RandomNumber.html.twig #}
<div>
    <strong>{{ this.randomNumber }}</strong>
</div>

要将其转换为 “实时” 组件(即可以在前端实时重新渲染的组件),请将组件的 AsTwigComponent 属性替换为 AsLiveComponent 并添加 DefaultActionTrait

1
2
3
4
5
6
7
8
9
10
11
// src/Twig/Components/RandomNumber.php
- use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
+ use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
+ use Symfony\UX\LiveComponent\DefaultActionTrait;

- #[AsTwigComponent]
+ #[AsLiveComponent]
  class RandomNumber
  {
+     use DefaultActionTrait;
  }

然后,在模板中,确保你的整个组件周围有一个 HTML 元素,并使用 attributes 变量 初始化 Stimulus 控制器

1
2
3
4
- <div>
+ <div {{ attributes }}>
      <strong>{{ this.randomNumber }}</strong>
  </div>

您的组件现在是一个实时组件…… 只是我们还没有添加任何会导致组件更新的东西。让我们从简单开始,添加一个按钮,当点击时,它将重新渲染组件并给用户一个新的随机数

1
2
3
4
5
6
7
<div {{ attributes }}>
    <strong>{{ this.randomNumber }}</strong>

    <button
        data-action="live#$render"
    >Generate a new number!</button>
</div>

就是这样!当您单击按钮时,将进行 Ajax 调用以获取组件的新副本。该 HTML 将替换当前的 HTML。换句话说,您刚刚生成了一个新的随机数!这很酷,但让我们继续前进,因为…… 事情会变得更酷。

提示

如果您使用 Symfony MakerBundle,您可以使用 make:twig-component 命令轻松创建一个新组件

1
$ php bin/console make:twig-component --live EditPost

提示

需要在你的组件上做一些额外的数据初始化?创建一个 mount() 方法或使用 PostMount 钩子:Twig Component mount 文档

LiveProps:有状态的组件属性

让我们通过添加一个 $max 属性来使我们的组件更灵活

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// src/Twig/Components/RandomNumber.php
namespace App\Twig\Components;

// ...
use Symfony\UX\LiveComponent\Attribute\LiveProp;

#[AsLiveComponent]
class RandomNumber
{
    #[LiveProp]
    public int $max = 1000;

    public function getRandomNumber(): int
    {
        return rand(0, $this->max);
    }

    // ...
}

通过此更改,我们可以在渲染组件时控制 $max 属性

1
{{ component('RandomNumber', { max: 500 }) }}

但是 LiveProp 属性是怎么回事?具有 LiveProp 属性的属性成为此组件的 “有状态” 属性。换句话说,每次我们单击 “生成一个新数字!” 按钮时,当组件重新渲染时,它将记住 $max 属性的原始值,并生成一个 0 到 500 之间的随机数。如果您忘记添加 LiveProp,当组件重新渲染时,这两个值将不会在对象上设置。

简而言之:LiveProps 是 “有状态的属性”:它们将在渲染时始终被设置。大多数属性将是 LiveProps,常见的例外是保存服务的属性(这些不需要是有状态的,因为每次渲染组件之前都会自动装配它们)。

LiveProp 数据类型

LiveProps 必须是可以发送到 JavaScript 的值。支持的值是标量(int、float、string、bool、null)、数组(标量值数组)、枚举、DateTime 对象、Doctrine 实体对象、DTO 或 DTO 数组。

有关处理更复杂的数据,请参阅 水合

数据绑定

像 React 或 Vue 这样的前端框架的最佳部分之一是 “数据绑定”。如果您不熟悉,这就是您将某些 HTML 元素(例如 <input>)的值 “绑定” 到组件对象上的属性的位置。

例如,我们是否可以允许用户更改 $max 属性,然后在他们这样做时重新渲染组件?当然可以!而才是 live components 真正闪光的地方。

向模板添加一个输入框

1
2
3
4
5
6
7
{# templates/components/RandomNumber.html.twig #}
<div {{ attributes }}>
    <input type="number" data-model="max">

    Generating a number between 0 and {{ max }}
    <strong>{{ this.randomNumber }}</strong>
</div>

2.5

在 2.5 版本之前,你还需要在 <input> 上设置 value="{{ max }}"。现在,对于所有 “data-model” 字段,这都会自动设置。

关键是 data-model 属性。由于它,当用户键入时,组件上的 $max 属性将自动更新!

2.3

在 2.3 版本之前,您还需要一个 data-action="live#update" 属性。现在应该删除该属性。

如何做到的?Live components 监听 input 事件并发送 Ajax 请求以使用新数据重新渲染组件!

嗯,实际上,我们还缺少一步。默认情况下,LiveProp 是 “只读” 的。出于安全目的,除非您使用 writable=true 选项允许,否则用户无法更改 LiveProp 的值并重新渲染组件

1
2
3
4
5
6
7
8
9
10
11
12
13
// src/Twig/Components/RandomNumber.php
  // ...

  class RandomNumber
  {
      // ...

-     #[LiveProp]
+     #[LiveProp(writable: true)]
      public int $max = 1000;

      // ...
  }

现在它可以工作了:当你在 max 框中键入时,组件将重新渲染,并在该范围内生成一个新的随机数。

防抖

如果用户快速键入 5 个字符,我们不希望发送 5 个 Ajax 请求。幸运的是,live components 添加了自动防抖:它在键入之间等待 150 毫秒的暂停,然后才发送 Ajax 请求进行重新渲染。这是内置的,因此您无需考虑它。但是,您可以通过 debounce 修饰符延迟

1
<input data-model="debounce(100)|max">

字段 “change” 时的延迟更新

有时,您可能希望一个字段仅在用户更改输入并且移动到另一个字段后才重新渲染。在这种情况下,浏览器会分派一个 change 事件。要在此事件发生时重新渲染,请使用 on(change) 修饰符

1
<input data-model="on(change)|max">

延迟重新渲染直到稍后

在其他时候,您可能想要更新属性的内部值,但等待稍后重新渲染组件(例如,直到单击按钮)。为此,请使用 norender 修饰符

1
<input data-model="norender|max">

对于使用 ComponentWithFormTrait 的表单,请覆盖 getDataModelValue() 方法

1
2
3
4
private function getDataModelValue(): ?string
{
    return 'norender|*';
}

提示

你也可以在 Twig 中定义这个值

1
{{ form_start(form, {attr: {'data-model': 'norender|*'}}) }}

现在,当您键入时,max “模型” 将在 JavaScript 中更新,但它尚不会进行 Ajax 调用来重新渲染组件。无论下一次重新渲染确实发生何时,都将使用更新后的 max 值。

这可以与触发点击时渲染的按钮一起使用

1
2
<input data-model="norender|coupon">
<button data-action="live#$render">Apply</button>

显式强制重新渲染

在某些情况下,您可能想要显式强制组件重新渲染。例如,考虑一个结账组件,该组件提供一个优惠券输入,该输入必须仅在单击关联的 “应用优惠券” 按钮时使用

1
2
<input data-model="norender|coupon">
<button data-action="live#$render">Apply coupon</button>

输入框上的 norender 选项确保在此输入更改时组件不会重新渲染。live#$render 动作是一个特殊的内置动作,它触发重新渲染。

使用 name="" 而不是 data-model

如果您正在构建表单(稍后会详细介绍表单),则可以依赖 name 属性,而不是向每个字段添加 data-model

2.3

自 2.3 版本起,form 上的 data-model 属性是必需的。

要激活此功能,您必须向 <form> 元素添加 data-model 属性

1
2
3
4
5
6
7
8
9
10
<div {{ attributes }}>
    <form data-model="*">
        <input
            name="max"
            value="{{ max }}"
        >

        // ...
    </form>
</div>

data-model* 值不是必需的,但通常使用。您也可以使用普通的修饰符,例如 data-model="on(change)|*",例如,仅发送每个字段内部的 change 事件的模型更新。

当外部 JavaScript 更改字段时,模型更新不起作用

假设您使用一个 JavaScript 库,该库您设置字段的值:例如一个 “日期选择器” 库,它隐藏了原生的 <input data-model="publishAt"> 字段,并在用户选择日期时在幕后设置它。

在这种情况下,模型(例如 publishAt)可能无法正确更新,因为 JavaScript 不会触发正常的 change 事件。要解决此问题,您需要 “hook” 到 JavaScript 库并直接设置模型(或在 data-model 字段上触发 change 事件)。请参阅 手动触发元素更改

实体的 LiveProp & 更复杂的数据

LiveProp 数据必须是简单的标量值,只有少数例外,例如 DateTime 对象、枚举 & Doctrine 实体对象。当 LiveProp 发送到前端时,它们会被 “脱水”。当从前端发送 Ajax 请求时,脱水的数据随后被 “水合” 回到原始状态。Doctrine 实体对象是 LiveProp 的一个特殊情况

1
2
3
4
5
6
7
8
use App\Entity\Post;

#[AsLiveComponent]
class EditPost
{
    #[LiveProp]
    public Post $post;
}

如果 Post 对象是持久化的,则它会被脱水为实体的 id,然后通过查询数据库将其水合回来。如果对象未持久化,则它会被脱水为一个空数组,然后通过创建一个对象(即 new Post())将其水合回来。

Doctrine 实体的数组和其他 “简单” 值(如 DateTime)也受支持,只要 LiveProp 具有 LiveComponents 可以读取的正确 PHPDoc

1
2
/** @var Product[] */
public $products = [];

从 docblock 中提取集合类型需要 phpdocumentor/reflection-docblock 库。请确保它已安装在您的应用程序中

1
$ composer require phpdocumentor/reflection-docblock

可写对象属性或数组键

默认情况下,用户无法更改实体 LiveProp属性。您可以通过将 writable 设置为可写的属性名称来允许这样做。这也可用作仅使数组的某些键可写的一种方式

1
2
3
4
5
6
7
8
9
10
11
use App\Entity\Post;

#[AsLiveComponent]
class EditPost
{
    #[LiveProp(writable: ['title', 'content'])]
    public Post $post;

    #[LiveProp(writable: ['allow_markdown'])]
    public array $options = ['allow_markdown' => true, 'allow_html' => false];
}

现在 post.titlepost.contentoptions.allow_markdown 可以像正常的模型名称一样使用

1
2
3
4
5
6
7
8
9
10
11
12
13
<div {{ attributes }}>
    <input data-model="post.title">
    <textarea data-model="post.content"></textarea>

    Allow Markdown?
    <input type="checkbox" data-model="options.allow_markdown">

    Preview:
    <div>
        <h3>{{ post.title }}</h3>
        {{ post.content|markdown_to_html }}
    </div>
</div>

对象上的任何其他属性(或数组上的键)都将是只读的。

对于数组,您可以设置 writable: true 以允许更改、添加或删除数组中的任何

1
2
3
4
5
6
7
8
9
10
11
#[AsLiveComponent]
class EditPost
{
    // ...

    #[LiveProp(writable: true)]
    public array $options = ['allow_markdown' => true, 'allow_html' => false];

    #[LiveProp(writable: true)]
    public array $todoItems = ['Train tiger', 'Feed tiger', 'Pet tiger'];
}

注意

可写路径值使用与顶级属性相同的过程(即 Symfony 的序列化器)进行脱水/水合。

复选框、选择元素单选按钮 & 数组

2.8

使用复选框设置布尔值的功能在 LiveComponent 2.8 中添加。

复选框可用于设置布尔值或字符串数组

1
2
3
4
5
6
7
8
9
#[AsLiveComponent]
class EditPost
{
    #[LiveProp(writable: true)]
    public bool $agreeToTerms = false;

    #[LiveProp(writable: true)]
    public array $foods = ['pizza', 'tacos'];
}

在模板中,在复选框上设置 value 属性将在选中时设置该值。如果未设置 value,则复选框将设置一个布尔值

1
2
3
4
5
<input type="checkbox" data-model="agreeToTerms">

<input type="checkbox" data-model="foods[]" value="pizza">
<input type="checkbox" data-model="foods[]" value="tacos">
<input type="checkbox" data-model="foods[]" value="sushi">

selectradio 元素更简单一些:使用它们来设置单个值或值数组

1
2
3
4
5
6
7
8
9
10
11
#[AsLiveComponent]
class EditPost
{
    // ...

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

    #[LiveProp(writable: true)]
    public array $foods = ['pizza', 'tacos'];
}
1
2
3
4
5
6
7
8
9
<input type="radio" data-model="meal" value="breakfast">
<input type="radio" data-model="meal" value="lunch">
<input type="radio" data-model="meal" value="dinner">

<select data-model="foods" multiple>
    <option value="pizza">Pizza</option>
    <option value="tacos">Tacos</option>
    <option value="sushi">Sushi</option>
</select>

LiveProp 日期格式

2.8

format 选项在 Live Components 2.8 中引入。

如果您有一个可写的 LiveProp,它是某种 DateTime 实例,您可以使用 format 选项控制前端模型的格式

1
2
#[LiveProp(writable: true, format: 'Y-m-d')]
public ?\DateTime $publishOn = null;

现在您可以将其绑定到前端使用相同格式的字段

1
<input type="date" data-model="publishOn">

允许实体更改为另一个实体

如果,您想要允许用户切换到另一个实体,而不是更改实体的属性,该怎么办?例如

1
2
3
4
5
<select data-model="post">
    {% for post in posts %}
        <option value="{{ post.id }}">{{ post.title }}</option>
    {% endfor %}
</select>

要使 post 属性本身可写,请使用 writable: true

1
2
3
4
5
6
7
8
use App\Entity\Post;

#[AsLiveComponent]
class EditPost
{
    #[LiveProp(writable: true)]
    public Post $post;
}

注意

这将允许用户将 Post 更改为数据库中的任何实体。有关更多信息,请参阅:https://github.com/symfony/ux/issues/424

如果您希望用户能够更改 Post 某些属性,请使用特殊的 LiveProp::IDENTITY 常量

1
2
3
4
5
6
7
8
use App\Entity\Post;

#[AsLiveComponent]
class EditPost
{
    #[LiveProp(writable: [LiveProp::IDENTITY, 'title', 'content'])]
    public Post $post;
}

请注意,能够更改对象的“标识”仅适用于脱水为标量值的对象(例如持久化实体,它们脱水为 id)。

在 LiveProp 上使用 DTO

2.12

DTO 对象的自动(反)水化在 LiveComponents 2.12 中引入。

您还可以将 DTO(即数据传输对象/任何简单类)与 LiveProp 一起使用,只要该属性具有正确的类型

1
2
3
4
5
class ComponentWithAddressDto
{
    #[LiveProp]
    public AddressDto $addressDto;
}

要处理 DTO 集合,请在 PHPDoc 中指定集合类型

1
2
3
4
5
6
7
8
class ComponentWithAddressDto
{
    /**
     * @var AddressDto[]
     */
    #[LiveProp]
    public array $addressDtoCollection;
}

从 docblock 中提取集合类型需要 phpdocumentor/reflection-docblock 库。请确保它已安装在您的应用程序中

1
$ composer require phpdocumentor/reflection-docblock

以下是 DTO 对象的(反)水化工作原理

  • 所有“属性”(公共属性或通过 getter/setter 方法的伪属性)都会被读取和脱水。如果属性是可设置但不可获取(或反之亦然),则会抛出错误。
  • PropertyAccess 组件用于获取/设置值,这意味着除了公共属性外,还支持 getter 和 setter 方法。
  • DTO 不能有任何构造函数参数。

如果此解决方案不符合您的需求,还有其他两个选项可以使其工作

使用序列化器进行水合

2.8

useSerializerForHydration 选项在 LiveComponent 2.8 中添加。

要通过 Symfony 的序列化器进行水化/脱水,请使用 useSerializerForHydration 选项

1
2
3
4
5
class ComponentWithAddressDto
{
    #[LiveProp(useSerializerForHydration: true)]
    public AddressDto $addressDto;
}

您还可以在 LiveProp 上设置 serializationContext 选项。

使用方法进行水合:hydrateWith & dehydrateWith

您可以通过在 LiveProp 上设置 hydrateWithdehydrateWith 选项来完全控制水化过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class ComponentWithAddressDto
{
    #[LiveProp(hydrateWith: 'hydrateAddress', dehydrateWith: 'dehydrateAddress')]
    public AddressDto $addressDto;

    public function dehydrateAddress(AddressDto $address)
    {
        return [
            'street' => $address->street,
            'city' => $address->city,
            'state' => $address->state,
        ];
    }

    public function hydrateAddress($data): AddressDto
    {
        return new AddressDto($data['street'], $data['city'], $data['state']);
    }
}

水合扩展

2.8

HydrationExtensionInterface 系统在 LiveComponents 2.8 中添加。

如果您经常水化/脱水相同类型的对象,您可以创建一个自定义水化扩展来简化此操作。例如,如果您经常水化自定义 Food 对象,则水化扩展可能如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
use App\Model\Food;
use Symfony\UX\LiveComponent\Hydration\HydrationExtensionInterface;

class FoodHydrationExtension implements HydrationExtensionInterface
{
    public function supports(string $className): bool
    {
        return is_subclass_of($className, Food::class);
    }

    public function hydrate(mixed $value, string $className): ?object
    {
        return new Food($value['name'], $value['isCooked']);
    }

    public function dehydrate(object $object): mixed
    {
        return [
            'name' => $object->getName(),
            'isCooked' => $object->isCooked(),
        ];
    }
}

如果您使用自动配置,那就完成了!否则,请使用 live_component.hydration_extension 标记服务。

提示

在内部,Doctrine 实体对象使用 DoctrineEntityHydrationExtension 来控制实体对象的自定义(反)水化。

手动更新模型

您还可以更直接地更改模型的值,而无需使用表单字段

1
2
3
4
5
6
<button
    type="button"
    data-model="mode"
    data-value="edit"
    data-action="live#update"
>Edit</button>

在此示例中,单击按钮会将组件上的 mode live 属性更改为值 editdata-action="live#update" 是触发更新的 Stimulus 代码。

在 JavaScript 中使用组件

想要从您自己的自定义 JavaScript 中更改模型的值甚至触发操作吗?没问题,这要归功于 JavaScript Component 对象,它附加到每个根组件元素。

例如,要编写您的自定义 JavaScript,您需要创建一个 Stimulus 控制器并将其放置在(或附加到)您的根组件元素周围

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// assets/controllers/some-custom-controller.js
// ...
import { getComponent } from '@symfony/ux-live-component';

export default class extends Controller {
    async initialize() {
        this.component = await getComponent(this.element);
    }

    // some Stimulus action triggered, for example, on user click
    toggleMode() {
        // e.g. set some live property called "mode" on your component
        this.component.set('mode', 'editing');
        // then, trigger a re-render to get the fresh HTML
        this.component.render();

        // or call an action
        this.component.action('save', { arg1: 'value1' });
    }
}

您还可以通过根组件元素上的特殊属性访问 Component 对象,尽管 getComponent() 是推荐的方法,因为它即使在组件尚未初始化时也能工作

1
2
const component = document.getElementById('id-of-your-element').__component;
component.mode = 'editing';

最后,您也可以直接设置模型字段的值。但是,请务必同时触发 change 事件,以便 live components 收到更改通知

1
2
3
4
const input = document.getElementById('favorite-food');
input.value = 'sushi';

input.dispatchEvent(new Event('change', { bubbles: true }));

向组件根元素添加 Stimulus 控制器

2.9

defaults() 方法与 stimulus_controller() 一起使用的能力在 TwigComponents 2.9 中添加,并且需要 symfony/stimulus-bundle。以前,stimulus_controller() 被传递给 attributes.add()

要将自定义 Stimulus 控制器添加到您的根组件元素

1
<div {{ attributes.defaults(stimulus_controller('some-custom', { someValue: 'foo' })) }}>

JavaScript 组件钩子

JavaScript Component 对象有许多钩子,您可以使用这些钩子在组件的生命周期内运行代码。要从 Stimulus 挂接到组件系统

1
2
3
4
5
6
7
8
9
10
11
12
13
// assets/controllers/some-custom-controller.js
// ...
import { getComponent } from '@symfony/ux-live-component';

export default class extends Controller {
    async initialize() {
        this.component = await getComponent(this.element);

        this.component.on('render:finished', (component) => {
            // do something after the component re-renders
        });
    }
}

注意

render:startedrender:finished 事件仅在组件被重新渲染时(通过操作或模型更改)才会被分派。

以下钩子可用(以及传递的参数)

  • connect 参数 (component: Component)
  • disconnect 参数 (component: Component)
  • render:started 参数 (html: string, response: BackendResponse, controls: { shouldRender: boolean })
  • render:finished 参数 (component: Component)
  • response:error 参数 (backendResponse: BackendResponse, controls: { displayError: boolean })
  • loading.state:started 参数 (element: HTMLElement, request: BackendRequest)
  • loading.state:finished 参数 (element: HTMLElement)
  • model:set 参数 (model: string, value: any, component: Component)

加载状态

通常,您希望在组件重新渲染或 操作正在处理时显示(或隐藏)元素。例如

1
2
3
4
5
<!-- show only when the component is loading -->
<span data-loading>Loading</span>

<!-- equivalent, longer syntax -->
<span data-loading="show">Loading</span>

或者,在组件加载时隐藏元素

1
2
<!-- hide when the component is loading -->
<span data-loading="hide">Saved!</span>

添加和删除类或属性

您可以添加或删除类,而不是隐藏或显示整个元素

1
2
3
4
5
6
7
8
<!-- add this class when loading -->
<div data-loading="addClass(opacity-50)">...</div>

<!-- remove this class when loading -->
<div data-loading="removeClass(opacity-50)">...</div>

<!-- add multiple classes when loading -->
<div data-loading="addClass(opacity-50 text-muted)">...</div>

有时您可能希望在加载时添加或删除 HTML 属性。这可以使用 addAttributeremoveAttribute 来完成

1
2
<!-- add the "disabled" attribute when loading -->
<div data-loading="addAttribute(disabled)">...</div>

注意

addAttribute()removeAttribute() 函数仅适用于空的 HTML 属性(disabledreadonlyrequired 等),而不适用于定义其值的属性(例如,这不起作用:addAttribute(style='color: red'))。

您还可以通过用空格分隔任意数量的指令来组合它们

1
<div data-loading="addClass(opacity-50) addAttribute(disabled)">...</div>

最后,您可以添加 delay 修饰符,以便在加载时间超过一定量的时间后才触发加载更改

1
2
3
4
5
6
7
8
<!-- Add class after 200ms of loading -->
<div data-loading="delay|addClass(opacity-50)">...</div>

<!-- Show after 200ms of loading -->
<div data-loading="delay|show">Loading</div>

<!-- Show after 500ms of loading -->
<div data-loading="delay(500)|show">Loading</div>

针对特定动作的加载

2.5

action() 修饰符在 Live Components 2.5 中引入。

要仅在触发特定操作时切换加载行为,请将 action() 修饰符与操作名称一起使用 - 例如 saveForm()

1
2
3
4
<!-- show only when the "saveForm" action is triggering -->
<span data-loading="action(saveForm)|show">Loading</span>
<!-- multiple modifiers -->
<div data-loading="action(saveForm)|delay|addClass(opacity-50)">...</div>

当特定模型更改时针对加载

2.5

model() 修饰符在 Live Components 2.5 中引入。

您也可以仅当刚刚更改了特定模型值时才切换加载行为,使用 model() 修饰符

1
2
3
4
5
6
7
8
<input data-model="email" type="email">

<span data-loading="model(email)|show">
    Checking if email is available...
</span>

<!-- multiple modifiers & child properties -->
<span data-loading="model(user.email)|delay|addClass(opacity-50)">...</span>

动作

Live components 需要一个用于重新渲染它的单个“默认操作”。默认情况下,这是一个空的 __invoke() 方法,可以使用 DefaultActionTrait 添加。Live components 实际上是 Symfony 控制器,因此您可以将正常的控制器属性/注释(即 #[Cache]/#[Security])添加到整个类或仅单个操作。

您还可以在组件上触发自定义操作。假设我们想为我们的“随机数”组件添加一个“重置最大值”按钮,当单击该按钮时,会将最小/最大数字设置回默认值。

首先,添加一个方法,并在其上方添加一个执行工作的 LiveAction 属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// src/Twig/Components/RandomNumber.php
namespace App\Twig\Components;

// ...
use Symfony\UX\LiveComponent\Attribute\LiveAction;

class RandomNumber
{
    // ...

    #[LiveAction]
    public function resetMax()
    {
        $this->max = 1000;
    }

    // ...
}

2.16

指定操作的 data-live-action-param 属性方式在 Live Components 2.16 中添加。以前,这是通过 data-action-name 完成的。

要调用此方法,请触发 live Stimulus 控制器上的 action 方法,并将 resetMax 作为名为 actionStimulus 操作参数 传递

1
2
3
4
<button
    data-action="live#action"
    data-live-action-param="resetMax"
>Reset Min/Max</button>

完成!当用户单击此按钮时,将发送一个 POST 请求,该请求将触发 resetMax() 方法!在调用该方法后,组件将像往常一样重新渲染,使用新的 $max 属性值!

您还可以为操作添加多个“修饰符”

1
2
3
4
5
6
<form>
    <button
        data-action="live#action"
        data-live-action-param="debounce(300)|save"
    >Save</button>
</form>

debounce(300) 在操作执行之前添加 300 毫秒的“防抖动”。换句话说,如果您真的快速点击 5 次,则只会发出一个 Ajax 请求!

您还可以使用 live_action twig 助手函数来渲染属性

1
2
3
4
5
<button {{ live_action('resetMax') }}>Reset Min/Max</button>

{# with modifiers #}

<button {{ live_action('save', {}, {'debounce': 300}) }}>Save</button>

动作 & 服务

组件操作的一个非常巧妙之处在于它们是真正的 Symfony 控制器。在内部,它们的处理方式与您使用路由创建的普通控制器方法完全相同。

这意味着,例如,您可以使用操作自动装配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// src/Twig/Components/RandomNumber.php
namespace App\Twig\Components;

// ...
use Psr\Log\LoggerInterface;

class RandomNumber
{
    // ...

    #[LiveAction]
    public function resetMax(LoggerInterface $logger)
    {
        $this->max = 1000;
        $logger->debug('The min/max were reset!');
    }

    // ...
}

动作 & 参数

2.16

指定操作参数的 data-live-{NAME}-param 属性方式在 Live Components 2.16 中添加。以前,这是在 data-action-name 属性内部完成的。

您还可以通过将每个参数添加为 Stimulus 操作参数 来将参数传递给您的操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<form>
    <button
        data-action="live#action"
        data-live-action-param="addItem"

        data-live-id-param="{{ item.id }}"
        data-live-item-name-param="CustomItem"
    >Add Item</button>
</form>

{# or #}

<form>
    <button {{ live_action('addItem', {'id': item.id, 'itemName': 'CustomItem' }) }}>Add Item</button>
</form>

在您的组件中,要允许传递每个参数,请添加 #[LiveArg] 属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// src/Twig/Components/ItemList.php
namespace App\Twig\Components;

// ...
use Psr\Log\LoggerInterface;
use Symfony\UX\LiveComponent\Attribute\LiveArg;

class ItemList
{
    // ...
    #[LiveAction]
    public function addItem(#[LiveArg] int $id, #[LiveArg('itemName')] string $name)
    {
        $this->id = $id;
        $this->name = $name;
    }
}

动作和 CSRF 保护

当操作被触发时,将发送一个带有自定义 Accept 标头的 POST 请求。此标头会自动设置并为您验证。换句话说,由于浏览器强制执行的 same-originCORS 策略,您可以轻松地从 CSRF 保护中受益。

为确保这种内置的 CSRF 保护仍然有效,请注意您的 CORS 标头(例如,不要使用 Access-Control-Allow-Origin: *)。

在测试模式下,CSRF 保护被禁用,以使测试更容易。

动作、重定向和 AbstractController

有时,您可能希望在操作执行后重定向(例如,您的操作保存了一个表单,然后您想要重定向到另一个页面)。您可以通过从您的操作返回 RedirectResponse 来做到这一点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// src/Twig/Components/RandomNumber.php
namespace App\Twig\Components;

// ...
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;

class RandomNumber extends AbstractController
{
    // ...

    #[LiveAction]
    public function resetMax()
    {
        // ...

        $this->addFlash('success', 'Max has been reset!');

        return $this->redirectToRoute('app_random_number');
    }

    // ...
}

您可能注意到一个有趣的技巧:为了使重定向更容易,该组件现在扩展了 AbstractController!这是完全允许的,并且使您可以访问所有正常的控制器快捷方式。我们甚至添加了一条 flash 消息!

上传文件

2.11

将文件上传到操作的功能在 2.11 版本中添加。

默认情况下,文件不会发送到组件。您需要使用 live action 来处理文件,并告知组件何时应发送文件

1
2
3
4
5
<input type="file" name="my_file" />
<button
    data-action="live#action"
    data-live-action-param="files|my_action"
/>

要使用操作发送文件(或多个文件),请使用 files 修饰符。不带参数,它会将所有待处理的文件发送到您的操作。您还可以指定修饰符参数来选择应上传的文件。

1
2
3
4
5
6
7
8
9
10
11
<p>
    <input type="file" name="my_file" />
    <input type="file" name="multiple[]" multiple />

    {# Send only file from first input #}
    <button data-action="live#action" data-live-action-param="files(my_file)|myAction" />
    {# You can chain modifiers to send multiple files #}
    <button data-action="live#action" data-live-action-param="files(my_file)|files(multiple[])|myAction" />
    {# Or send all pending files #}
    <button data-action="live#action" data-live-action-param="files|myAction" />
</p>

这些文件将在常规 $request->files 文件包中可用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// src/Twig/Components/FileUpload.php
namespace App\Twig\Components;

use Symfony\Component\HttpFoundation\Request;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveAction;
use Symfony\UX\LiveComponent\DefaultActionTrait;

#[AsLiveComponent]
class FileUpload
{
    use DefaultActionTrait;

    #[LiveAction]
    public function myAction(Request $request)
    {
        $file = $request->files->get('my_file');
        $multiple = $request->files->all('multiple');

        // Handle files
    }
}

提示

请记住,为了从单个输入发送多个文件,您需要在 HTML 元素上指定 multiple 属性,并在 name 末尾加上 []

下载文件

目前,Live Components 不原生支持直接从 LiveAction 返回文件响应。但是,您可以通过重定向到处理文件响应的路由来实现文件下载。

创建一个 LiveAction,生成文件下载的 URL 并返回 RedirectResponse

1
2
3
4
5
6
#[LiveAction]
public function initiateDownload(UrlGeneratorInterface $urlGenerator): RedirectResponse
{
    $url = $urlGenerator->generate('app_file_download');
    return new RedirectResponse($url);
}
1
2
3
4
5
6
7
8
<div {{ attributes }} data-turbo="false">
    <button
        data-action="live#action"
        data-live-action-param="initiateDownload"
    >
        Download
    </button>
</div>

提示

当 Turbo 启用时,如果 LiveAction 响应重定向到另一个 URL,Turbo 将发出请求来预取内容。在此处,添加 data-turbo="false" 可确保仅调用一次下载 URL。

表单

组件还可以帮助渲染 Symfony 表单,可以是整个表单(对于您键入时的自动验证很有用),也可以只渲染一个或一些字段(例如,textarea 的 markdown 预览或 依赖表单字段

在组件中渲染整个表单

假设您有一个绑定到 Post 实体的 PostType 表单类,并且您想在组件中渲染它,以便您可以在用户键入时获得即时验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
namespace App\Form;

use App\Entity\Post;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class PostType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('title')
            ->add('slug')
            ->add('content')
        ;
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => Post::class,
        ]);
    }
}

太棒了!在某个页面(例如“编辑帖子”页面)的模板中,渲染我们将在接下来创建的 PostForm 组件

1
2
3
4
5
6
7
8
9
10
{# templates/post/edit.html.twig #}
{% extends 'base.html.twig' %}

{% block body %}
    <h1>Edit Post</h1>

    {{ component('PostForm', {
        initialFormData: post,
    }) }}
{% endblock %}

好的:是时候构建 PostForm 组件了!Live Components 包附带一个特殊的 trait - ComponentWithFormTrait - 使处理表单变得容易

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
namespace App\Twig\Components;

use App\Entity\Post;
use App\Form\PostType;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\FormInterface;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveProp;
use Symfony\UX\LiveComponent\ComponentWithFormTrait;
use Symfony\UX\LiveComponent\DefaultActionTrait;

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

    /**
     * The initial data used to create the form.
     */
    #[LiveProp]
    public ?Post $initialFormData = null;

    protected function instantiateForm(): FormInterface
    {
        // we can extend AbstractController to get the normal shortcuts
        return $this->createForm(PostType::class, $this->initialFormData);
    }
}

该 trait 强制您创建一个 instantiateForm() 方法,该方法在每次通过 AJAX 渲染组件时使用。为了重新创建与原始表单相同的表单,我们传入 initialFormData 属性并将其设置为 LiveProp

此组件的模板将渲染表单,由于该 trait,该表单作为 form 可用

1
2
3
4
5
6
7
8
9
10
{# templates/components/PostForm.html.twig #}
<div {{ attributes }}>
    {{ form_start(form) }}
        {{ form_row(form.title) }}
        {{ form_row(form.slug) }}
        {{ form_row(form.content) }}

        <button>Save</button>
    {{ form_end(form) }}
</div>

就是这样!结果令人难以置信!当您完成更改每个字段时,组件会自动重新渲染 - 包括显示该字段的任何验证错误!太棒了!

这是如何工作的

  1. ComponentWithFormTrait 具有一个可写的 $formValues LiveProp,其中包含表单中每个字段的值。
  2. 当用户更改字段时,$formValues 中的该键会更新,并发送一个 Ajax 请求以重新渲染。
  3. 在 Ajax 调用期间,表单使用 $formValues 提交,表单重新渲染,并且页面已更新。

构建 “新文章” 表单组件

之前的组件已经可以用于编辑现有帖子或创建新帖子。对于新帖子,将新的 Post 对象传递给 initialFormData,或者完全省略它以使 initialFormData 属性默认为 null

1
2
3
4
{# templates/post/new.html.twig #}
{# ... #}

{{ component('PostForm') }}

通过 LiveAction 提交表单

处理表单提交的最简单方法是直接在组件中通过 LiveAction

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// ...
use Doctrine\ORM\EntityManagerInterface;
use Symfony\UX\LiveComponent\Attribute\LiveAction;

class PostForm extends AbstractController
{
    // ...

    #[LiveAction]
    public function save(EntityManagerInterface $entityManager)
    {
        // Submit the form! If validation fails, an exception is thrown
        // and the component is automatically re-rendered with the errors
        $this->submitForm();

        /** @var Post $post */
        $post = $this->getForm()->getData();
        $entityManager->persist($post);
        $entityManager->flush();

        $this->addFlash('success', 'Post saved!');

        return $this->redirectToRoute('app_post_show', [
            'id' => $post->getId(),
        ]);
    }
}

接下来,告诉 form 元素使用此操作

1
2
3
4
5
6
7
8
9
{# templates/components/PostForm.html.twig #}
{# ... #}

{{ form_start(form, {
    attr: {
        'data-action': 'live#action:prevent',
        'data-live-action-param': 'save'
    }
}) }}

现在,当表单提交时,它将通过 Ajax 执行 save() 方法。如果表单验证失败,它将使用错误重新渲染。如果成功,它将重定向。

使用普通的 Symfony 控制器提交

如果您愿意,您可以通过 Symfony 控制器提交表单。为此,像往常一样创建您的控制器,包括提交逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// src/Controller/PostController.php
class PostController extends AbstractController
{
    #[Route('/admin/post/{id}/edit', name: 'app_post_edit')]
    public function edit(Request $request, Post $post, EntityManagerInterface $entityManager): Response
    {
        $form = $this->createForm(PostType::class, $post);
        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            // save, redirect, etc
        }

        return $this->render('post/edit.html.twig', [
            'post' => $post,
            'form' => $form, // use $form->createView() in Symfony <6.2
        ]);
    }
}

如果验证失败,您希望 live component 使用表单错误而不是创建新表单进行渲染。为此,请将 form 变量传递到组件中

1
2
3
4
5
{# templates/post/edit.html.twig #}
{{ component('PostForm', {
    initialFormData: post,
    form: form
}) }}

在 LiveAction 中使用表单数据

每次对重新渲染 live component 进行 Ajax 调用时,都会使用最新的数据自动提交表单。

但是,有两件重要的事情要知道

  1. 当执行 LiveAction 时,表单尚未提交。
  2. 在表单提交之前,initialFormData 属性不会更新。

如果您需要在 LiveAction 中访问最新数据,您可以手动提交表单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ...

#[LiveAction]
public function save()
{
    // $this->initialFormData will *not* contain the latest data yet!

    // submit the form
    $this->submitForm();

    // now you can access the latest data
    $post = $this->getForm()->getData();
    // (same as above)
    $post = $this->initialFormData;
}

提示

如果您不调用 $this->submitForm(),则会在重新渲染组件之前自动调用它。

在 LiveAction 中动态更新表单

当对重新渲染 live component 进行 Ajax 调用时(无论是由于模型更改还是 LiveAction),都会使用来自 ComponentWithFormTrait$formValues 属性提交表单,该属性包含来自表单的最新数据。

有时,您需要从 LiveAction 动态更新表单上的某些内容。例如,假设您有一个“生成标题”按钮,单击该按钮将根据帖子的内容生成标题。

为此,您必须在提交表单之前直接更新 $this->formValues 属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ...

#[LiveAction]
public function generateTitle()
{
    // this works!
    // (the form will be submitted automatically after this method, now with the new title)
    $this->formValues['title'] = '... some auto-generated-title';

    // this would *not* work
    // $this->submitForm();
    // $post = $this->getForm()->getData();
    // $post->setTitle('... some auto-generated-title');
}

这很棘手。$this->formValues 属性是前端原始表单数据的数组,并且仅包含标量值(例如字符串、整数、布尔值和数组)。通过更新此属性,表单将提交,就好像用户已将新的 title 输入到表单中一样。然后,表单将使用新数据重新渲染。

注意

如果您要更新的字段是代码中的对象 - 例如与 EntityType 字段对应的实体对象 - 则需要使用表单前端使用的值。对于实体,那是 id

1
$this->formValues['author'] = $author->getId();

为什么不直接更新 $post 对象?提交表单后,已经创建了“表单视图”(前端的数据、错误等)。更改 $post 对象没有效果。即使在提交表单之前修改 $this->initialFormData 也无效:实际提交的 title 将覆盖它。

表单渲染问题

在大多数情况下,在组件内部渲染表单的效果非常好。但是,在某些情况下,您的表单可能无法按您期望的方式运行。

A) 文本框删除尾随空格

如果您在 input 事件上重新渲染字段(这是字段上的默认事件,每次您在文本框中键入内容时都会触发),那么如果您键入“空格”并暂停片刻,空格将消失!

这是因为 Symfony 文本字段会自动“修剪空格”。当您的组件重新渲染时,空格将消失……当用户正在键入时!要解决此问题,请在 change 事件(在文本框失去焦点后触发)上重新渲染,或将字段的 trim 选项设置为 false

1
2
3
4
5
6
7
8
9
public function buildForm(FormBuilderInterface $builder, array $options)
{
    $builder
        // ...
        ->add('content', TextareaType::class, [
            'trim' => false,
        ])
    ;
}

B) PasswordType 在重新渲染时丢失密码

如果您使用 PasswordType,当组件重新渲染时,输入将变为空白!这是因为,默认情况下,PasswordType 在提交后不会重新填充 <input type="password">

要解决此问题,请在您的表单中将 always_empty 选项设置为 false

1
2
3
4
5
6
7
8
9
public function buildForm(FormBuilderInterface $builder, array $options)
{
    $builder
        // ...
        ->add('plainPassword', PasswordType::class, [
            'always_empty' => false,
        ])
    ;
}

重置表单

2.10

resetForm() 方法在 LiveComponent 2.10 中添加。

通过操作提交表单后,您可能希望将表单“重置”回其初始状态,以便您可以再次使用它。通过在您的操作中调用 resetForm() 而不是重定向来执行此操作

1
2
3
4
5
6
7
#[LiveAction]
public function save(EntityManagerInterface $entityManager)
{
    // ...

    $this->resetForm();
}

使用动作更改你的表单:CollectionType

Symfony 的 CollectionType 可用于嵌入嵌入式表单的集合,包括允许用户动态添加或删除它们。Live components 使这一切成为可能,同时编写零 JavaScript。

例如,想象一下一个“博客帖子”表单,其中包含通过 CollectionType 嵌入的“评论”表单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
namespace App\Form;

use App\Entity\BlogPost;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class BlogPostFormType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('title', TextType::class)
            // ...
            ->add('comments', CollectionType::class, [
                'entry_type' => CommentFormType::class,
                'allow_add' => true,
                'allow_delete' => true,
                'by_reference' => false,
            ])
        ;
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(['data_class' => BlogPost::class]);
    }
}

现在,创建一个 Twig 组件来渲染表单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
namespace App\Twig;

use App\Entity\BlogPost;
use App\Entity\Comment;
use App\Form\BlogPostFormType;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\FormInterface;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveAction;
use Symfony\UX\LiveComponent\ComponentWithFormTrait;
use Symfony\UX\LiveComponent\DefaultActionTrait;

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

    #[LiveProp]
    public Post $initialFormData;

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

    #[LiveAction]
    public function addComment()
    {
        // "formValues" represents the current data in the form
        // this modifies the form to add an extra comment
        // the result: another embedded comment form!
        // change "comments" to the name of the field that uses CollectionType
        $this->formValues['comments'][] = [];
    }

    #[LiveAction]
    public function removeComment(#[LiveArg] int $index)
    {
        unset($this->formValues['comments'][$index]);
    }
}

此组件的模板有两个任务:(1) 像往常一样渲染表单,以及 (2) 包含触发 addComment()removeComment() 操作的链接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<div{{ attributes }}>
    {{ form_start(form) }}
        {{ form_row(form.title) }}

        <h3>Comments:</h3>
        {% for key, commentForm in form.comments %}
            <button
                data-action="live#action"
                data-live-action-param="removeComment"
                data-live-index-param="{{ key }}"
                type="button"
            >X</button>

            {{ form_widget(commentForm) }}
        {% endfor %}

        {# avoid an extra label for this field #}
        {% do form.comments.setRendered %}

        <button
            data-action="live#action"
            data-live-action-param="addComment"
            type="button"
        >+ Add Comment</button>

        <button type="submit" >Save</button>
    {{ form_end(form) }}
</div>

完成!在幕后,它的工作方式如下

A) 当用户单击“+ 添加评论”时,会发送一个 Ajax 请求,该请求会触发 addComment() 操作。

B) addComment() 修改 formValues,您可以将其视为表单的原始“POST”数据。

C) 仍然在 Ajax 请求期间,formValues 被“提交”到您的表单中。$this->formValues['comments'] 内部的新键告诉 CollectionType 您想要一个新的嵌入式表单。

D) 表单被渲染 - 现在带有另一个嵌入式表单!- 并且 Ajax 调用返回表单(带有新的嵌入式表单)。

当用户单击 removeComment() 时,会发生类似的过程。

注意

当使用 Doctrine 实体时,将 orphanRemoval: truecascade={"persist"} 添加到您的 OneToMany 关系中。在此示例中,这些选项将添加到 Post.comments 属性上方的 OneToMany 属性。这些选项有助于保存新项目并删除任何嵌入式表单被删除的项目。

使用 LiveCollectionType

2.2

LiveCollectionTypeLiveCollectionTrait 在 LiveComponent 2.2 中添加。

LiveCollectionType 使用上述相同的方法,但以通用方式,因此它需要的代码更少。此表单类型默认情况下为每一行添加一个“添加”和“删除”按钮,这要归功于 LiveCollectionTrait,它们开箱即用。

让我们以前面的示例为例,一个“博客帖子”表单,其中包含通过 LiveCollectionType 嵌入的“评论”表单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
namespace App\Form;

use App\Entity\BlogPost;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\UX\LiveComponent\Form\Type\LiveCollectionType;

class BlogPostFormType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('title', TextType::class)
            // ...
            ->add('comments', LiveCollectionType::class, [
                'entry_type' => CommentFormType::class,
            ])
        ;
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(['data_class' => BlogPost::class]);
    }
}

现在,创建一个 Twig 组件来渲染表单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
namespace App\Twig;

use App\Entity\BlogPost;
use App\Form\BlogPostFormType;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\FormInterface;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveProp;
use Symfony\UX\LiveComponent\DefaultActionTrait;
use Symfony\UX\LiveComponent\LiveCollectionTrait;

#[AsLiveComponent]
class BlogPostCollectionType extends AbstractController
{
    use LiveCollectionTrait;
    use DefaultActionTrait;

    #[LiveProp]
    public BlogPost $initialFormData;

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

不需要自定义模板,只需像往常一样渲染表单

1
2
3
<div {{ attributes }}>
    {{ form(form) }}
</div>

这会自动渲染连接到 live component 的添加和删除按钮。如果您想自定义按钮和集合行的渲染方式,可以使用 Symfony 的内置表单主题技术,但您应该注意,按钮不属于表单树。

注意

在底层,LiveCollectionType 以特殊方式向表单添加 button_addbutton_delete 字段。这些字段未作为常规表单字段添加,因此它们不属于表单树,而仅属于表单视图。button_add 被添加到集合视图变量,button_delete 被添加到每个项目视图变量。

以下是这些技术的一些示例。

如果您只想自定义某些属性,最简单的方法是使用表单类型中的选项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ...
$builder
    // ...
    ->add('comments', LiveCollectionType::class, [
        'entry_type' => CommentFormType::class,
        'label' => false,
        'button_delete_options' => [
            'label' => 'X',
            'attr' => [
                'class' => 'btn btn-outline-danger',
            ],
        ]
    ])
;

内联渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<div {{ attributes }}>
    {{ form_start(form) }}
        {{ form_row(form.title) }}

        <h3>Comments:</h3>
        {% for key, commentForm in form.comments %}
            {# render a delete button for every row #}
            {{ form_row(commentForm.vars.button_delete, { label: 'X', attr: { class: 'btn btn-outline-danger' } }) }}

            {# render rest of the comment form #}
            {{ form_row(commentForm, { label: false }) }}
        {% endfor %}

        {# render the add button #}
        {{ form_widget(form.comments.vars.button_add, { label: '+ Add comment', attr: { class: 'btn btn-outline-primary' } }) }}

        {# render rest of the form #}
        {{ form_row(form) }}

        <button type="submit" >Save</button>
    {{ form_end(form) }}
</div>

覆盖评论项目的特定块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{% form_theme form 'components/_form_theme_comment_list.html.twig' %}

<div {{ attributes }}>
    {{ form_start(form) }}
        {{ form_row(form.title)

        <h3>Comments:</h3>
        <ul>
            {{ form_row(form.comments, { skip_add_button: true }) }}
        </ul>

        {# render rest of the form #}
        {{ form_row(form) }}

        <button type="submit" >Save</button>
    {{ form_end(form) }}
</div>
1
2
3
4
5
6
7
{# templates/components/_form_theme_comment_list.html.twig #}
{%- block _blog_post_form_comments_entry_row -%}
    <li class="...">
        {{ form_row(form.content, { label: false }) }}
        {{ form_row(button_delete, { label: 'X', attr: { class: 'btn btn-outline-danger' } }) }}
    </li>
{% endblock %}

注意

您可以将表单主题放入组件模板并使用 {% form_theme form _self %}。但是,由于组件模板不扩展任何内容,因此它将无法按预期工作,您必须将 form_theme 指向单独的模板。请参阅 如何使用表单主题

覆盖通用按钮和集合条目

adddelete 按钮作为单独的 ButtonType 表单类型渲染,并且可以像通过 live_collection_button_addlive_collection_button_delete 块前缀的常规表单类型一样进行自定义

1
2
3
4
5
6
7
8
9
10
11
12
{% block live_collection_button_add_widget %}
    {% set attr = attr|merge({'class': attr.class|default('btn btn-ghost')}) %}
    {% set translation_domain = false %}
    {% set label_html = true %}
    {%- set label -%}
        <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
            <path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
        </svg>
        {{ 'form.collection.button.add.label'|trans({}, 'forms') }}
    {%- endset -%}
    {{ block('button_widget') }}
{% endblock live_collection_button_add_widget %}

要控制每一行的渲染方式,您可以覆盖与 LiveCollectionType 相关的块。这与 传统的 collection type 的工作方式相同,但您应该使用 live_collection_*live_collection_entry_* 作为前缀。

例如,默认情况下,添加按钮放置在项目之后(在我们的例子中是评论)。让我们将其移动到它们之前。

1
2
3
4
5
6
{%- block live_collection_widget -%}
    {%- if button_add is defined and not button_add.rendered -%}
        {{ form_row(button_add) }}
    {%- endif -%}
    {{ block('form_widget') }}
{%- endblock -%}

现在在每一行周围添加一个 div

1
2
3
4
5
6
7
8
{%- block live_collection_entry_row -%}
    <div>
        {{ block('form_row') }}
        {%- if button_delete is defined and not button_delete.rendered -%}
            {{ form_row(button_delete) }}
        {%- endif -%}
    </div>
{%- endblock -%}

作为另一个示例,让我们为 live collection type 创建一个通用的 bootstrap 5 主题,在表格行中渲染每个项目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
{%- block live_collection_widget -%}
    <table class="table table-borderless form-no-mb">
        <thead>
        <tr>
            {% for child in form|last %}
                <td>{{ form_label(child) }}</td>
            {% endfor %}
            <td></td>
        </tr>
        </thead>
        <tbody>
            {{ block('form_widget') }}
        </tbody>
    </table>
    {%- if skip_add_button|default(false) is same as(false) and button_add is defined and not button_add.rendered -%}
        {{ form_widget(button_add, { label: '+ Add Item', attr: { class: 'btn btn-outline-primary' } }) }}
    {%- endif -%}
{%- endblock -%}

{%- block live_collection_entry_row -%}
    <tr>
        {% for child in form %}
            <td>{{- form_row(child, { label: false }) -}}</td>
        {% endfor %}
        <td>
            {{- form_row(button_delete, { label: 'X', attr: { class: 'btn btn-outline-danger' } }) -}}
        </td>
    </tr>
{%- endblock -%}

要在模板中稍后渲染添加按钮,您可以最初使用 skip_add_button 跳过渲染,然后在之后手动渲染它

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<table class="table table-borderless form-no-mb">
    <thead>
        <tr>
            <td>Item</td>
            <td>Priority</td>
            <td></td>
        </tr>
    </thead>
    <tbody>
        {{ form_row(form.todoItems, { skip_add_button: true }) }}
    </tbody>
</table>

{{ form_widget(form.todoItems.vars.button_add, { label: '+ Add Item', attr: { class: 'btn btn-outline-primary' } }) }}

验证(没有表单)

注意

如果您的组件 包含表单,则验证是自动内置的。请按照这些文档了解更多详细信息。

如果您正在使用 Symfony 的 form component 构建表单,您仍然可以验证您的数据。

首先使用 ValidatableComponentTrait 并添加您需要的任何约束

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use App\Entity\User;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveProp;
use Symfony\UX\LiveComponent\ValidatableComponentTrait;

#[AsLiveComponent]
class EditUser
{
    use ValidatableComponentTrait;

    #[LiveProp(writable: ['email', 'plainPassword'])]
    #[Assert\Valid]
    public User $user;

     #[LiveProp]
     #[Assert\IsTrue]
    public bool $agreeToTerms = false;
}

请务必将 IsValid 属性/注释添加到您希望也验证该属性上的对象的任何属性。

由于此设置,组件现在将在每次渲染时自动验证,但以一种智能方式:属性只有在其“模型”已在前端更新后才会被验证。系统会跟踪哪些模型已更新,并且仅存储这些字段在重新渲染时的错误。

您还可以在操作中手动触发整个对象的验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use Symfony\UX\LiveComponent\Attribute\LiveAction;

#[AsLiveComponent]
class EditUser
{
    // ...

    #[LiveAction]
    public function save()
    {
        // this will throw an exception if validation fails
        $this->validate();

        // perform save operations
    }
}

如果验证失败,则会抛出异常,但组件将被重新渲染。在您的模板中,使用 _errors 变量渲染错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{% if _errors.has('post.content') %}
    <div class="error">
        {{ _errors.get('post.content') }}
    </div>
{% endif %}
<textarea
    data-model="post.content"
    class="{{ _errors.has('post.content') ? 'is-invalid' : '' }}"
></textarea>

{% if _errors.has('agreeToTerms') %}
    <div class="error">
        {{ _errors.get('agreeToTerms') }}
    </div>
{% endif %}
<input type="checkbox" data-model="agreeToTerms" class="{{ _errors.has('agreeToTerms') ? 'is-invalid' : '' }}"/>

<button
    type="submit"
    data-action="live#action:prevent"
    data-live-action-param="save"
>Save</button>

一旦组件经过验证,组件将“记住”它已被验证。这意味着,如果您编辑一个字段并且组件重新渲染,它将再次被验证。

重置验证错误

如果您想清除验证错误(例如,以便您可以再次重用表单),您可以调用 resetValidation() 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ...
class EditUser
{
    // ...

    #[LiveAction]
    public function save()
    {
        // validate, save, etc

        // reset your live props to the original state
        $this->user = new User();
        $this->agreeToTerms = false;
        // clear the validation state
        $this->resetValidation();
    }
}

更改时的实时验证

一旦启用验证,每个字段将在其模型更新的那一刻被验证。默认情况下,这发生在 input 事件中,因此当用户在文本字段中键入内容时。通常,这太多了(例如,您希望用户在验证电子邮件地址之前完成键入完整的电子邮件地址)。

要仅在“change”上验证,请使用 on(change) 修饰符

1
2
3
4
5
<input
    type="email"
    data-model="on(change)|user.email"
    class="{{ _errors.has('post.content') ? 'is-invalid' : '' }}"
>

延迟 / 懒加载组件

当页面加载时,所有组件都会立即渲染。如果组件渲染量很大,您可以将其渲染推迟到页面加载后。这是通过发出 Ajax 调用以加载组件的真实内容来完成的,可以在页面加载后立即加载 (defer) 或在组件变得可见时加载 (lazy)。

注意

在幕后,你的组件确实在初始页面加载时被创建并挂载,但它的模板并没有被渲染。因此,请将你的繁重工作放在组件的方法中(例如,getProducts()),这些方法仅从组件的模板中调用。

加载 “defer”(加载时 Ajax)

2.13.0

延迟加载组件的功能在 Live Components 2.13 版本中添加。

如果组件渲染开销很大,你可以延迟渲染它,直到页面加载完成后。要做到这一点,添加一个 loading="defer" 属性

1
2
{# With the HTML syntax #}
<twig:SomeHeavyComponent loading="defer" />
1
2
{# With the component function #}
{{ component('SomeHeavyComponent', { loading: 'defer' }) }}

这会渲染一个空的 <div> 标签,但会触发一个 Ajax 调用,以便在页面加载完成后渲染真正的组件。

加载 “lazy”(可见时 Ajax)

2.17.0

“懒加载”组件的功能在 Live Components 2.17 版本中添加。

lazy 选项类似于 defer,但它会将组件的加载延迟到它进入视口时。这对于位于页面底部且在用户滚动到它们之前不需要的组件非常有用。

要使用此功能,请将 loading="lazy" 属性设置为你的组件

1
2
{# With the HTML syntax #}
<twig:Acme foo="bar" loading="lazy" />
1
2
{# With the Twig syntax #}
{{ component('SomeHeavyComponent', { loading: 'lazy' }) }}

这会渲染一个空的 <div> 标签。真正的组件仅在它出现在视口中时才会被渲染。

Defer 还是 Lazy?

deferlazy 选项可能看起来相似,但它们服务于不同的目的: defer 对于渲染开销很大但在页面加载时需要的组件很有用。 lazy 对于用户滚动到它们之前不需要(甚至可能永远不会渲染)的组件很有用。

加载内容

你可以定义一些内容在组件加载时渲染,可以在组件模板内部(placeholder 宏)或从调用模板(loading-template 属性和 loadingContent 块)定义。

2.16.0

在组件模板中定义 placeholder 宏是在 Live Components 2.16.0 版本中添加的。

在组件模板中,定义一个 placeholder 宏,在组件的主要内容之外。当组件被延迟时,将调用此宏

1
2
3
4
5
6
7
8
9
10
11
12
{# templates/recommended-products.html.twig #}
<div {{ attributes }}>
    {# This will be rendered when the component is fully loaded #}
    {% for product in this.products %}
        <div>{{ product.name }}</div>
    {% endfor %}
</div>

{% macro placeholder(props) %}
    {# This content will (only) be rendered as loading content #}
    <span class="loading-row"></span>
{% endmacro %}

props 参数包含传递给组件的 props。你可以使用它来自定义占位符内容。假设你的组件显示一定数量的产品(用 size prop 定义)。你可以使用它来定义一个显示相同行数的占位符

1
2
{# In the calling template #}
<twig:RecommendedProducts size="3" loading="defer" />
1
2
3
4
5
6
7
8
{# In the component template #}
{% macro placeholder(props) %}
    {% for i in 1..props.size %}
        <div class="loading-product">
            ...
        </div>
    {% endfor %}
{% endmacro %}

要从调用模板自定义加载内容,你可以使用 loading-template 选项来指向一个模板

1
2
3
4
5
{# With the HTML syntax #}
<twig:SomeHeavyComponent loading="defer" loading-template="spinning-wheel.html.twig" />

{# With the component function #}
{{ component('SomeHeavyComponent', { loading: 'defer', 'loading-template': 'spinning-wheel.html.twig' }) }}

或覆盖 loadingContent

1
2
3
4
5
6
7
8
9
{# With the HTML syntax #}
<twig:SomeHeavyComponent loading="defer">
    <twig:block name="loadingContent">Custom Loading Content...</twig:block>
</twig:SomeHeavyComponent>

{# With the component tag #}
{% component SomeHeavyComponent with { loading: 'defer' } %}
    {% block loadingContent %}Loading...{% endblock %}
{% endcomponent %}

loading-templateloadingContent 被定义时,placeholder 宏将被忽略。

要将初始标签从 div 更改为其他内容,请使用 loading-tag 选项

1
{{ component('SomeHeavyComponent', { loading: 'defer', 'loading-tag': 'span' }) }}

轮询

你还可以使用“轮询”来持续刷新组件。在组件的顶层元素上,添加 data-poll

1
2
3
4
<div
      {{ attributes }}
+     data-poll
  >

这将每 2 秒发出一个请求以重新渲染组件。你可以通过添加 delay() 修饰符来更改此设置。当你这样做时,你需要明确表示你想调用 $render 方法。要延迟 500 毫秒

1
2
3
4
<div
    {{ attributes }}
    data-poll="delay(500)|$render"
>

你也可以触发一个特定的“动作”而不是正常的重新渲染

1
2
3
4
5
6
7
8
9
<div
    {{ attributes }}

    data-poll="save"
    {#
    Or add a delay() modifier:
    data-poll="delay(2000)|save"
    #}
>

当 LiveProp 更改时更改 URL

2.14

url 选项在 Live Components 2.14 版本中引入。

如果你希望在 LiveProp 更改时更新 URL,你可以使用 url 选项来实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// src/Twig/Components/SearchModule.php
namespace App\Twig\Components;

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

#[AsLiveComponent]
class SearchModule
{
    use DefaultActionTrait;

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

现在,当用户更改 query prop 的值时,URL 中的查询参数将被更新以反映组件的新状态,例如:https://my.domain/search?query=my+search+string

如果你在浏览器中加载此 URL,LiveProp 值将使用查询字符串初始化(例如,my search string)。

注意

URL 通过 history.replaceState() 更改。因此不会添加新条目。

支持的数据类型

你可以在你的 URL 绑定中使用标量、数组和对象

JavaScript prop URL 表示
'some search string' prop=some+search+string
42 prop=42
['foo', 'bar'] prop[0]=foo&prop[1]=bar
{ foo: 'bar', baz: 42 } prop[foo]=bar&prop[baz]=42

当页面加载时,如果查询参数绑定到一个 LiveProp(例如,/search?query=my+search+string),值 - my search string - 在设置到属性之前会经过 hydration 系统。如果一个值无法被 hydrated,它将被忽略。

多个查询参数绑定

你可以在你的组件中使用任意数量的 URL 绑定。为了确保状态完全在 URL 中表示,所有绑定的 props 都将被设置为查询参数,即使它们的值没有改变。

例如,如果你声明以下绑定

1
2
3
4
5
6
7
8
9
10
11
12
// ...
#[AsLiveComponent]
class SearchModule
{
    #[LiveProp(writable: true, url: true)]
    public string $query = '';

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

    // ...
}

并且你只设置 query 值,那么你的 URL 将被更新为 https://my.domain/search?query=my+query+string&mode=fulltext

控制查询参数名称

2.17

as 选项在 LiveComponents 2.17 版本中添加。

你可以使用 LiveProp 定义中的 as 选项,而不是使用 prop 的字段名作为查询参数名

1
2
3
4
5
6
7
8
9
10
11
// ...
use Symfony\UX\LiveComponent\Metadata\UrlMapping;

#[AsLiveComponent]
class SearchModule
{
    #[LiveProp(writable: true, url: new UrlMapping(as: 'q'))]
    public string $query = '';

    // ...
}

然后 query 值将出现在 URL 中,例如 https://my.domain/search?q=my+query+string

如果你需要在特定页面上更改参数名称,你可以利用 修饰符 选项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// ...
use Symfony\UX\LiveComponent\Metadata\UrlMapping;

#[AsLiveComponent]
class SearchModule
{
    #[LiveProp(writable: true, url: true, modifier: 'modifyQueryProp')]
    public string $query = '';

    #[LiveProp]
    public ?string $alias = null;

    public function modifyQueryProp(LiveProp $liveProp): LiveProp
    {
        if ($this->alias) {
            $liveProp = $liveProp->withUrl(new UrlMapping(as: $this->alias));
        }
        return $liveProp;
    }
}
1
<twig:SearchModule alias="q" />

通过这种方式,你还可以在同一页面中多次使用该组件,并避免参数名称冲突

1
2
<twig:SearchModule alias="q1" />
<twig:SearchModule alias="q2" />

验证查询参数值

与任何可写的 LiveProp 一样,由于用户可以修改此值,你应该考虑添加 验证。当你将 LiveProp 绑定到 URL 时,初始值不会自动验证。要验证它,你必须设置一个 PostMount hook

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// ...
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\UX\LiveComponent\ValidatableComponentTrait;
use Symfony\UX\TwigComponent\Attribute\PostMount;

#[AsLiveComponent]
class SearchModule
{
    use ValidatableComponentTrait;

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

    #[LiveProp(writable: true, url: true)]
    #[Assert\NotBlank]
    public string $mode = 'fulltext';

    #[PostMount]
    public function postMount(): void
    {
        // Validate 'mode' field without throwing an exception, so the component can
        // be mounted anyway and a validation error can be shown to the user
        if (!$this->validateField('mode', false)) {
            // Do something when validation fails
        }
    }

    // ...
}

注意

如果你想仅在 PostMount hook 中使用特定的验证规则,你可以使用 验证组

组件之间的通信:发射事件

2.8

发出事件的功能在 Live Components 2.8 版本中添加。

事件允许你在页面上的任何两个组件之间进行通信。

发射事件

有三种发出事件的方式

2.16

data-live-event-param 属性在 Live Components 2.16 版本中添加。以前,它被称为 data-event

  1. 来自 Twig

    1
    2
    3
    4
    <button
        data-action="live#emit"
        data-live-event-param="productAdded"
    >
  2. 来自你的 PHP 组件,通过 ComponentToolsTrait

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    use Symfony\UX\LiveComponent\ComponentToolsTrait;
    
    class MyComponent
    {
        use ComponentToolsTrait;
    
        #[LiveAction]
        public function saveProduct()
        {
            // ...
    
            $this->emit('productAdded');
        }
    }
  3. 来自 JavaScript,使用你的组件
1
this.component.emit('productAdded');

监听事件

要监听一个事件,添加一个方法,并在其上方添加 #[LiveListener]

1
2
3
4
5
6
7
8
#[LiveProp]
public int $productCount = 0;

#[LiveListener('productAdded')]
public function incrementProductCount()
{
    $this->productCount++;
}

因此,当任何其他组件发出 productAdded 事件时,将进行 Ajax 调用以调用此方法并重新渲染组件。

在幕后,事件监听器也是 LiveActions <actions>,因此你可以自动装配任何你需要的服务。

将数据传递给监听器

你还可以将额外的(标量)数据传递给监听器

1
2
3
4
5
6
7
8
9
#[LiveAction]
public function saveProduct()
{
    // ...

    $this->emit('productAdded', [
        'product' => $product->getId(),
    ]);
}

在你的监听器中,你可以通过在前面添加 #[LiveArg] 来访问与参数名称匹配的参数

1
2
3
4
5
6
#[LiveListener('productAdded')]
public function incrementProductCount(#[LiveArg] int $product)
{
    $this->productCount++;
    $this->lastProductId = $product;
}

并且由于事件监听器也是 actions,你可以使用实体名称类型提示参数,就像你在控制器中一样

1
2
3
4
5
6
#[LiveListener('productAdded')]
public function incrementProductCount(#[LiveArg] Product $product)
{
    $this->productCount++;
    $this->lastProduct = $product;
}

作用域事件

默认情况下,当一个事件被发出时,它会被发送到页面上所有组件。你可以通过各种方式限定这些范围

仅向父组件发出

如果你只想向父组件发出事件,请使用 emitUp() 方法

1
2
3
4
<button
    data-action="live#emitUp"
    data-live-event-param="productAdded"
>

或者,在 PHP 中

1
$this->emitUp('productAdded');

仅向具有特定名称的组件发出

如果你只想向具有特定名称的组件发出事件,请使用 name() 修饰符

1
2
3
4
<button
    data-action="live#emit"
    data-live-event-param="name(ProductList)|productAdded"
>

或者,在 PHP 中

1
$this->emit('productAdded', componentName: 'ProductList');

仅向自己发出

要仅向自己发出事件,请使用 emitSelf() 方法

1
2
3
4
<button
    data-action="live#emitSelf"
    data-live-event-param="productAdded"
>

或者,在 PHP 中

1
$this->emitSelf('productAdded');

分发浏览器/JavaScript 事件

有时你可能希望从你的组件分发一个 JavaScript 事件。你可以使用它来发出信号,例如,应该关闭一个模态框

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
use Symfony\UX\LiveComponent\ComponentToolsTrait;
// ...

class MyComponent
{
    use ComponentToolsTrait;

    #[LiveAction]
    public function saveProduct()
    {
        // ...

        $this->dispatchBrowserEvent('modal:close');
    }
}

这将在组件的顶层元素上分发一个 modal:close 事件。在自定义 Stimulus 控制器中监听此事件通常很方便 - 就像 Bootstrap 的模态框一样

1
2
3
4
5
6
7
8
9
10
11
12
// assets/controllers/bootstrap-modal-controller.js
import { Controller } from '@hotwired/stimulus';
import Modal from 'bootstrap/js/dist/modal';

export default class extends Controller {
    modal = null;

    initialize() {
        this.modal = Modal.getOrCreateInstance(this.element);
        window.addEventListener('modal:close', () => this.modal.hide());
    }
}

只需确保此控制器附加到模态框元素

1
2
3
4
5
<div class="modal fade" {{ stimulus_controller('bootstrap-modal') }}>
    <div class="modal-dialog">
        ... content ...
    </div>
</div>

你还可以将数据传递给事件

1
2
3
$this->dispatchBrowserEvent('product:created', [
    'product' => $product->getId(),
]);

这将成为事件的 detail 属性

1
2
3
window.addEventListener('product:created', (event) => {
    console.log(event.detail.product);
});

嵌套组件

需要在一个 live 组件内部嵌套另一个 live 组件吗?没问题!作为经验法则,每个组件都存在于其自身隔离的宇宙中。这意味着如果父组件重新渲染,它不会自动导致子组件重新渲染(但它可以 - 继续阅读)。或者,如果子组件中的模型更新,它也不会更新其父组件中的模型(但它可以 - 继续阅读)。

父子系统是智能的。通过一些技巧(例如嵌入组件列表的 key prop),你可以使其行为完全符合你的需求。

每个组件彼此独立地重新渲染

如果父组件重新渲染,默认情况下不会导致任何子组件重新渲染,但你可以使其这样做。让我们看一个待办事项列表组件的示例,其中有一个子组件渲染待办事项的总数

1
2
3
4
5
6
7
8
9
10
11
12
{# templates/components/TodoList.html.twig #}
<div {{ attributes }}>
    <input data-model="listName">

    {% for todo in todos %}
        ...
    {% endfor %}

    {{ component('TodoFooter', {
        count: todos|length
    }) }}
</div>

假设用户更新了 listName 模型,并且父组件重新渲染。在这种情况下,子组件将不会按设计重新渲染:每个组件都存在于其自己的宇宙中。

2.8

updateFromParent 选项在 Live Components 2.8 版本中添加。以前,当传递给子组件的任何 props 发生更改时,子组件都会重新渲染。

但是,如果用户添加了一个新的待办事项,那么我们确实希望 TodoFooter 子组件重新渲染:使用新的 count 值。要触发此操作,请在 TodoFooter 组件中,添加 updateFromParent 选项

1
2
3
4
5
6
#[LiveComponent]
class TodoFooter
{
    #[LiveProp(updateFromParent: true)]
    public int $count = 0;
}

现在,当父组件重新渲染时,如果 count prop 的值发生更改,子组件将发出第二个 Ajax 请求来重新渲染自身。

注意

要使其工作,渲染 TodoFooter 组件时传递的 prop 名称必须与具有 updateFromParent 的属性名称匹配 - 例如 {{ component('TodoFooter', { count: todos|length }) }}。如果你传入不同的名称并通过 mount() 方法设置 count 属性,子组件将无法正确重新渲染。

子组件保留其可修改的 LiveProp 值

如果在前面的示例中,TodoFooter 组件也具有 isVisible LiveProp(writable: true) 属性,该属性最初为 true,但可以更改(通过链接点击)为 false。当 count 更改时重新渲染子组件是否会导致它重置回其原始值?不会!当子组件重新渲染时,它将保留所有 props 的当前值,除了那些标记为 updateFromParent 的 props。

如果你确实希望当父组件中的某些值更改时,你的整个子组件都重新渲染(包括重置可写的 live props),该怎么办?这可以通过手动为你的组件提供一个 id 属性来实现,如果组件应该完全重新渲染,该属性将会更改

1
2
3
4
5
6
7
8
9
{# templates/components/TodoList.html.twig #}
<div {{ attributes }}>
    <!-- ... -->

    {{ component('TodoFooter', {
        count: todos|length,
        id: 'todo-footer-'~todos|length
    }) }}
</div>

在这种情况下,如果待办事项的数量发生变化,则组件的 id 属性也会发生变化。这表示组件应该完全重新渲染自身,丢弃任何可写的 LiveProp 值。

子组件中的动作不会影响父组件

再次强调,每个组件都是其自身隔离的宇宙!例如,假设你的子组件有

1
<button data-action="live#action" data-live-action-param="save">Save</button>

当用户点击该按钮时,它将尝试仅调用组件中的 save action,即使 save action 实际上只存在于父组件中。对于 data-model 也是如此,尽管在这种情况下有一些特殊的处理(请参阅下一点)。

与父组件通信

有两种主要方式可以从子组件向父组件通信

  1. 发出事件

    最灵活的通信方式:任何信息都可以从子组件发送到父组件。

  2. 从子组件更新父组件模型

    作为“同步”子组件模型和父组件模型的简单方法很有用:当子组件模型更改时,父组件模型也会更改。

从子组件更新父模型

假设一个子组件有一个

1
<textarea data-model="value">

当用户更改此字段时,这将更新组件中的 value 字段……因为(是的,我们再说一遍):每个组件都是其自身隔离的宇宙。

但是,有时这不是你想要的!有时,当子组件模型更改时,这也应该更新父组件上的模型。要做到这一点,请将 dataModel (或 data-model)属性传递给子组件

1
2
3
4
5
{# templates/components/PostForm.html.twig #}
{{ component('TextareaField', {
    dataModel: 'content',
    error: _errors.get('content'),
}) }}

这会做两件事

  1. 一个名为 value 的 prop 将被传递到 TextareaField,设置为来自父组件的 content(即与手动将 value: content 传递到组件相同)。
  2. value prop 在 TextareaField 内部更改时,content prop 将在父组件上更改。

结果是,当 value 更改时,父组件也会重新渲染,这要归功于其 content prop 发生了更改。

注意

如果你在服务器上更改子组件的 LiveProp (例如,在重新渲染期间或通过 action),该更改将不会反映在共享该模型的任何父组件上。

你还可以使用 parentProp:childProp 语法指定子组件 prop 的名称。以下内容与上面相同

1
2
3
4
<!-- same as dataModel: 'content' -->
{{ component('TextareaField', {
    dataModel: 'content:value',
}) }}

如果你的子组件有多个模型,请用空格分隔每个模型

1
2
3
{{ component('TextareaField', {
    dataModel: 'user.firstName:first user.lastName:last',
}) }}

在这种情况下,子组件将接收 firstlast props。并且,当这些更新时,父组件上的 user.firstNameuser.lastName 模型将被更新。

完整的嵌入式组件示例

让我们看一个完整的、复杂的嵌入组件示例。假设你有一个 EditPost

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
namespace App\Twig\Components;

use App\Entity\Post;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveAction;
use Symfony\UX\LiveComponent\Attribute\LiveProp;

#[AsLiveComponent]
final class EditPost extends AbstractController
{
    #[LiveProp(writable: ['title', 'content'])]
    public Post $post;

    #[LiveAction]
    public function save(EntityManagerInterface $entityManager)
    {
        $entityManager->flush();

        return $this->redirectToRoute('some_route');
    }
}

和一个 MarkdownTextarea

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
namespace App\Twig\Components;

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

#[AsLiveComponent]
final class MarkdownTextarea
{
    #[LiveProp]
    public string $label;

    #[LiveProp]
    public string $name;

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

EditPost 模板中,你渲染 MarkdownTextarea

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{# templates/components/EditPost.html.twig #}
<div {{ attributes }}>
    <form data-model="on(change)|*">
        <input
            type="text"
            name="post[title]"
            value="{{ post.title }}"
        >

        {{ component('MarkdownTextarea', {
            name: 'post[content]',
            dataModel: 'post.content:value',
            label: 'Content',
        }) }}

        <button
            data-action="live#action"
            data-live-action-param="save"
        >Save</button>
    </form>
</div>
1
2
3
4
5
6
7
8
9
10
<div {{ attributes }} class="mb-3">
    <textarea
        name="{{ name }}"
        data-model="value"
    ></textarea>

    <div class="markdown-preview">
        {{ value|markdown_to_html }}
    </div>
</div>

请注意,MarkdownTextarea 允许传入动态的 name 属性。这使得该组件可以在任何表单中重复使用。

元素列表的渲染怪癖

如果你在组件中渲染元素列表,为了帮助 LiveComponents 理解在重新渲染之间哪个元素是哪个(即,如果某些元素重新排序或删除某些元素),你可以为每个元素添加一个 id 属性

1
2
3
4
5
6
{# templates/components/Invoice.html.twig #}
{% for lineItem in lineItems %}
    <div id="{{ lineItem.id }}">
        {{ lineItem.name }}
    </div>
{% endfor %}

嵌入式组件列表的渲染怪癖

想象一下,你的组件渲染一个子组件列表,并且当用户在搜索框中键入内容或单击项目上的“删除”时,列表会发生变化……在这种情况下,可能会删除错误的子组件,或者现有的子组件可能不会在应该消失时消失。

2.8

key prop 在 Symfony UX Live Component 2.8 版本中添加。

要修复此问题,请为每个子组件添加一个 key prop,该 prop 对该组件是唯一的

1
2
3
4
5
6
7
{# templates/components/InvoiceCreator.html.twig #}
{% for lineItem in invoice.lineItems %}
    {{ component('InvoiceLineItemForm', {
        lineItem: lineItem,
        key: lineItem.id,
    }) }}
{% endfor %}

key 将用于生成 id 属性,该属性将用于识别每个子组件。你也可以直接传入 id 属性,但 key 更方便一些。

循环 + “新建” 项的技巧

让我们更花哨一点。在循环遍历当前行项目之后,你决定再渲染一个组件来创建一个新的行项目。在这种情况下,你可以传入一个 key,设置为类似 new_line_item 的内容

1
2
3
4
5
6
{# templates/components/InvoiceCreator.html.twig #}
// ... loop and render the existing line item components

{{ component('InvoiceLineItemForm', {
    key: 'new_line_item',
}) }}

想象一下,你还有一个 LiveActionInvoiceLineItemForm 内部,它将新的行项目保存到数据库。为了更加花哨,它向父组件发出一个 lineItem:created 事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// src/Twig/InvoiceLineItemForm.php
// ...

#[AsLiveComponent]
final class InvoiceLineItemForm
{
    // ...

    #[LiveProp]
    #[Valid]
    public ?InvoiceLineItem $lineItem = null;

    #[PostMount]
    public function postMount(): void
    {
        if (!$this->lineItem) {
            $this->lineItem = new InvoiceLineItem();
        }
    }

    #[LiveAction]
    public function save(EntityManagerInterface $entityManager)
    {
        if (!$this->lineItem->getId()) {
            $this->emit('lineItem:created', $this->lineItem);
        }

        $entityManager->persist($this->lineItem);
        $entityManager->flush();
    }
}

最后,父组件 InvoiceCreator 监听此事件,以便它可以重新渲染行项目(现在将包含新保存的项目)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/Twig/InvoiceCreator.php
// ...

#[AsLiveComponent]
final class InvoiceCreator
{
    // ...

    #[LiveListener('lineItem:created')]
    public function addLineItem()
    {
        // no need to do anything here: the component will re-render
    }
}

这将完美地工作:当一个新的行项目被保存时,InvoiceCreator 组件将重新渲染,并且新保存的行项目将与底部的额外 new_line_item 组件一起显示。

但可能会发生一些令人惊讶的事情:new_line_item 组件不会更新!它将保留片刻之前的数据和 props(即,表单字段仍将包含数据),而不是渲染一个新的、空的组件。

为什么?当 live 组件重新渲染时,它认为页面上现有的 key: new_line_item 组件与它即将渲染的相同的新组件相同。并且由于传递给该组件的 props 没有改变,因此它看不到任何重新渲染它的理由。

要修复此问题,你有两个选项

1) 使 key 动态化,以便在添加新项目后它将有所不同

1
2
3
{{ component('InvoiceLineItemForm', {
    key: 'new_line_item_'~lineItems|length,
}) }}

2) 在 InvoiceLineItemForm 组件保存后重置其状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// src/Twig/InvoiceLineItemForm.php
// ...

#[AsLiveComponent]
class InvoiceLineItemForm
{
    // ...

    #[LiveAction]
    public function save(EntityManagerInterface $entityManager)
    {
        $isNew = null === $this->lineItem->getId();

        $entityManager->persist($this->lineItem);
        $entityManager->flush();

        if ($isNew) {
            // reset the state of this component
            $this->emit('lineItem:created', $this->lineItem);
            $this->lineItem = new InvoiceLineItem();
            // if you're using ValidatableComponentTrait
            $this->clearValidation();
        }
    }
}

将内容(块)传递给组件

通过块将内容传递给 Live 组件的工作方式与你将内容传递给 Twig 组件的方式完全相同。但有一个重要的区别:当组件重新渲染时,任何仅在“外部”模板中定义的变量都将不可用。例如,这将不起作用

1
2
3
4
5
6
{# templates/some_page.html.twig #}
{% set message = 'Variables from the outer part of the template are only available during  the initial render' %}

{% component Alert %}
    {% block content %}{{ message }}{% endblock %}
{% endcomponent %}

局部变量仍然可用

1
2
3
4
5
6
7
{# templates/some_page.html.twig #}
{% component Alert %}
    {% block content %}
        {% set message = 'this works during re-rendering!' %}
        {{ message }}
    {% endblock %}
{% endcomponent %}

钩子:处理组件行为

大多数时候,你只需将数据传递给你的组件,让它处理剩下的事情。但是,如果你需要在组件生命周期的某些阶段执行更复杂的操作,你可以利用生命周期钩子。

PostHydrate 钩子

#[PostHydrate] 钩子在组件的状态从客户端加载后立即调用。如果你需要在数据被 hydrated 后处理或调整数据,这将非常有用。

PreDehydrate 钩子

#[PreDehydrate] 钩子在你的组件的状态被发送回客户端之前触发。你可以在数据被序列化并返回到客户端之前使用它来修改或清理数据。

PreReRender 钩子

#[PreReRender] 钩子在 HTTP 请求期间重新渲染组件之前调用。它不会在初始渲染期间运行,但在你需要在重新渲染之前调整状态并将其发送回客户端时很有用。

钩子优先级

你可以使用 priority 参数控制钩子的执行顺序。如果在组件中注册了多个相同类型的钩子,则优先级值较高的钩子将首先运行。这允许你管理在同一生命周期阶段内执行操作的顺序

1
2
3
4
5
6
7
8
9
10
11
#[PostHydrate(priority: 10)]
public function highPriorityHook(): void
{
    // Runs first
}

#[PostHydrate(priority: 1)]
public function lowPriorityHook(): void
{
    // Runs last
}

高级功能

智能重新渲染算法

当组件重新渲染时,新的 HTML 会“变形”到页面上现有的元素上。例如,如果重新渲染包含现有元素上的新 class,则该 class 将被添加到该元素。

2.8

智能重新渲染算法在 LiveComponent 2.8 版本中引入。

渲染系统也很智能,足以知道元素何时被 LiveComponents 系统外部的某些东西更改:例如,一些 JavaScript 向元素添加了一个 class。在这种情况下,当组件重新渲染时,该 class 将被保留。

该系统无法处理所有边缘情况,因此以下是一些需要记住的事项

  • 如果 JavaScript 更改了元素上的属性,则该更改将被保留
  • 如果 JavaScript 添加了一个新元素,则该元素将被保留
  • 如果 JavaScript 删除了最初由组件渲染的元素,则该更改将丢失:该元素将在下一次重新渲染期间重新添加。
  • 如果 JavaScript 更改了元素的文本,则该更改将丢失:在下一次重新渲染期间,它将被恢复为来自服务器的文本。
  • 如果元素从组件中的一个位置移动到另一个位置,则该更改将丢失:该元素将在下一次重新渲染期间重新添加到其原始位置。

神秘的 id 属性

在整个文档中多次提到 id 属性以解决各种问题。它通常不是必需的,但可能是解决某些复杂问题的关键。但它是什么?

注意

key prop 用于在子组件上创建一个 id 属性。因此,本节中的所有内容同样适用于 key prop。

id 属性是元素或组件的唯一标识符。它在组件重新渲染时的变形过程中使用:它帮助 变形库 将现有 HTML 中的元素或组件与新的 HTML“连接”起来。

跳过更新某些元素

如果你的组件中有一个元素,你希望在组件重新渲染时更改它,你可以添加一个 data-live-ignore 属性

1
<input name="favorite_color" data-live-ignore>

但你很少甚至永远不需要这样做。即使你编写修改元素的 JavaScript,该更改也会被保留(请参阅 Live Components)。

注意

强制忽略的元素重新渲染,请为其父元素提供一个 id 属性。在重新渲染期间,如果此值发生更改,则元素的所有子元素都将重新渲染,即使是那些带有 data-live-ignore 的元素。

覆盖 HTML 而不是 Morphing

通常,当组件重新渲染时,新的 HTML 会“变形”到页面上现有的元素上。在某些极少数情况下,你可能希望简单地用新的 HTML 覆盖元素的现有内部 HTML,而不是对其进行变形。这可以通过添加 data-skip-morph 属性来完成

1
2
3
<select data-skip-morph>
    <option>...</option>
</select>

在这种情况下,对 <select> 元素属性的任何更改仍将被“变形”到现有元素上,但内部 HTML 将被覆盖。

为你的组件定义另一个路由

2.7

route 选项在 LiveComponents 2.7 版本中添加。

live 组件的默认路由是 /components/{_live_component}/{_live_action}。有时,自定义此 URL 可能很有用 - 例如,使组件位于特定的防火墙下。

要使用不同的路由,首先声明它

1
2
3
4
5
# config/routes.yaml
live_component_admin:
    path: /admin/_components/{_live_component}/{_live_action}
    defaults:
        _live_action: 'get'

然后在你的组件上指定这个新路由

1
2
3
4
5
6
7
8
9
10
// src/Twig/Components/RandomNumber.php
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\DefaultActionTrait;

- #[AsLiveComponent]
+ #[AsLiveComponent(route: 'live_component_admin')]
  class RandomNumber
  {
      use DefaultActionTrait;
  }

2.14

urlReferenceType 选项在 LiveComponents 2.14 版本中添加。

你还可以控制生成的 URL 的类型

1
2
3
4
5
6
7
8
9
10
11
// src/Twig/Components/RandomNumber.php
+ use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
  use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
  use Symfony\UX\LiveComponent\DefaultActionTrait;

- #[AsLiveComponent]
+ #[AsLiveComponent(urlReferenceType: UrlGeneratorInterface::ABSOLUTE_URL)]
  class RandomNumber
  {
      use DefaultActionTrait;
  }

在 LiveProp 更新时添加钩子

2.12

onUpdated 选项在 LiveComponents 2.12 版本中添加。

如果你想在特定的 LiveProp 更新后运行自定义代码,你可以通过添加一个 onUpdated 选项来实现,该选项设置为组件上的公共方法名称

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#[AsLiveComponent]
class ProductSearch
{
    #[LiveProp(writable: true, onUpdated: 'onQueryUpdated')]
    public string $query = '';

    // ...

    public function onQueryUpdated($previousValue): void
    {
        // $this->query already contains a new value
        // and its previous value is passed as an argument
    }
}

一旦 query LiveProp 被更新,onQueryUpdated() 方法将被调用。先前的值作为第一个参数传递到那里。

如果你允许对象属性是可写的,你也可以监听一个特定键的更改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
use App\Entity\Post;

#[AsLiveComponent]
class EditPost
{
    #[LiveProp(writable: ['title', 'content'], onUpdated: ['title' => 'onTitleUpdated'])]
    public Post $post;

    // ...

    public function onTitleUpdated($previousValue): void
    {
        // ...
    }
}

动态设置 LiveProp 选项

2.17

modifier 选项在 LiveComponents 2.17 版本中添加。

如果你需要动态配置 LiveProp 的选项,你可以使用 modifier 选项来使用组件中的自定义方法,该方法返回你的 LiveProp 的修改版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#[AsLiveComponent]
class ProductSearch
{
    #[LiveProp(writable: true, modifier: 'modifyAddedDate')]
    public ?\DateTimeImmutable $addedDate = null;

    #[LiveProp]
    public string $dateFormat = 'Y-m-d';

    // ...

    public function modifyAddedDate(LiveProp $prop): LiveProp
    {
        return $prop->withFormat($this->dateFormat);
    }
}

然后,在模板中使用你的组件时,你可以更改用于 $addedDate 的日期格式

1
2
3
{{ component('ProductSearch', {
    dateFormat: 'd/m/Y'
}) }}

所有 LiveProp::with* 方法都是不可变的,因此你需要使用它们的返回值作为你的新 LiveProp。

注意

避免依赖于在其他修饰符方法中也使用修饰符的 props。例如,如果上面的 $dateFormat 属性也具有 modifier 选项,那么从 modifyAddedDate 修饰符方法中引用它将是不安全的。这是因为 $dateFormat 属性此时可能尚未被 hydrated。

调试组件

需要列出或调试一些组件问题。 Twig Component debug 命令 可以帮助你。

测试助手

2.11

测试助手在 LiveComponents 2.11 版本中添加。

与 Live-Components 交互

对于测试,你可以使用 InteractsWithLiveComponents trait,它使用 Symfony 的测试客户端来渲染和向你的组件发出请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\UX\LiveComponent\Test\InteractsWithLiveComponents;

class MyComponentTest extends KernelTestCase
{
    use InteractsWithLiveComponents;

    public function testCanRenderAndInteract(): void
    {
        $testComponent = $this->createLiveComponent(
            name: 'MyComponent', // can also use FQCN (MyComponent::class)
            data: ['foo' => 'bar'],
        );

        // render the component html
        $this->assertStringContainsString('Count: 0', $testComponent->render());

        // call live actions
        $testComponent
            ->call('increase')
            ->call('increase', ['amount' => 2]) // call a live action with arguments
        ;

        $this->assertStringContainsString('Count: 3', $testComponent->render());

        // call live action with file uploads
        $testComponent
            ->call('processUpload', files: ['file' => new UploadedFile(...)]);

        // emit live events
        $testComponent
            ->emit('increaseEvent')
            ->emit('increaseEvent', ['amount' => 2]) // emit a live event with arguments
        ;

        // set live props
        $testComponent
            ->set('count', 99)
        ;

        // Submit form data ('my_form' for your MyFormType form)
        $testComponent
            ->submitForm(['my_form' => ['input' => 'value']], 'save');

        $this->assertStringContainsString('Count: 99', $testComponent->render());

        // refresh the component
        $testComponent->refresh();

        // access the component object (in its current state)
        $component = $testComponent->component(); // MyComponent

        $this->assertSame(99, $component->count);

        // test a live action that redirects
        $response = $testComponent->call('redirect')->response(); // Symfony\Component\HttpFoundation\Response

        $this->assertSame(302, $response->getStatusCode());

        // authenticate a user ($user is instance of UserInterface)
        $testComponent->actingAs($user);

        // set the '_locale' route parameter (if the component route is localized)  
        $testComponent->setRouteLocale('fr');

        // customize the test client
        $client = self::getContainer()->get('test.client');

        // do some stuff with the client (ie login user via form)

        $testComponent = $this->createLiveComponent(
            name: 'MyComponent',
            data: ['foo' => 'bar'],
            client: $client,
        );
    }
}

注意

InteractsWithLiveComponents trait 只能在扩展 Symfony\Bundle\FrameworkBundle\Test\KernelTestCase 的测试中使用。

测试 LiveCollectionType

要测试在包含 LiveCollectionType 的 Live Component 中提交表单(使用上面的 submitForm 助手),你首先需要以编程方式向表单添加所需数量的条目,复制单击“添加”按钮的操作。

因此,如果以下是使用的表单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
use Symfony\UX\LiveComponent\Form\Type\LiveCollectionType;

// Parent FormType used in the Live Component
class LiveCollectionFormType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder->add('children', LiveCollectionType::class, [
                'entry_type' => ChildFormType::class,
            ])
        ;
    }
}

// Child Form Type used for each entry in the collection
class ChildFormType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('name', TextType::class)
            ->add('age', IntegerType::class)
        ;
    }
}

在提交表单之前,使用 LiveCollectionTrait 中的 addCollectionItem 方法来动态地向表单的 children 字段添加条目

1
2
3
4
5
6
7
8
9
10
11
12
// Call the addCollectionItem method as many times as needed, specifying the name of the collection field.
$component->call('addCollectionItem', ['name' => 'children']);
$component->call('addCollectionItem', ['name' => 'children']);
//... can be called as many times as you need entries in your 'children' field

// ... then submit the form by providing data for all the fields in the ChildFormType for each added entry:
$component->submitForm([ 'live_collection_form' => [
    'children' => [
        ['name' => 'childName1', 'age' => 10],
        ['name' => 'childName2', 'age' => 15],
    ]
]]);

向后兼容性承诺

此 bundle 旨在遵循与 Symfony 框架相同的向后兼容性承诺:http://symfony.ac.cn/doc/current/contributing/code/bc.html

对于 JavaScript 文件,公共 API(即,文档化的功能和来自主 JavaScript 文件的导出)受到向后兼容性承诺的保护。但是,JavaScript 文件中的任何内部实现(即,来自内部文件的导出)都不受保护。

这项工作,包括代码示例,已根据 Creative Commons BY-SA 3.0 许可获得许可。
目录
    版本