[Blazor] 客製化元件

不管是哪一個前端的框架或套件,寫一個專屬於自己專案的元件,是很基本的行為,今天要來分享的是在 Blazor 中建立元件的一些小撇步。

two-way binding (雙向綁定)

基本使用

Blazor 是一個標榜可以做到雙向綁定的一個前端框架,先來看看最基本的範例,使用 bind 這個前綴就可以做到雙向綁定,等於是把 value 以及 onchange 做整合。

1
2
3
4
5
6
<input @bind="data" />
<input value="@data" @onchange="@((ChangeEventArgs __e) => data = __e.Value.ToString())" />

@code {
string data;
}

透過上面的範例,可以發現到這兩個行為是相同的,從這邊可以理解到所謂的雙向綁定,也就是將做到輸入以及資料的輸出兩個行為做結合。

元件雙向綁定

根據上面的行為,我們要來嘗試所謂的雙向綁定,在 blazor 中有一個規則,只要事件的名稱結尾是 Changed 就可以搭配 bind 做雙向綁定。

底下我們先寫一個元件來測試雙向綁定是否正確,元件的名稱就直接叫做 Jimmy.razor,並且公開一個屬性叫做 Value

1
2
3
4
5
6
7
8
9
10
11
<button @onclick="clickChange">change value</button>

@code {
[Parameter] public string Value { get; set; }
[Parameter] public EventCallback<string> ValueChanged { get; set; }

void clickChange()
{
ValueChanged.InvokeAsync("jimmy");
}
}

我們就在另一個元件中使用剛剛所制定的 Jimmy 這個元件,並且將資料輸出在畫面上。

1
2
3
4
5
6
7
8
@page "/"

<Jimmy @bind-Value="data" />
<span>hi @data</span>

@code {
string data;
}

執行以後應該可以看到像下面的結果,雙向綁定的行為是成功的。
two-way-binding

多層雙向綁定

接著我們要寫一個元件叫做 TextField.razor,裡面會包含 label 以及 input,並且希望能夠直接將 input 的資料做雙向綁定。

如果只照著前面的做法來做,會遇到一個問題,input 本身有雙向綁定,會需要一個屬性來承接,但又要能直接跟開放出去的屬性作連接,這時可以使用一個新的屬性來做關聯。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<label>
@Text
<input @bind="Data" />
</label>

@code {
[Parameter] public string Text { get; set; }
[Parameter] public string Value { get; set; }
[Parameter] public EventCallback<string> ValueChanged { get; set; }
string Data {
get => Value;
set => ValueChanged.InvokeSync(value);
}
}

參數

未定義參數

如果是很明確所要開放的參數,通常我們會直接定義屬性,但 html 原生的屬性,就不會想要用這樣的方式一個一個去做開放,我們可以將 ParameterAttribute 的屬性 CaptureUnmatchedValues 設定為 true,就可以拿到剩下未定義但外面有傳的參數。

1
2
3
4
5
6
<div @attributes="Attributes" id="test" />

@code {
[Parameter(CaptureUnmatchedValues = true)]
public IReadOnlyDictionary<string, object> Attributes { get; set; }
}
1
<TextField id="jimmy" />

這邊可以看到輸出的 id 會是 test 而不是 jimmy ,要特別注意的是定義的順序,會影響到輸出的結果,如果希望以外面定義的為主,那就要把 @attributes 放在最後面。

重新組合 class

元件通常會有自己的樣式,也會提供給外面覆寫 class,我們可以透過上一個方法所定義的屬性,來取得外面所定義的 class。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<div class="@GetClass()" />

@code {
[Parameter(CaptureUnmatchedValues = true)]
public IReadOnlyDictionary<string, object> Attributes { get; set; }

string GetClass()
{
var baseClass = "d-flex flex-column";
if (Attributes != null && Attributes.TryGetValue("class", out var @class) && !string.IsNullOrEmpty(Convert.ToString(@class)))
{
return $"{baseClass} {@class}";
}

return baseClass;
}
}

使用樣板

在寫元件時,為了要開放給使用者決定部分區的內容,我們可以透過開放樣板屬性的方式來達成, RenderFragment 這個類別可以來承接 html 的內容。

使用預設名稱

只有屬性名稱叫做 ChildContent ,使用端才能省略不用屬性名稱包住內容。

1
2
3
4
5
6
7
8
<!-- JimmyTemplate.razor -->
<div class="button-wrapper">
@ChildContent
</div>

@code {
[Parameter] public RenderFragment ChildContent { get; set; }
}
1
2
3
4
<JimmyTemplate>
<button>confirm</button>
<button>cancel</button>
</JimmyTemplate>

非預設名稱

不管有幾個樣板,只要屬性名稱不叫 ChildContent ,使用端都要使用屬性名稱包住。

1
2
3
4
5
6
7
8
<!-- JimmyTemplate.razor -->
<div class="button-wrapper">
@CustomWrapper
</div>

@code {
[Parameter] public RenderFragment CustomWrapper { get; set; }
}
1
2
3
4
5
6
<JimmyTemplate>
<CustomWrapper>
<button>confirm</button>
<button>cancel</button>
</CustomWrapper>
</JimmyTemplate>

參考