跳到內容

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') }}

當使用者在方塊中輸入時,組件將自動重新渲染並顯示新的結果!

想要一些示範嗎?請查看 https://ux.symfony.com/live-component#demo

安裝

使用 Composer 和 Symfony Flex 安裝套件

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 Hook: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 屬性,然後在他們執行此操作時重新渲染組件?當然可以!而正是即時組件真正發光發熱的地方。

將輸入新增至範本

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 方塊中輸入時,組件將在該範圍內使用新的隨機數字重新渲染。

防抖動 (Debouncing)

如果使用者快速輸入 5 個字元,我們不希望傳送 5 個 Ajax 請求。幸運的是,live components 新增了自動防抖動:它會在輸入之間等待 150 毫秒的暫停,然後再傳送 Ajax 請求以重新渲染。這是內建的,因此您無需考慮它。但是,您可以透過 debounce 修飾符延遲

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

欄位「變更」時延遲更新

有時,您可能希望欄位僅在使用者變更輸入移動到另一個欄位後才重新渲染。瀏覽器在這種情況下會分派 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 屬性為必要項。

若要啟用此功能,您必須將 data-model 屬性新增至 <form> 元素

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 被傳送到前端時,它們會被「脫水 (dehydrated)」。當從前端發送 Ajax 請求時,脫水的資料接著會被「補水 (hydrated)」回原始狀態。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;
}

請注意,能夠變更物件的「身分 (identity)」僅適用於脫水為純量值的物件(例如持久化實體,它們會脫水為 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 不能有任何建構子引數。

如果此解決方案不符合您的需求,還有其他兩種選項可以使其運作

使用序列化器進行水合 (Hydrating)

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

在 TwigComponents 2.9 中新增了將 defaults() 方法與 stimulus_controller() 一起使用的功能,並且需要 symfony/stimulus-bundle。先前,stimulus_controller() 傳遞至 attributes.add()

若要將自訂 Stimulus 控制器新增至您的根組件元素

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

JavaScript 組件 Hook

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!這是完全允許的,並讓您可以存取所有正常的控制器快速鍵。我們甚至新增了一個快閃訊息!

上傳檔案

2.11

將檔案上傳到動作的功能在 2.11 版中新增。

預設情況下,檔案不會傳送到組件。您需要使用即時動作來處理檔案,並告知組件何時應傳送檔案

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 中使用表單資料

每次發出 Ajax 呼叫以重新渲染 live component 時,都會使用最新資料自動提交表單。

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

  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 中動態更新表單

當發出 Ajax 呼叫以重新渲染 live component 時(無論是由於模型變更還是 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 也無效:實際提交的標題會覆寫該標題。

表單渲染問題

在大多數情況下,在組件內渲染表單的效果非常好。但是,在某些情況下,您的表單可能無法按您想要的方式運作。

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 指向單獨的範本。請參閱如何使用表單主題

覆寫通用按鈕和集合項目

新增和刪除按鈕會渲染為單獨的 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 相關的區塊。這與傳統集合類型的運作方式相同,但您應該改用 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 的表單組件,您仍然可以驗證您的資料。

首先使用 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

「延遲 (lazy)」載入組件的功能在 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> 標籤。真實的組件僅在它出現在可視範圍內時才會渲染。

延後或延遲?

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。您可以使用它來自訂 placeholder 內容。假設您的元件顯示特定數量的產品(以 size prop 定義)。您可以使用它來定義一個顯示相同行數的 placeholder。

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' }) }}

輪詢 (Polling)

您也可以使用「輪詢 (polling)」來持續重新整理元件。在您的元件的最上層元素上,新增 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"
>

您也可以觸發特定的「動作 (action)」來取代正常的重新渲染。

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 系統。如果值無法被 hydration,它將會被忽略。

多個查詢參數綁定

您可以在元件中使用任意數量的 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 版本中新增的。

您可以不使用 prop 的欄位名稱作為查詢參數名稱,而是使用 LiveProp 定義中的 as 選項。

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 值將會以 https://my.domain/search?q=my+query+string 的形式出現在 URL 中。

如果您需要在特定頁面上變更參數名稱,您可以利用 modifier 選項。

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,由於使用者可以修改這個值,您應該考慮新增 驗證 (validation)。當您將 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 中使用特定的驗證規則,您可以使用 驗證群組 (validation groups)

組件之間的通訊:發射事件

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>,因此您可以自動注入 (autowire) 您需要的任何服務。

將資料傳遞至監聽器

您也可以將額外的(純量)資料傳遞給監聽器。

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,您可以對參數進行類型提示 (type-hint),使用實體名稱,就像在控制器中一樣。

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 事件。您可以使用這個來發出信號,例如,應該關閉一個彈出視窗 (modal)。

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 controller 中監聽這個事件通常很方便 - 就像 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());
    }
}

