Blazor コンポーネント内の DOM 要素への参照を JavaScript 関数に渡す

## 前回の実装では 2 つ以上の Handsontable グリッドを同一ページ内に並べられなかった

先日、Blazor アドベントカレンダーに以下の投稿がありました。

https://qiita.com/yaju/items/bcca337af393f15a6795

Blazor が備える JavaScript 相互運用機能を使って、Excel ライクなグリッド JavaScript ライブラリ「Handsontable」を Blazor から使ってみる、というテーマでしたね。

これを少し改良しまして、Handsontable のオブジェクトをグローバルスコープの変数に置くことなく使えるようにした記事を投稿しました (下記)。

https://qiita.com/jsakamoto/items/654f8fbde25c44e54eb9

さて上記の投稿にて自分は以下のように記しました。

> **1ページ内に複数の Handsontable グリッドを並べて使うこともできるようになりました**

ということで、1 ページ内に複数の Handsontable グリッドを並べるのを実際にやってみたところ、このままではうまくいかないことに気づきました。

Handsontable グリッドを構築する JavaScript 関数 `createGrid` 内で、Handsontable グリッドを構築する対象の要素指定が **id = "grid" の要素固定**になってたんですね (下記)。

```js:script.js
function createGrid() {
  ...
  // 👇 id = "grid" の要素にしか Handsontable グリッドを構築できない!
  let grid = document.getElementById("grid");

  return new Handsontable(grid, {
    ...
  });
}
```

幸い、Blazor から JavaScript 関数を呼び出す際には、引数を渡せます。ですので「Handsontable グリッドを構築する対象の要素の id」を `createGrid` JavaScript 関数に渡すようにしてみましょう。まず、`createGrid` JavaScript 関数を以下のように変更してみます。

```js:script.js
// 👇 関数の引数に、Handsontable グリッドを構築する対象の要素の id を受け取るようにして...
function createGrid(elementId) {
  ...
  // 👇 引数で指定された id を持つ要素に Handsontable グリッドを構築する!
  let grid = document.getElementById(elementId);

  return new Handsontable(grid, {
    ...
  });
}
```

あとはこれを利用する Blazor 側もあわせて変更します (下記)。

```csharp:Index.razor
<!-- Handsontable グリッドを2つ並べるので、id を違えて構築先 div 要素も2つ配置 -->
<div id="grid1"></div>

<div id="grid2"></div>

@code {
  // Handsontable オブジェクトへの参照を持つフィールド変数も2つ用意
  private IJSObjectReference? _grid1;

  private IJSObjectReference? _grid2;

  protected override async Task OnAfterRenderAsync(bool firstRender)
  {
    ...
    // 引数にグリッドを構築する対象の要素の id を渡す!                         👇ここ!
    _grid1 = await jsRuntime.InvokeAsync<IJSObjectReference>("createGrid", "grid1");
    _grid2 = await jsRuntime.InvokeAsync<IJSObjectReference>("createGrid", "grid2");
    ...
```
これで本当に、Handsontable グリッドをページ内で複数配置することができるようになりました。

## 要素の id を指定する以外に方法はないのか?

ところで、要素の id でグリッドを構築する先の要素を指定するのは一抹の不安が残ります。というのは、**要素の id の衝突の危険性**です。

まぁ、今回紹介しているような Handsontable グリッドをページ内に貼り付けるシナリオではそう心配したものではないでしょう。けれども、すごく込み入ったアプリケーションの場合など、万が一 id が重複していると**予期しない要素上に Handsontable グリッドが構築**されてしまいます。実際には重複ないよう id を割り振るでしょうから、そういった事故はそうそう起きるとは思えませんものの、id が衝突しないように気を払わないとならないコストは発生します。

さらには、Handsontable グリッドをラップして再利用可能な Razor コンポーネントにまとめようとした場合、その Razor コンポーネントにも "Id" パラメータープロパティを設け、引き続き重複に気を遣いながら id を割り当てるしかないのでしょうか。

## `ElementReference` 型および `@ref` 構文が使える!

実は Blazor には、コンポーネント内にレンダリングした HTML 要素 (DOM 要素) への "参照" を示す、**`ElementReference` 型**というのが存在します。Blazor コンポーネントがそれ自身内で直にレンダリングする DOM 要素への参照を、この `ElementReference` 型の値として保持できるのです。使い方としては、コンポーネント内でレンダリングする DOM 要素のマークアップに、**`@ref` キーワード**を使って `ElementReference` 型の変数を指定することで、その DOM 要素への参照が `ElementReference` 型の変数に格納されます。

