4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

BlazorAdvent Calendar 2022

Day 22

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

Last updated at Posted at 2022-12-22

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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 型の変数に格納されます。

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

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

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

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

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

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

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

@code {
  ...

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

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 (文字列) が引数に指定されてきましたが (下記)...

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

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

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

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!

4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?