只需確保這個 controller 已附加到彈出視窗元素即可。

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 component 巢狀放入另一個 live component 中嗎?沒問題!根據經驗法則,每個元件都存在於自己獨立的世界中。這表示如果父元件重新渲染,它不會自動導致子元件重新渲染(但可以 - 請繼續閱讀)。或者,如果子元件中的 model 更新,它也不會更新其父元件中的 model(但可以 - 請繼續閱讀)。

父子系統是智慧的。透過一些技巧(例如嵌入式元件列表的 key prop),您可以使其行為完全符合您的需求。

每個組件彼此獨立重新渲染

如果父元件重新渲染,預設情況下不會導致任何子元件重新渲染,但您可以使其執行此操作。讓我們來看一個範例,一個 todo 列表元件,其子元件渲染 todo 項目總數。

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 model,且父元件重新渲染。在這種情況下,子元件將不會依設計重新渲染:每個元件都存在於自己的世界中。

2.8

updateFromParent 選項是在 Live Components 2.8 版本中新增的。先前,當傳遞給子元件的任何 props 變更時,子元件都會重新渲染。

然而,如果使用者新增了一個新的 todo 項目,那麼我們確實希望 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>

在這種情況下,如果 todo 的數量變更,則元件的 id 屬性也會變更。這表示元件應該完全重新渲染自身,並捨棄任何可寫入的 LiveProp 值。

子組件中的動作不會影響父組件

再次強調,每個元件都是自己獨立的世界!例如,假設您的子元件具有:

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

當使用者點擊該按鈕時,它會嘗試僅在元件中呼叫 save action,即使 save action 實際上只存在於父元件中。對於 data-model 也是如此,儘管對於這種情況有一些特殊的處理方式(請參閱下一個重點)。

與父組件通訊

有兩種主要的從子元件與父元件溝通的方式:

  1. 發射事件

    最靈活的溝通方式:任何資訊都可以從子元件發送到父元件。

  2. 從子元件更新父元件 model

    作為一種簡單的方式來「同步」子元件 model 與父元件 model 非常有用:當子元件 model 變更時,父元件 model 也會變更。

從子組件更新父模型

假設一個子元件具有:

1
<textarea data-model="value">

當使用者變更這個欄位時,這將更新元件中的 value 欄位…因為(是的,我們再次強調):每個元件都是自己獨立的世界。

然而,有時這不是您想要的!有時,當子元件 model 變更時,也應該更新父元件上的 model。若要執行此操作,請將 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),則該變更將不會反映在任何共享該 model 的父元件上。

您也可以使用 parentProp:childProp 語法來指定子元件 prop 的名稱。以下與上述相同:

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

如果您的子元件有多個 models,請用空格分隔每個 model。

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

在這種情況下,子元件將會收到 firstlast props。而且,當這些 props 更新時,父元件上的 user.firstNameuser.lastName models 將會更新。

完整嵌入組件範例

讓我們來看一個完整的、複雜的嵌入式元件範例。假設您有一個 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,該 key 對於該元件是唯一的。

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',
}) }}

想像一下,您在 InvoiceLineItemForm 內部也有一個 LiveAction,它將新的行項目儲存到資料庫。為了更進階,它會向父元件發射一個 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 components 重新渲染時,它會認為頁面上現有的 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();
        }
    }
}

將內容 (區塊) 傳遞至組件

透過區塊 (blocks) 將內容傳遞給 Live components 的運作方式與您將內容 傳遞給 Twig Components 的方式完全相同。但有一個重要的差異:當元件重新渲染時,任何僅在「外部」範本中定義的變數將不可用。例如,這將無法運作:

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 %}

Hook:處理組件行為

大多數時候,您只需將資料傳遞給您的元件,讓它處理其餘的事情。但是,如果您需要在元件生命週期的特定階段執行更複雜的操作,您可以利用生命週期鉤子 (lifecycle hooks)。

PostHydrate Hook

#[PostHydrate] hook 會在元件的狀態從客戶端載入後立即呼叫。如果您需要在 hydration 後處理或調整資料,這會很有用。

PreDehydrate Hook

#[PreDehydrate] hook 會在您的元件的狀態發送回客戶端之前觸發。您可以使用它來修改或清理資料,然後再將其序列化並返回給客戶端。

