Angularでセレクト部品のオプションをカスタムする

はじめに

こんにちは!株式会社VoicyでWebフロントエンジニアをしているしーちゃん(@shinobear01)です。 今回は、Angular Materialを使ったセレクト部品のオプションのカスタム方法について書きます。

セレクト部品のオプションのカスタム方法

今回、デザインをもとに新たにセレクト部品を作る必要がありました。 デザインを見て、セレクト部品について調べたところ、 HTMLのデフォルトのオプション部品をカスタムするのは難しいと書いてあったので、諦めかけました。 しかし、いろいろ試行錯誤した結果、指定のデザインを実現できたので記録しておきます!

実際に実現したデザインは以下です。

セレクト部品の完成版
今回実現したデザインの特徴としては、以下が挙げられます。

  1. セレクト部分(リストを表示するトリガー)とオプション部分(リスト表示)が重ならない
  2. セレクト部分の右端に、オプション部分が開いているかどうかを示す矢印を表示する
  3. オプション部分を選択すると文字が強調される、かつ、右端に指定の画像を表示する

カスタムしたセレクト部品の説明

Angular Materialで使った部品の選定

まず結論から言うと、今回Angular Materialから使用した部品はMenuだけです。

セレクト部品を1から自分で作成するのは大変なので、今回はカスタムするとしても、何かしらのもとになる部品をAngular Materialから持ってくるという方針で進めました。 候補に挙げた部品には以下があります。

この中でまず、Selectを使ってみました。

Select

Selectは、単体で見ると名前の通り動作が一番理想系に近かったのですが、セレクト部分とオプション部分がつながっているまたは重なっているデザインでした。Menuにある、overlapTriggerのプロパティがあれば、セレクト部分とオプション部分を重ねるかどうかを指定できるのですが、Selectにはこのプロパティはなく実現できそうにありませんでした。

1.セレクト部分とオプション部分が重ならない

これを満たすことができないため、他の部品を見ることにしました。 1の条件を満たす要素を探したところ、Menuがありました。

Menu

こちらは、1を満たしていました。

しかし、2, 3が実現できません。

2.セレクト部分の右端に、オプション部分が開いているかどうかを示す矢印を表示する

3.オプション部分を選択すると文字が強調される、かつ、右端に指定の画像を表示する

そこで、 Menuの中で表示する項目をカスタムする方法を取ろうと思い、見つけたのがListでした。

List
Menuのオプション部分にこのListを表示する方法を考えました。しかし以下のオプションが選択されていることを示す画像を変更するのが難しそうでした。

3.オプション部分を選択すると文字が強調される、かつ、右端に指定の画像を表示する

その結果、Menuのオプション部分は自分で作成することにしました。

CSSでng-deepとクラスを指定する方法

次に、どのようにカスタマイズしたか書きます。

Menuの中に自分でngForを指定してリストを表示したところまでは良かったのですが、どう指定しても編集できないスタイルが出てきてしまいました。以下の画像では、オプション部分に影や余計なマージンが指定されています。

ng-deep指定なし

ng-deepを使用しないと指定できないプロパティ

そこで::ng-deep を使います。Angularには::ng-deepを使用しないとカスタムできないスタイルが指定されていることがあります。

しかし、単純に::ng-deepだけを使用すると、他のコンポーネントで使っているスタイルに影響が出る可能性があります。 そこで今回は、::ng-deepを指定するクラスを絞るために、 MatMenuの機能であるMatMenuContentbackdropClassを使用しました。 backdropClassに適当なクラスを指定して、::ng-deepと指定したクラス名で絞り込みすると、クラスを付与したコンポーネントだけでそのスタイルが適応されます。

ちなみに、通常よく行うようにMatMenuにclass="select-mat-menu"を指定した場合、CSSにおいて.select-mat-menuで絞り込みを行ってもスタイルが適応されません。 これは、以下の図を見てもらうとわかると思います。

backdropClassを適応するとcdk-overlay-backdropにクラスを追加できる

MatMenuのオプション部分は、cdk-overlay-containerという上部の層に表示されます。MatMenuを表示するmat-menuタグにクラスをつけても階層が異なり、絞り込みが行えません。そこで、backdropClassにクラスを指定することでcdk-overlay-container直下のcdk-overlay-backdropにクラスを追加します。

こうすることで、以下のように今まで直接いじれなかったmat-menu-panelmat-menu-contentを指定した.select-mat-menuの任意のクラスで絞って上書きすることができます。

::ng-deep {
  .select-mat-menu {
    + * .cdk-overlay-pane > div.mat-menu-panel {
      border-radius: 8px !important;
      box-shadow: none !important;
      border: 1px solid #dbdada !important;
      margin-top: 8px !important;
      min-height: max-content !important;
      max-width: 100% !important;
      & > div.mat-menu-content {
        padding: 2px 0 !important;
      }
    }
  }
}

補足ですが、::ng-deepはAngular公式サイトで非推奨になっているのでトリッキーな方法かもしれません。使用する場合は自己責任でお願いします!

おわりに

今回は、Angularでセレクト部品のオプションをカスタムする方法について書きました。 完成してからSelectを::ng-deepでカスタムして実現も可能だったかもと思いました。いろんな実装をして経験値を高めていきたいです。