🐥note.

小鳥とMicrosoft <3 なエンジニアの技術Blog📚

ElementReferenceと@refを使用しBlazorからJavaScript側へHTML要素の参照を渡す話

BlazorでJavaScriptのライブラリをラップしたい場合なんかに有用なRazorのElementReference@refの小話です。

目次です

はじめに

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要素の参照をいい感じにハンドリングしたいですよね。そういった場合@refElementReferenceを使用すれば良いみたいです。

@refとElementReferenceとは?

docs.microsoft.com

こちらでチラッと解説されていますね。

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相互運用する

@refElementReferenceを使用し冒頭に掲載したChart.jsの線グラフ描画してみます。

Blazor Componentの編集

画面を描画したタイミングでJavaScriptShowChart関数を実行しています。
引数には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.htmlJavaScriptのコードを追加します。
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>

実行結果

ドジャ~ン

f:id:piyo_esq:20200220103000p:plain
BlazorでChart.jsを動かした図

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.htmlJavaScriptは受け取った文字列を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での検証用のサンプルコードを載せます。

実行結果

以下は起動時の状態です。

f:id:piyo_esq:20200220103055p:plain
起動した状態

Pageロード時はElementReferenceのIdは空っぽです。

それぞれボタンを押下してみます。

ボタン押下後

Component自身が持つElementReferenceのIdのみ表示されました。
IdはOnAfterRender以降のライフサイクルから保持していることが分かります。

一方、親Componentが持つElementReference[Parameter]として受け取った子ComponentはOnAfterRenderでもIdを持てていないことが分かります。子Componentの初期化は親ComponentのOnInitializedのタイミングで行われます。前途の通りOnAfterRenderまでElementReferenceによるHTML参照は持てないのでこういった結果となります。  

おわり

BlazorでHTML要素を扱う必要があるJavaScriptライブラリのラッパーを作る場合には必須要素なんじゃないでしょうか?