最新のAngularを使って、ポケモンのサンドウィッチレシピツールつくたよ

こんにちは、[あかい](https://twitter.com/Ver1000000000)です🐗

本記事は [Angular Advent Calendar 2022](https://qiita.com/advent-calendar/2022/angular) の7日目の記事の代打です🏏⚾

6日目は[@beltway7さん](https://ksakae1216.com/entry/2022/12/06/070000)で8日目は[@KOHETs](https://qiita.com/KOHETs/items/642fb07fe87ca3e9f0cf)さんです！

---

[今作のポケモン](https://www.pokemon.co.jp/ex/sv/ja/)には、[「さまざまな材料をかけあわせてサンドウィッチを作る」](https://www.pokemon.co.jp/ex/sv/ja/features/221006_02/)というコンテンツがあります。

これを行うとき、手探りで作るのがすごく大変だったので、いい感じにレシピを検索できるWebアプリを作りました🥪

https://pokemon-sandwich-recipes.ver1000000.com/

なんと、作り始めてその当日(※1)にリリースすることができ、
今回ひしひしとAngularの**パワー**を実感したので、その所感をお伝えしたいと思います！

(※1 現在、リリース後4人日くらいかけて改善/機能追加を行っちゃってはいますが、
コアのデザインや機能に大きく変更はないはずです)

実際にアプリを動かしながらこの記事を読んで「あ〜ここがこうね〜」みたいなのを感じ取ってもらえればと思います。

# Anguar CDK
今回は「サラッとイイカンジでオリジナル」にUIを構築したかったので、[Angular CDK](https://material.angular.io/cdk)を利用することにしました！

今回利用しているCDKのモジュールは、次の3コです！

- [CdkListboxModule](https://material.angular.io/cdk/listbox/)
- [CdkAccordionModule](https://material.angular.io/cdk/accordion/)
- [ScrollingModule](https://material.angular.io/cdk/scrolling/)

## セレクトボックス - CdkListboxModule
超お手軽に**アクセシビリティに配慮したセレクトボックス**を実現しています。

- [CdkListboxModule](https://material.angular.io/cdk/listbox/)

![20221219182157.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/104311/896ab34a-24a3-c99d-9bf2-bf2ddf4895e0.png)

勝手にセレクトボックス内のキーボード操作機能とかついてくれるのサイコーですね……。

共通コンポーネントとして`/src/app/shared/components/selectbox/selectbox.component.*`みたいなのを切り出しました。

実装はtsとhtmlで50行程度です、スバラシ！

## アコーディオン - CdkAccordionModule
リリース後のスマホ表示対応で、表示領域の確保を行うために
セレクトボックスをアコーディオンで開閉するようにしました。

- [CdkAccordionModule](https://material.angular.io/cdk/accordion/)

![20221219182916.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/104311/8c538c79-50ca-4551-e7f2-28d77e7d229a.png)

正直変数用意してclassを付け替えるだけの実装とあまり変わりないのですが、
こちらもCDKを利用することでrole属性がイイカンジに付与されてアクセシビリティを担保できます。

ただ、今回ちょっとめんどくさがって、CSSのメディアクエリでアコーディオンの有効/無効化を操作していますので、
アコーディオンが使われていないデスクトップUIでも、DOMに`role="button"`がついてしまっています。

本来は[LayoutModule](https://material.angular.io/cdk/layout)とかを利用して、
cdk-accordion要素の切り替え操作とかをしなければいけないかもしれませんネ……。

## 仮想スクロール - ScrollingModule
CDKのv15で無事experimentalが外れた仮想スクロール機能も活用しています。

- [ScrollingModule](https://material.angular.io/cdk/scrolling/)

当初は[CdkTable](https://material.angular.io/cdk/table/)を利用してUIを組みました。
しかし、本アプリのように巨大なリストを一覧表示するためには、
やはり仮想スクロールを用いたほうがパフォーマンスが出るため、
リリース後にul/li要素を用いたcdk-virtual-scroll-viewportに置き換えました。

![20221219183954.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/104311/f8e04913-e73f-b902-c55c-3230bd24672d.png)

パフォーマンスの計測などは今回行っていないのですが、
実際に動かしてみて、想像以上に応答速度がはやくなったので驚きましたね！

## CDKに期待してたけどなかったのであきらめたこと
[Angular MaterialにはタブUI](https://material.angular.io/components/tabs/)が用意されているのですが、残念ながらCDKには同じものはないようでした😢

ここは我慢して、他のコンポーネントでも利用した前述の[CdkListboxModule](https://material.angular.io/cdk/listbox/)を使って
CSSでタブっぽく仕上げました。

![20221219184609.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/104311/0195853c-29b2-91b0-7fa7-66efe2f1dc4f.png)

role属性には値として`tab`とかもあるのですが、ListBoxを利用したここにはもちろんそういったものは入っていないので、
アクセシビリティ的には☓ですネ😢😢


# v15の目玉機能たちについて
このアプリを作り始めたときは、ちょうどv15がリリースされたばかりなのですが、
v15の目玉機能の一部も早速利用しています！

## Standalone components
v15からはNgModuleを不要とした[Standalone components](https://angular.jp/guide/standalone-components)というものがstableになっておりまして、
今回は試しに使ってみました！

必要なファイル数が減ったことや、
コンポーネントをひと目見るだけで依存がすべてマルワカリになるのが嬉しかったですね。

```ts
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush,
  standalone: true,
  imports: [
    // Common Modules
    CommonModule,
    NgOptimizedImage,
    RouterModule,
    // CDK Modules
    CdkListboxModule,
    ScrollingModule,
    // Components and Pipes
    AdsComponent,
    SearchBoxSectionComponent,
    SelectboxComponent,
    ShareButtonsComponent,
    I18nPipe,
  ],
  styleUrls: ['./app.component.scss'],
})
```

importsに全部ドバっと記述されるので、こんな感じでコメントアウトしておくと見やすいんでないかな〜と思います！

## NgOptimizedImage
v15からstableになったAPIの中には他にも[NgOptimizedImage](https://angular.jp/guide/image-directive)ディレクティブというものが存在します。

これは、img要素のsrc属性を拡張したngSrc属性を提供し、遅延読み込みなどによるさまざまな画像の最適化を行います！

https://angular.jp/guide/image-directive

やってくれることがいろいろあって割とよくわかってないところもあるのですが、

> enforces a number of image best practices

と[公式ドキュメントで言ってる](https://angular.io/guide/image-directive)ので、使い得でしょう！！

# 多国語化 i18n(の反省とか)
Angularには[公式ドキュメントに載ってるi18nの手法](https://angular.jp/guide/i18n-overview)があります。

いわく、`@angular/localize`パッケージを`ng add`することで有効化できるようです。

i18n対応はなんだかんだ一度も経験がなく、
「色々と準備が必要で面倒だなぁ」と思ったので、
今回は次のようなパイプを作ってオレオレi18nをしてしまいました。

```ts
@Pipe({
  name: 'i18n',
  standalone: true,
})
export class I18nPipe implements PipeTransform {
  constructor(private settingsService: SettingsService) {}

  transform(value: string, placeholders: (string | number)[] = []): string {
    const lang = this.settingsService.getLanguage();
    const dict = lang === 'ja' ? ja : en;
    const text = dict.get(value) || value;
    const placed = placeholders.reduce<string>(
      (a, b) => a.replace('%%%', `${b}`),
      text
    );
    return placed;
  }
}

// ...ここに百行くらいの翻訳Mapがある
```

そんなに悪くない感じもするのですが、SEOの観点から見ると、
ロボットに認識される言語が翻訳前のものになってしまうなどの問題があります。
(実装しながら気づきました。)

工数が取れるのであれば、やはり公式のi18nに則るのがよいですかねぇ😢

Angularでは人気のi18nライブラリに[ngx-translate](https://github.com/ngx-translate/core)というものがあるので、こちらもいつか触ってみたいですね。

# その他
爆速開発に一役買ってくれるライブラリとして、eslint/prettierなんかも入れました！
勢いだけの一人開発でも、キレイなコードにしたいですよね！！！
この辺をもろもろ加味して、今回は次のような感じでライブラリを追加してます！

```sh
$ ng add @angular/cdk
$ ng add @angular-eslint/schematics
$ npm i -D prettier eslint eslint-config-prettier @typescript-eslint/eslint-plugin @typescript-eslint/parser @angular/language-service
```

---

**The Great and Powerful Angular!!**