PreReRender Hook

#[PreReRender] hook 會在 HTTP 請求期間重新渲染元件之前呼叫。它不會在初始渲染期間執行,但當您需要在將狀態發送回客戶端以進行重新渲染之前調整狀態時,它會很有用。

Hook 優先順序

您可以使用 priority 參數來控制 hook 的執行順序。如果在一個元件中註冊了多個相同類型的 hook,則具有較高 priority 值的 hook 將會先執行。這允許您管理在同一個生命週期階段中執行 actions 的順序。

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 會「morph」到頁面上的現有元素上。例如,如果重新渲染在現有元素上包含一個新的 class,則該 class 將會新增到該元素。

2.8

智慧型重新渲染演算法是在 LiveComponent 2.8 版本中引入的。

渲染系統也夠智慧,可以知道元素何時被 LiveComponents 系統外部的某些東西變更:例如,一些 JavaScript 將一個 class 新增到一個元素。在這種情況下,當元件重新渲染時,該 class 將會被保留。

系統無法處理所有邊緣情況,因此以下是一些需要記住的事項:

  • 如果 JavaScript 變更了元素上的屬性,則該變更會被保留
  • 如果 JavaScript 新增了一個新元素,則該元素會被保留
  • 如果 JavaScript 移除了一個原本由元件渲染的元素,則該變更將會遺失:該元素將會在下一次重新渲染期間重新新增。
  • 如果 JavaScript 變更了元素的文字,則該變更將會遺失:它將會在下一次重新渲染期間還原為來自伺服器的文字。
  • 如果元素從元件中的一個位置移動到另一個位置,則該變更將會遺失:該元素將會在下一次重新渲染期間重新新增到其原始位置。

神秘的 id 屬性

在整個文件中多次提到 id 屬性以解決各種問題。它通常不是必需的,但可能是解決某些複雜問題的關鍵。但它到底是什麼?

注意

key prop 用於在子元件上建立 id 屬性。因此,本節中的所有內容同樣適用於 key prop。

id 屬性是元素或元件的唯一識別符。它在元件重新渲染時的 morphing 過程中使用:它幫助 morphing library 將現有 HTML 中的元素或元件與新的 HTML「連接」起來。

略過更新特定元素

如果您有一個元件內部的元素,您希望在元件重新渲染時變更它,您可以新增一個 data-live-ignore 屬性。

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

但您應該很少需要這個,如果有的話。即使您編寫 JavaScript 來修改元素,該變更也會被保留(請參閱 Live Components)。

注意

若要強制忽略的元素重新渲染,請為其父元素提供一個 id 屬性。在重新渲染期間,如果這個值變更,則該元素的所有子元素都將會重新渲染,即使是那些具有 data-live-ignore 的元素。

覆寫 HTML 而非變形 (Morphing)

通常,當元件重新渲染時,新的 HTML 會「morph」到頁面上的現有元素上。在某些極少數情況下,您可能希望僅使用新的 HTML 覆寫元素的現有 inner HTML,而不是 morphing 它。這可以透過新增 data-skip-morph 屬性來完成。

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

在這種情況下,對 <select> 元素屬性的任何變更仍然會「morph」到現有元素上,但 inner HTML 將會被覆寫。

為您的組件定義另一個路由

2.7

route 選項是在 LiveComponents 2.7 版本中新增的。

live components 的預設路由是 /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 更新時新增 Hook

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。

注意

避免在其他 modifiers 方法中依賴也使用 modifier 的 props。例如,如果上面的 $dateFormat 屬性也具有 modifier 選項,那麼從 modifyAddedDate modifier 方法中引用它將是不安全的。這是因為 $dateFormat 屬性在此時可能尚未 hydration。

偵錯組件

需要列出或偵錯一些元件問題嗎?Twig Component debug 命令 可以幫助您。

測試輔助程式

2.11

測試 helper 是在 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

若要測試在 Live Component 中提交表單(使用上面的 submitForm helper),其中包含 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
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 framework 相同的向後相容性承諾:https://symfony.dev.org.tw/doc/current/contributing/code/bc.html

對於 JavaScript 檔案,公用 API(即,主要 JavaScript 檔案中記錄的功能和 exports)受到向後相容性承諾的保護。但是,JavaScript 檔案中的任何內部實作(即,來自內部檔案的 exports)都不受保護。

這項工作,包括程式碼範例,是根據 Creative Commons BY-SA 3.0 授權條款授權的。
目錄
    版本