こんにちは。フロントエンドエンジニアの松岡です。
Angular でアイコン表示コンポーネントを作る方法を紹介します。
- 本記事のソースコードは Angular v15 を前提としています。
- コンポーネントメタデータの
selector
のプレフィクスを省略したりstyles
を直接記述するなど、ガイドライン準拠より説明の簡単さを優先していますのでご留意ください。
TL;DR
src/app/icon.component.ts(アイコン表示コンポーネント)
import { Component, Input } from '@angular/core';
@Component({
standalone: true,
selector: 'icon',
template: `
<svg width="0" height="0" fill="currentColor">
<use
attr.href="assets/icon.svg#{{ icon }}"
attr.xllink:href="assets/icon.svg#{{ icon }}"
></use>
</svg>`,
styles: [`
svg {
width: 100%;
height: 100%;
}
`],
})
export class IconComponent {
@Input() icon!: string;
}
asset/icon.svg(アイコンの定義)
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
>
<symbol id="bell">
<svg><path ... /></svg>
</symbol>
<symbol id="bookmark">
<svg><path ... /></svg>
</symbol>
</svg>
src/app/page1.component.ts(アイコン表示コンポーネントの利用者)
@Component({
standalone: true,
selector: 'page1',
imports: [
IconComponent,
],
template: `
<icon [icon]="'bell'"></icon>
<icon [icon]="'bookmark'"></icon>
`
})
export default class Page1Component { ... }
アイコン表示コンポーネントとは
記事タイトルにある「アイコン表示コンポーネント」とは アイコンをレンダリングするだけのコンポーネント を指しています。テキストの強調や補足のためにアイコンを使うことってありますよね。インラインで SVG を記述する方法もあれば、個別にコンポーネントを宣言していろいろなページで再利用する方法もあります。
インライン SVG はテンプレートの行数が増えるのと「これって何のアイコンだっけ?」と迷うことが多く、私は個別にコンポーネント宣言するほうを好んで使っています。
増殖するアイコン表示コンポーネント
まず、個別のコンポーネント宣言のソースコードを紹介します。
@Component({
standalone: true,
selector: 'bell-icon',
template: `<svg>...</svg>`,
...
})
export class BellIconComponent { ... }
このやり方だと、違うパターンのアイコンが欲しくなるたびにコンポーネントが増えていきます。
src/app/icons/
├── bell-icon.component.ts
├── bookmark-icon.component.ts
├── calculator-icon.component.ts
├── calendar-icon.component.ts
├── cloud-icon.component.ts
├── cube-icon.component.ts
├── document-icon.component.ts
アイコンが増えるのに比例してインポートも増えていきます。
@Component({
standalone: true,
selector: 'page1',
imports: [
BellIconComponent,
BookmarkIconComponent,
CalculatorIconComponent,
],
...
})
export default class Page1Component { ... }
この数が膨大になると パフォーマンスにそこそこ影響 しそうです。また aria-hidden
属性の追加など「アイコン表示コンポーネント」共通の振る舞いを持たせたい時、増殖した すべてのコンポーネントを変更しないといけない のも厄介でした。
<use>
と <symbol>
でアイコンが埋め込みできる
HTML の <use>
と <symbol>
を使えば、アイコン表示コンポーネントはひとつに集約できます。Google Chrome では <use>
は 2012 年、<symbol>
は 2008 年に実装されていますが、つい最近まで知りませんでした。
MDN
<use>
https://developer.mozilla.org/ja/docs/Web/SVG/Element/use
MDN
<symbol>
https://developer.mozilla.org/en-US/docs/Web/SVG/Element/symbol
以降はこれらのタグを使ってひとつに集約した「アイコン表示コンポーネント」について説明します。
ひとつに集約したアイコン表示コンポーネント
<use>
タグは外部にある SVG のノードを参照します。これを使って外部からノードを指定できるようにします。
@Component({
selector: 'icon',
template: `
<svg width="0" height="0" fill="currentColor">
<use
attr.href="assets/icon.svg#{{ icon }}"
attr.xllink:href="assets/icon.svg#{{ icon }}"
></use>
</svg>`,
...
})
export class IconComponent {
@Input() icon!: string;
}
width
とheight
はそれぞれ初期値に 0 を与えて、CSS ファイルの読み込み前にアイコンが画面いっぱいに広がって表示されるのを防ぎます。attr.href
とattr.xllink
に同じパスを指定しているのは、attr.xllink
しかサポートしていないブラウザの互換性を考慮したものです。
アイコン定義
<symbol>
でアイコンを列挙し id
にアイコンを識別するためのテキストを与えます。
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
>
<symbol id="bell">
<svg><path ... /></svg>
</symbol>
<symbol id="bookmark">
<svg><path ... /></svg>
</symbol>
</svg>
アイコン表示コンポーネントを利用する
利用側で、どのアイコンを使うか指定します。このテキストは <symbol>
に与えた id
と同じものです。
@Component({
selector: 'page1',
imports: [
IconComponent,
],
template: `
<icon [icon]="'bell'"></icon>
<icon [icon]="'bookmark'"></icon>
`,
...
})
export default class Page1Component { ... }
集約すると何が嬉しいのか
プロジェクトのファイル構造がすっきりシンプルになります。
/src
├── app
│ ├── app.component.ts
│ ├── icon.component.ts
│ └── page1.component.ts
└── assets
│ └── icon.svg
アイコンを増やす時は icon.svg
に新しい <symbol>
を定義します。VS Code では SVG Preview などエクステンションを使えば <symbol>
ごとのプレビュー画像が確認できます。
サイズ指定など共通の振る舞いを持たせたい時、1 ファイルを変更するだけで済みます。
ブラウザでの挙動
npm run ng serve
アイコンは Angular のアセットとして配置しているため、モジュールを読み込むたびにアイコン解決の HTTP リクエストが走ります。モジュールを遅延ロードをしている場合はルーティングの loadChildren
や loadComponent
が評価されるタイミングと同等です。
export const appRoutes: Route[] = [
{
path: 'page1',
loadComponent: () => import('./page1.component'),
},
{
path: 'page2',
loadComponent: () => import('./page2.component'),
},
];
/page1
を開いたタイミングでGET icon.svg
がリクエストされる/page2
を開いたタイミングでGET icon.svg
がリクエストされる/page1
を再び開いたタイミングでGET icon.svg
がリクエストされる
https://heroicons.com/ からアイコンを 10 パターンほどコピーして icon.svg
に貼り付けたところ、レスポンスのサイズは 237 バイトでした。SVG はテキストのためレスポンスは軽量です。
ソースコードの多い複雑なアイコンや膨大な数のアイコンを使うようなケースでは、icon.svg
のファイル分割など検討したほうが良いかもしれません。
ビルド
npm run ng build
アイコン表示コンポーネントをひとつに集約したケース(今回紹介した方法)と、個々にコンポーネント宣言したケース(旧来の方法)を比較してみました。どちらも同じ 10 パターンのアイコンを使い /page1
と /page2
それぞれで 5 パターンずつアイコンを読み込むようにしました。
export const appRoutes: Route[] = [
{
path: 'page1',
loadComponent: () => import('./page1.component'),
},
{
path: 'page2',
loadComponent: () => import('./page2.component'),
},
];
ビルドファイルが吐き出される dist
配下のファイルサイズは以下のとおりです。★マークを付けたファイルに大きな差が出ました。
ファイル | サイズ(ひとつに集約) | サイズ(個々にコンポーネント宣言) |
---|---|---|
合計 | 288 kB | 292 kB |
3rdpartylicenses.txt | 13 kB | 13 kB |
{chunk}.{hash}.js ★ | 770 bytes | 4.1 kB |
{chunk}.{hash}.js ★ | 780 bytes | 4.3 kB |
assets ★ | 96 bytes | 0 |
common.{hash}.js ★ | 610 bytes | 0 |
favicon.ico | 15 kB | 15 kB |
index.html | 579 bytes | 582 bytes |
main.{hash}.js | 200 kB | 199 kB |
polyfills.{hash}.js | 34 kB | 34 kB |
runtime.{hash}.js | 2.7 kB | 2.7 kB |
styles.{hash}.css | 0 | 0 |
ひとつに集約したケースは ランタイムの読み込みに負荷 がかかり、個々にコンポーネント宣言したケースは 初回のモジュール読み込みに負荷 がかかる、ということが読み取れます。どちらもメリット・デメリットがありそうですが、ファイル構造のシンプルさで言えば「ひとつに集約」のほうが扱いやすいのではと思っています。
おわりに
ブラウザとビルドの検証に使ったソースコードは GitHub で公開しています。
https://github.com/ringtail003/angular-icon
よければ是非、ひとつに集約したアイコン表示コンポーネントを実装してみてください。増殖したコンポーネントに困っているどなたかの参考になれば幸いです 😊