そして `IJSRuntime` や `IJSObjectReference` の Invoke 系メソッドの引数として `ElementReference` 型の値を渡すことで、そうやって Blazor から呼び出される JavaScript 関数あるいはメソッドに、その参照している DOM 要素を渡すことができます。

この方式を使って、要素の id 指定不要で Handsontable グリッドを構築するようにやってみましょう。

まずは DOM 要素への参照を収めておく `ElementReference` 型のフィールド変数を用意します。今回は 2 つの Handsontable グリッドを同一コンポーネント内に並べてみますので、2つの  `ElementReference` 型のフィールド変数をコードブロックに書き足します。

```csharp:Index.razor
...
@code {
  // 以下の 2 つの ElementReference 型フィールド変数を追加する
  private ElementReference _div1Ref;
  private ElementReference _div2Ref;
 ...
```

そしてちょっと前後しますが、Handsontable グリッドを構築する対象の DOM 要素 (`div` タグ) に、`@ref` キーワードを使って、上記で用意した ElementReference 型フィールド変数に各々その参照を格納するようにします。この変更が仕上がると要素の id は不要になりますので、この時点で id 属性は削除しておきます。

```html:Index.razor
<!-- id 属性は削除し、代わりに @ref キーワードで 
     ElementReference 型フィールド変数に自身の参照を収めるようにする-->
<div @ref="_div1Ref"></div>

<div @ref="_div2Ref"></div>

@code {
  ...
```
これで、各 div 要素への参照が、それぞれ `_div1Ref`, `_div2Ref` のフィールド変数に格納されるようになりますので、(要素の id に替わって) これら DOM 要素への参照を `createGrid` JavaScript 関数の引数に渡すようにします。

```csharp:Index.razor
...
@code {
  protected override async Task OnAfterRenderAsync(bool firstRender) {
    ...
    // 要素の id を示す文字列ではなくて、ElementReference 型で示される DOM 要素への参照を渡す!
    //                                                                    👇ここ!
    _grid1 = await jsRuntime.InvokeAsync<IJSObjectReference>("createGrid", _div1Ref);
    _grid2 = await jsRuntime.InvokeAsync<IJSObjectReference>("createGrid", _div2Ref);
    ...
```

次は JavaScript 側の変更です。これまでは `createGrid` JavaScript 関数には、Handsontable グリッドを構築する対象の要素の id (文字列) が引数に指定されてきましたが (下記)...

```js:scritp.js
//                  👇 引数には、要素の id を示す文字列が渡されるので...
function createGrid(elementId) {
  ...
  // そのような id を持つ要素を取得してから...
  let grid = document.getElementById(elementId);

  // その取得した要素にグリッドを構築!
  return new Handsontable(grid, {...});
}
```

今や、グリッドを構築する対象の **DOM 要素の参照そのもの**が、**Blazor 側から `createGrid` JavaScript 関数の引数に渡されます**から、その引数に渡された DOM 要素への参照に対して直接、グリッドを構築すればよくなります (下記)。

```js:scritp.js
//                  👇 引数に要素の参照が直接渡されるようになった!
function createGrid(element) {
  ...
  // なので、引数に指定された DOM 要素参照を対象に、直接グリッドを構築!
  return new Handsontable(element, {...});
}
```

以上で、要素に割り振る id 値を検討する必要なく、マークアップする DOM 要素に直接結びつく変数を介して、JavaScript に要素への参照そのものを直に渡して操作できるようになりました!

## まとめ

コンポーネント内にマークアップする DOM 要素は、`@ref` キーワードを使って、`ElementReference` 型の変数にその DOM 要素への参照を収めることができます。および、`ElementReference` 型の値は、Blazor の JavaScript 相互運用機能を使って、JavaScript の関数やメソッドにそのまま DOM 要素への参照として引き渡すことができます。

このように、要素の id を介することなく直接に DOM 要素への参照を取得して使用する方法があると、複数インスタンスを扱う場合に id 衝突の心配や id の割り振り・命名に心をすり減らす必要がなくなり、心の平穏を保てますね!

さらには今回の投稿ではそこまで踏み込みませんでしたが、このようにコンポーネントがレンダリングする DOM 要素と直接に関わる機能を、再利用可能な Razor コンポーネントとして切りだそうとした場合に、そのカプセル化の文脈において、この DOM 参照の取得と引き渡しの技法のありがたみに気づかされるはずです。

_Happy Coding!_