BlazorでJavaScriptのライブラリをラップしたい場合なんかに有用なRazorのElementReference
と@ref
の小話です。
目次です
- はじめに
- @refとElementReferenceとは?
- Chart.jsを@refとElementReferenceでいい感じにJS相互運用する
- ChartのデータをBlazor側から与える
- ElementReferenceの注意点
- おわり
はじめに
JavaScriptのライブラリを使用する際、HTML要素を引数として与える関数があります。
例えばChart.js
というグラフを描画できるライブラリを使用する場合、下記ctx
の部分です。
var ctx = document.getElementById('myChart').getContext('2d'); var chart = new Chart(ctx, { // The type of chart we want to create type: 'line', // The data for our dataset data: { labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'], datasets: [{ label: 'My First dataset', backgroundColor: 'rgb(255, 99, 132)', borderColor: 'rgb(255, 99, 132)', data: [0, 10, 5, 2, 20, 30, 45] }] }, // Configuration options go here options: {} });
BlazorでJavaScriptを相互運用する場合、HTML要素の参照をいい感じにハンドリングしたいですよね。そういった場合@ref
とElementReference
を使用すれば良いみたいです。
@refとElementReferenceとは?
こちらでチラッと解説されていますね。
ElementReference
はRazor内に記載したHTML要素への参照を意味する型で、左記参照を得るためにHTML要素に定義する属性が@ref
のようです。
以下にMicrosoft Docsのサンプルコードを転載します。
Blazor側コード
@inject IJSRuntime JSRuntime <input @ref="_username" /> <button @onclick="SetFocus">Set focus on username</button> @code { private ElementReference _username; public async Task SetFocus() { await JSRuntime.InvokeVoidAsync( "exampleJsFunctions.focusElement", _username); } }
JavaScript側コード
window.exampleJsFunctions = { focusElement : function (element) { element.focus(); } }
ボタンを押下するとInput要素の参照をJavaScript側へ渡してInput要素をfocusするというサンプルのようです。
Chart.jsを@refとElementReferenceでいい感じにJS相互運用する
@ref
とElementReference
を使用し冒頭に掲載したChart.js
の線グラフ描画してみます。
Blazor Componentの編集
画面を描画したタイミングでJavaScriptのShowChart
関数を実行しています。
引数にはCanvas要素の参照であるcanvasElementReference
を渡しています。
@inject IJSRuntime JSInterop <canvas @ref="canvasElementReference"/> @code { private ElementReference canvasElementReference; protected override void OnAfterRender(bool firstRender) { JSInterop.InvokeVoidAsync("ShowChart", canvasElementReference); } }
index.htmlの編集
index.html
にJavaScriptのコードを追加します。
ShowChart
関数のelm
引数はBlazor側から受け取ったCanvas
要素への参照を意味しています。
<!DOCTYPE html> <html> <head> <!-- 途中省略 --> + <script src="https://cdn.jsdelivr.net/npm/chart.js@2.8.0"></script> </head> <body> <app>Loading...</app> <!-- 途中省略 --> + <script type="text/javascript"> + window.ShowChart = function (elm) { + var chart = new Chart(elm, { + type: 'line', + data: { + labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'], + datasets: [{ + label: 'My First dataset', + backgroundColor: 'rgb(255, 99, 132)', + borderColor: 'rgb(255, 99, 132)', + data: [0, 10, 5, 2, 20, 30, 45] + }] + }, + }); + } + </script> </body> </html>
実行結果
ドジャ~ン
ChartのデータをBlazor側から与える
JavaScript内のdatasets
やらlabels
やらはBlazorから受け渡したいですよね。
Blazor内で上記設定項目をClass化し、JsonにSerializeして渡すようにしてみましょう。
テキトーにChart.js
のデータ構造をクラス化して...
[Serializable] public class ChartJSDataModel { [JsonPropertyName("labels")] public string[] Labels { get; set; } [JsonPropertyName("datasets")] public DataSetsModel[] DataSets { get; set; } } [Serializable] public class DataSetsModel { [JsonPropertyName("label")] public string Label { get; set; } [JsonPropertyName("backgroundColor")] public string BackgroundColor { get; set; } [JsonPropertyName("borderColor")] public string BorderColor { get; set; } [JsonPropertyName("data")] public int[] Data { get; set; } }
Blazor側でデータを作ってInvokeしてあげます
protected override void OnAfterRender(bool firstRender) { var dataModel = new ChartJSDataModel(); dataModel.Labels = new string[] { "January", "February", "March", "April", "May", "June", "July" }; dataModel.DataSets = new DataSetsModel[] { new DataSetsModel(){ Label = "My First dataset", BackgroundColor = "rgb(255, 99, 132)", BorderColor = "rgb(255, 99, 132)", Data = new int[]{0, 10, 5, 2, 20, 30, 45} } }; JSInterop.InvokeVoidAsync("ShowChart", canvasElementReference, JsonSerializer.Serialize(dataModel)); }
index.html
のJavaScriptは受け取った文字列をJSON.parse
でオブジェクト化してあげれば...
<script type="text/javascript">> window.ShowChart = function (elm, dataSet) { var chart = new Chart(elm, { type: 'line', data: JSON.parse(dataSet), options: {} }); } </script>
ドジャ~ン(画像省略)
rgb(...)
が文字列だったり色々改良すべき点はありますが、動かすだけならサクっとできちゃいますね。
JavaScriptライブラリをBlazor用にラップしたライブラリを作成する場合もきっとこれの応用なのでしょう。
ElementReferenceの注意点
@ref
属性の値からElementReference
型のHTML要素の参照を得るのは実画面が描画される段階です。Blazorのライフサイクルにあてはめて考えるとOnAfterRender
, OnAfterRenderAsync
の段階で初めて参照を持ちます。
つまりSetParameterAsync
, OnInitialized
, OnInitializedAsync
, OnParameterSet
, OnParameterSetAsync
ではElementReference
への参照を使うことはできません。
以下にBlazor WebAssemblyでの検証用のサンプルコードを載せます。
実行結果
以下は起動時の状態です。
Pageロード時はElementReferenceのIdは空っぽです。
それぞれボタンを押下してみます。
Component自身が持つElementReferenceのIdのみ表示されました。
IdはOnAfterRender
以降のライフサイクルから保持していることが分かります。
一方、親Componentが持つElementReference
を[Parameter]
として受け取った子ComponentはOnAfterRender
でもIdを持てていないことが分かります。子Componentの初期化は親ComponentのOnInitialized
のタイミングで行われます。前途の通りOnAfterRender
までElementReference
によるHTML参照は持てないのでこういった結果となります。
おわり
BlazorでHTML要素を扱う必要があるJavaScriptライブラリのラッパーを作る場合には必須要素なんじゃないでしょうか?