原文鏈接:https://blazor-university.com/javascript-interop/calling-javascript-from-dotnet/passing-html-element-references/
傳遞 HTML 元素引用
源代碼[1]
在編寫 Blazor 應用程序時,不鼓勵對文檔對象模型 (DOM) 進行操作,因為它可能會干擾其增量渲染樹[2],對 HTML 的任何更改都應在我們組件內的 .NET 代碼中進行管理。
有時我們可能希望繼續讓 JavaScript 與我們生成的 HTML 交互。實現這一點的標準 JavaScript 方法是給我們的 HTML 元素一個 id,并讓 JavaScript 使用 document.getElementById('someId')
來定位它。在靜態生成的 HTML 頁面中,這非常簡單,但是當通過組合許多組件的輸出來動態創建頁面時,很難確保 ID 在所有組件中都是唯一的。Blazor 使用 @ref
元素標記和 ElementReference
結構解決了這個問題。
@ref 和元素引用
當我們需要對 HTML 元素的引用時,我們應該使用 @ref
裝飾該元素(或 Blazor 組件)。我們通過創建一個類型為 ElementReference
的成員并使用 @ref
屬性在元素上識別它來識別我們組件中的哪個成員將持有對 HTML 元素的引用。
@page?"/"<h1?@ref=MyElementReference>Hello,?world!</h1>
Welcome?to?your?new?app.@code?{ElementReference?MyElementReference;
}
第 3 行
定義一個 HTML 元素并使用
@ref
指定在引用該元素時我們將使用組件中的哪個成員 (MyElementReference
)。第 7 行
引用用
@ref
裝飾的元素時將使用的成員。
如果我們更改新 Blazor 應用程序的 Index.razor 文件以添加對 h1
元素的元素引用并運行應用程序,我們將看到類似于以下生成的 HTML 的內容。
<h1?_bl_bc0f34fa-16bd-4687-a8eb-9e3838b5170d="">Hello,?world!</h1>
添加此特殊格式的屬性是 Blazor 如何唯一標識元素而無需劫持元素的 id
參數。我們現在將使用 @ref``、ElementReference
和 JavaScript 互操作來解決一個常見問題。
案例:元素自動聚焦
HTML 規范有一個 autofocus
屬性,可以應用于任何可聚焦的元素;當一個頁面被加載時,瀏覽器會找到第一個用 autofocus
裝飾的元素并給它焦點。由于 Blazor 應用程序不會真正導航(HTML 被簡單地重寫并且瀏覽器 URL 更改),當我們導航到新 URL 并向用戶呈現新內容時,瀏覽器不會掃描 autofocus
屬性。這意味著將 autofocus
屬性放在輸入上不起作用。這是我們將使用 JavaScript Interop、@ref
和 ElementReference
解決的問題。
觀察自動聚焦問題
首先創建一個新的 Blazor 應用程序。
在每個頁面中,用每個
@page
指令下方的相同標記替換內容。
Enter?your?name:?<input?autofocus?/>
運行應用程序并觀察 <input>
元素如何不會自動獲得焦點,甚至在第一頁加載時也不會。
解決自動聚焦問題
在
wwwroot
文件夾中創建一個腳本文件夾。在該文件夾中創建一個名為
AutoFocus.js
的新文件并輸入以下腳本。
var?BlazorUniversity?=?BlazorUniversity?||?{};
BlazorUniversity.setFocus?=?function?(element)?{element.focus();
};
確保在 /Pages/_Host.cshtml(服務器端 Blazor 應用程序)或 /wwwroot/index.html(WebAssembly Blazor 應用程序)中添加對此腳本的引用。
在 Index.razor 頁面中更改標記如下:
@page?"/"
@inject?IJSRuntime?JSRuntime
Enter?your?name
<input?@ref=ReferenceToInputControl?/>@code
{ElementReference?ReferenceToInputControl;protected?override?async?Task?OnAfterRenderAsync(bool?firstRender){if?(firstRender)await?JSRuntime.InvokeVoidAsync("BlazorUniversity.setFocus",?ReferenceToInputControl);}
}
第 4 行
使用
@ref
裝飾器為輸入提供一個在組件內唯一的標識。第 8 行
這是將持有元素標識的成員,該成員必須是
ElementReference
類型。第 12 行
如果這是該組件第一次渲染,則元素引用將傳遞給我們的 JavaScript,它為元素提供焦點。
現在,在頁面之間切換應該會導致第一頁上的輸入在呈現特定頁面時獲得焦點。
組件化我們的自動聚焦解決方案
添加 JavaScript 以在每個頁面上設置焦點并不需要太多工作,但它是重復的。此外,根據顯示的選項卡將自動對焦設置為選項卡控件中的第一個控件將需要更多工作。這是我們應該以可重用的形式編寫的那種東西。
首先,更改我們其中一個頁面的標記,使其使用新的 AutoFocus
控件。
@page?"/"
Enter?your?name
<input?@ref=ReferenceToInputControl?/>
<AutoFocus?Control=ReferenceToInputControl/>@code?{ElementReference?ReferenceToInputControl;
}
在 /Shared 文件夾中創建一個名為 Autofocus.razor 的新組件并輸入以下標記。
@inject?IJSRuntime?JSRuntime
@code?{[Parameter]public?ElementReference?Control?{?get;?set;?}protected?override?async?Task?OnAfterRenderAsync(bool?firstRender){if?(firstRender)await?JSRuntime.InvokeVoidAsync("BlazorUniversity.setFocus",?Control);}
}
第 4 行
為組件定義一個參數
Control
,該參數接受一個ElementReference
來標識哪個控件應該獲得焦點。第 9 行
執行我們的 JavaScript 以將焦點設置到指定的控件。
這個解決方案的問題在于,組件參數的值是在渲染樹構建過程中傳遞的,而元素引用在構建渲染樹并且結果已經在瀏覽器中渲染為 HTML 之后才有效。此解決方案導致錯誤 element.focus is not a function
,因為 ElementReference
在其值被傳遞給我們的 AutoFocus
組件時無效。
注意:不要過早使用元素引用!
正如我們在渲染樹[3]部分中看到的,在其渲染階段,Blazor 根本不會更新瀏覽器 DOM。只有在所有組件的渲染完成后,Blazor 才會比較新的和以前的渲染樹,然后用盡可能少的更改更新 DOM。
這意味著在構建渲染樹時,使用 @ref
引用的元素可能還不存在于瀏覽器 DOM 中——因此任何通過 JavaScript 與它們交互的嘗試都將失敗。因此,我們不應該嘗試在除 OnAfterRender
或 OnAfterRenderAsync
之外的任何組件生命周期方法中使用 ElementReference
的實例,并且由于組件的參數是在構建渲染樹期間設置的,我們不能將 ElementReference
作為參數傳遞,因為它是在組件的生命周期中為時過早。當然,從用戶事件(例如按鈕單擊)訪問引用是可以接受的,因為該頁面已經生成為 HTML。
事實上,直到調用 OnAfterRender*
方法之前,甚至不會設置 ElementReference
的實例。Blazor 流程如下:
為頁面生成虛擬渲染樹。
將更改應用到瀏覽器的 HTML DOM。
對于每個
@ref
修飾元素,更新 Blazor 組件中的ElementReference
成員。執行
OnAfterRender*
生命周期方法。
我們可以通過更改標準 Blazor 應用程序的 Index.razor 組件來證明這個過程,在組件生命周期的各個點將 ElementReference
序列化為字符串,并將序列化的文本呈現到屏幕上。將新項目中的 Index.razor 更改為以下標記并運行應用程序。
@page?"/"<h1?@ref=MyElementReference>Hello,?world!</h1>
<button?@onclick=ButtonClicked>Show?serialized?reference</button><code><pre>@Log</pre></code>Welcome?to?your?new?app.@code?{string?Log;ElementReference?MyElementReference;protected?override?void?OnInitialized(){Log?+=?"OnInitialized:?";ShowSerializedReference();}protected?override?void?OnAfterRender(bool?firstRender){Log?+=?"OnAfterRender:?";ShowSerializedReference();}private?void?ButtonClicked(){Log?+=?"Button?clicked:?";ShowSerializedReference();}private?void?ShowSerializedReference(){Log?+=?System.Text.Json.JsonSerializer.Serialize(MyElementReference)?+?"\r\n";}
}
我們的組件實例已創建。執行
OnInitialized
(第 15 行)。MyElementReference
的值被序列化為我們的Log
字符串(第 33 行)。生成渲染樹。
瀏覽器的 DOM 已更新
Blazor 檢查使用
@ref
修飾的元素并更新它們標識的ElementReference
。OnAfterRender
在我們的組件上執行(第 21 行)。MyElementReference
的值被序列化為我們的Log
字符串,但不顯示 - 我們必須調用StateHasChanged
才能看到它,但Log
的值已經更新。用戶單擊按鈕。
MyElementReference
的值被序列化為我們的Log
字符串。Blazor 執行
StateHasChanged
以響應按鈕單擊。我們在屏幕上看到更新的
Log
以顯示從第 7 步和第 9 步添加的值——這兩個都顯示了一個非空標識符。
完成 AutoFocus ?組件
我們可以傳入一個 Func<ElementReference>
,而不是傳入 ElementReference
本身,我們的 AutoFocus
組件然后可以在其 OnAfterRender*
生命周期方法中執行此 Func
——此時返回的值將是有效的。
將 AutoFocus
控件更改為接受 Func
,并確保設置的值不為空。
@inject?IJSRuntime?JSRuntime
@code?{[Parameter]public?Func<ElementReference>?GetControl?{?get;?set;?}protected?override?async?Task?OnAfterRenderAsync(bool?firstRender){if?(GetControl?is?null)throw?new?ArgumentNullException(nameof(GetControl));if?(firstRender)await?JSRuntime.InvokeVoidAsync("BlazorUniversity.setFocus",?GetControl());}
}
該組件現在可以按如下方式使用:
@page?"/"
Enter?your?name
<input?@ref=ReferenceToInputControl?/>
<AutoFocus?GetControl=@(?()?=>?ReferenceToInputControl)/>@code?{ElementReference?ReferenceToInputControl;
}
注意:未來的 Blazor 計劃自動創建 ElementReference
成員。
參考資料
[1]
源代碼: https://github.com/mrpmorris/blazor-university/tree/master/src/JavaScriptInterop/HtmlElementReferences