よくある svg の symbol と use の組み合わせでアイコン表示をしてみる
とある Blazor アプリケーションにて、svg の symbol 定義と、その symbol 定義を参照してアイコン表示する実装をすることになりました。
具体的には、まず Razor コンポーネント内に下記のように svg の symbol 定義をマークアップします。(ちなみに下記例は、Google Fonts の Material Symbols から入手した、太陽 (🌞"sun")、月 (🌙"moon")、星 (⭐"star") のアイコンを、それぞれ <symbol id="...
で symbol 定義したものの抜粋です。)
<svg xmlns="http://www.w3.org/2000/svg" display="none">
<defs>
<symbol id="sun" viewBox="0,0,48,48">
<path d="M24.05 46.55..." />
</symbol>
<symbol id="moon" viewBox="0,0,48,48">
<path d="M24 42q-7.5..." />
</symbol>
<symbol id="star" viewBox="0,0,48,48">
<path d="m16.15 37.75..." />
</symbol>
</defs>
</svg>
上記 svg 要素は、あとでこのコンポーネント内に描画する svg 要素からここで定義した図形を参照して表示するための "定義集" です。ですから、上記 svg 要素は表示上は見えては困ります。そこで、display="none"
の属性を指定して非表示としてあります。
あとは、表示用の svg 要素で use 要素を使い、上記 symbol 定義の id 属性値を指すセレクタで表示したい図形を指定することで、目的の図形がページ上に表示するようレンダリングします。例えば下記は、id 属性値が "sun" である symbol 定義の図形をページ上に表示する例です。
...
<svg xmlns="http://www.w3.org/2000/svg">
<use xlink:href="#sun" />
</svg>
...
ここまでは、ごく普通に使われているであろう、svg の symbol および use を組み合わせた図形表示の定番実装に過ぎません。
動的にこれらを表示できるようにする
これは静的な HTML を編集しているのではなく、Blazor ですから、これら svg によるアイコン表示を、動的に表示できるように実装してみます。具体的には、太陽、月、星のいずれかの図形を指定してページに追加できる3つのボタンを実装します。下図が実現イメージです。
作るものが決まりましたので、実際に実装していきます。
まずは、太陽・月・星の各図形を示す enum
定数 = SymbolType
と、それを格納する List<T>
フィールド変数を、Razor コンポーネントに実装します。SymbolType
enum 定数の名前は、冒頭でマークアップした、svg の symbol 定義の id 属性値に合せます (C# の命名規則に寄せたので、英字の大小だけ不一致ですが)。
...
@code {
// 👇 Symbol 定義の id 属性値を示す enum 定数を定義
private enum SymbolType
{
Sun,
Moon,
Star
}
// 👇 上記 Symbol enum 定数を格納する List<T> フィールド変数を定義
private readonly List<SymbolType> _Symbols = new();
}
この Razor コンポーネントの HTML マークアップ部分で、上記 _Symbol
フィールド変数に格納された SymbolType
の要素を @foreach(...)
ループで列挙して svg 要素としてレンダリングするようにします。なお、前述のとおり、SymbolType
enum 定数の名前で Symbol 定義の id 属性値を示すようにしていますが、英字の大小だけ不一致なので、SymbolType
enum 定数の名前を小文字化する処理を忘れないようにします。
...
<p>
<!-- 👇 フィールド変数 _Symbols に格納された SymbolType enum 定数を列挙 -->
@foreach (var symbol in _Symbols)
{
<svg xmlns="http://www.w3.org/2000/svg">
<!-- 👇 列挙された SymbolType enum 定数名を小文字化して、それで id セレクタ指定 -->
<use xlink:href="#@(symbol.ToString().ToLower())" />
</svg>
}
</p>
@code {
...
続けて、引数に指定した図形 (SymbolType
) を、_Symbols
List フィールド変数に要素追加するメソッドを実装します。
...
@code {
...
// 👇引数に指定された図形を _Symbols リストに追加
private void AddSymbol(SymbolType symbol)
{
_Symbols.Add(symbol);
}
}
最後に、太陽・月・星の各図形を追加する 3 つのボタンを設け、そのクリックイベントで上記で用意した図形追加用のメソッド AddSymbol(...)
を呼び出すようにします。
...
<!-- 👇 太陽・月・星の各図形を追加する 3 つのボタンを設置、クリックされたら AddSymbol を呼び出す -->
<p>
<button @onclick="@(() => AddSymbol(SymbolType.Sun))">Add Sun</button>
<button @onclick="@(() => AddSymbol(SymbolType.Moon))">Add Moon</button>
<button @onclick="@(() => AddSymbol(SymbolType.Star))">Add Star</button>
</p>
<p>
@foreach (var symbol in _Symbols)
{
...
これで完成です!
早速実行してみるも、なぜか図形が表示されない!?
早速実行してみると...
あれれ、たしかに svg 要素は追加されているようですが、肝心の svg 図形が表示されません。何を間違ったのでしょうか。
問題の切り分けのために、xlink:href
に指定している Symbol 定義の id セレクタ指定を、即値に一時変更してみました。
...
@foreach (var symbol in _Symbols)
{
<svg xmlns="http://www.w3.org/2000/svg">
<!-- 👇 xlink:href に指定する id セレクタ指定を即値にしてみる -->
<use xlink:href="#sun" />
...
すると、どうやらちゃんと図形表示されるようです。即値にしましたので、どのボタンをクリックしても、太陽 = "#sun" の図形しか表示はされませんが、とにかくそれ以外の実装には間違いはなさそうです。
では、SymbolType
enum 定数の名前を文字列化したり小文字化したりする処理にミスがあったのでしょうか。引き続き、xlink:href
指定を即値で指定しますが、ただし、C# の文字列即値を Razor 構文で xlink:href
属性に指定するようにしてみます。
...
@foreach (var symbol in _Symbols)
{
<svg xmlns="http://www.w3.org/2000/svg">
<!-- 👇 Razor 構文で C# の文字列即値で指定してみる -->
<use xlink:href="@("#sun")" />
...
すると再び図形が表示されなくなってしまいました...。
どうも、Razor 構文で動的に xlink:href
を指定するだけで、このようにうまく機能しなくなるようです。これは Blazor 側の問題なのか、svg use 要素の xlink:href
の仕様の都合なのか、いずれにせよいったいどうしたものでしょうか...。
xlink:href
は廃止予定!?
ネット上に何か同じ事で困っている事例がないか、検索していたところ、以下の検索結果が目に留まりました。
2016年ということで、既に6年も前の投稿ですが... xlink:href
は廃止予定だったんですね!
大変不勉強なもので、このことを知っていませんでした...。
実はこの技法、React 向けの某有名 OSS の実装からパクったで学んだのですが、そちらも歴史が古いせいか、最新コミットでも xlink:href
で図形指定していたんで気づかなかったです (所詮言訳ですが)。
改めて MDN 上の記載を確認しても (下記リンク先)、しっかり "Deprecated: This feature is no longer recommended"、すなわち「非推奨: この機能は推奨されなくなりました」と明記されていました。
さておき、上記リンク先の MDN の記載を読むと、xlink:href
の代替としては単純に、「 href を使え」 ということだそうです。そこで、先の Blazor アプリでの実装も、xlink:href
から href
に書き換えてみました。
...
@foreach (var symbol in _Symbols)
{
<svg xmlns="http://www.w3.org/2000/svg">
<!-- 👇 xlink:href ではなく、href を使う -->
<use href="#@(symbol.ToString().ToLower())" />
...
すると...
ちゃんと期待どおりに動作するようになりました!
まとめ
Blazor のコンポーネント内で、svg の use 要素を使って、別途定義された Symbol を参照して図形を表示する際、いずれの Symbol を参照するのかのセレクタ指定に xlink:href
を使っていると、図形が表示されない場合があることがわかりました。
そしてそもそも xlink:href
の使用は少なくとも、自分がわかった範囲でも 2016 年時点で既に非推奨であったようです (stackoverflow にそのような投稿があったのを見つけた、というだけで、裏付け取ってませんが)。
代わりに href
属性でセレクタ指定することで、Blazor コンポーネント内での使用でも、問題なく図形表示されることが確認できました。
今回は、歴史の古い、あるいは、古いブラウザに対応するためのフォールバック指定がされている OSS の実装を手本としていたことが敗因のひとつかと考えています。ちゃんと MDN やその他公式の資料に目を通す習慣がないといけないですね。
Learn, Practice, Share :)