Blazorアプリでダークモードを実装してみる

ダークモードは、OS、アプリケーション、今や様々な場面で実装されています。
Blazorでtwitter風のダークモードを実装してみましょう。

<img src ="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/110304/4c04f2a5-745d-76c3-c48e-47c77e18821a.gif" width=90%>

[サンプルコード](https://github.com/kimux8/BlazorDarkMode)

# カスタムCSSによるUIの変更

「Blazor darkmode」で検索すると例がいくつか見つかりました。その中で、「Blazor WebAssembly - Dark Mode - CSS Variables - Applying Themes」というタイトルの動画を参考に、簡単にダークモードを実装してみたいと思います。

<iframe width="560" height="315" src="https://www.youtube.com/embed/a6W1e4QKYhw" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

# CSSのスコープ

Blazorプロジェクトを作成するといくつかの`CSS`ファイルが作成されています。`CSS`の範囲を確認してみます。`CSS`の分離が行えるようになってから、`app.css`に記述されていたスタイルがコンポーネントごとに設定できるのが確認できます。`app.css`は全体に適応されます。`app.css`に定義しておくことで、アプリケーション全体に適応できそうです。

![pic.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/110304/c31f32ba-45e0-0a03-8130-977049c5d12f.png)

## app.cssへの記述

全てのコンポーネントに適用される`app.css`へ、コンポーネントに適応したい定義をあらかじめ書いておきます。デフォルトは`::root`、ダークブルーは`.dark-mode-blue`、ブラックは`.dark-mode-black`として、それぞれ適応したい色やスタイルを定義しておきます。

```css:app.css
:root {
    /* white */
    --background: white;
    --font-color: black;
    --highlight: rgba(0,0,0,0.4);
    --highlight-2: #e2e3e5;
    --link: #0366d6;
    --background-img: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
    --border-color: #1861ac;
    --btn-font-color: #fff;
    --btn-background-color: #1b6ec2;
}

.dark-mode-blue {
    /* dark-mode blue */
    --background: rgb(21, 32, 43);
    --font-color: white;
    --highlight: rgb(21, 32, 43);
    --highlight-2: #414648;
    --link: #3ca4ff;
    --background-img: none;
    --border-color: #3ca4ff;
    --btn-font-color: white;
    --btn-background-color: rgb(21, 32, 43);
}

.dark-mode-black {
    /* dark-mode black */
    --background: black;
    --font-color: white;
    --highlight: black;
    --highlight-2: #414648;
    --link: #3ca4ff;
    --background-img: none;
    --border-color: #3ca4ff;
    --btn-font-color: white;
    --btn-background-color: black
}
```

これらのカスタムCSSを、ユーザーが選択したものが適用したい箇所に記述してみましょう。
例えば、`MainLayout`のコンポーネントの背景色と文字色は次のようになります。

```CSS:MainLayout.razor.css
div.main {
    background-color: var(--background);
    color: var(--font-color);
}
```
# ダークモードの適用

適用したいスタイルを使いたいところで指定します。今回は、twitterのようにすべてにダークモードを指定したかったので、最上位の`page`に対し、ダークモードを適用しています。
`StateContainer`は、適用するデザインの状態管理クラスです。文字列`Property`の変更とともにコンポーネントの`StateHasChanged`を呼び出して、再描写を行えるようにしておきます。

(参考)[ASP.NET Core Blazor 状態管理]
(https://docs.microsoft.com/ja-jp/aspnet/core/blazor/state-management?view=aspnetcore-5.0&pivots=webassembly)

```html:MainLayout.razor
@inherits LayoutComponentBase

@implements IDisposable
@inject StateContainer StateContainer

<div class="page @(StateContainer.Property)"> 👈 ここで全体にスタイルが適用されるように指定
    <div class="sidebar">
        <NavMenu StateContainer="StateContainer"/>
    </div>

    <div class="main">
        <div class="top-row px-4">
            <a href="http://blazor.net" target="_blank" class="ml-md-auto">About</a>
        </div>

        <div class="content px-4">
            @Body
        </div>
    </div>
</div>



@code {

    protected override void OnInitialized()
    {
        StateContainer.OnChange += StateHasChanged;
    }

    public void Dispose()
    {
        StateContainer.OnChange -= StateHasChanged;
    }
}

```

# ダークモードへの切り替え

ダークモードへの切り替えは、`StateContainer`の`Property`に、`CSS`で定義したクラス名がセットされることで、コンポーネントの`StateHasChanged`が呼ばれ、UIに反映されます。
Blazorではコンポーネント間におけるデータの受け渡しに`StateContainer`のような状態管理は必要になってきますね🤔

```html:NavBar.razor
<div class="top-row pl-4 navbar navbar-dark">
    <a class="navbar-brand" href="">BlazorDarkMode</a>
    <button class="navbar-toggler" @onclick="ToggleNavMenu">
        <span class="navbar-toggler-icon"></span>
    </button>
</div>

<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
    <ul class="nav flex-column">
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
                <span class="oi oi-home" aria-hidden="true"></span> Home
            </NavLink>
        </li>
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="counter">
                <span class="oi oi-plus" aria-hidden="true"></span> Counter
            </NavLink>
        </li>
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="fetchdata">
                <span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
            </NavLink>
        </li>
        👇ボタンにより適応したいスタイルのクラス名をStateContainerのPropertyにセット
        <li class="nav-item px-3">
            <button class="btn btn-primary " @onclick="@(_=>StateContainer.Property = "")">デフォルト</button>
        </li>
        <li class="nav-item px-3">
            <button class="btn btn-primary dark-mode-blue" @onclick="@(_=>StateContainer.Property = "dark-mode-blue")">ダークブルー</button>
        </li>
        <li class="nav-item px-3">
            <button class="btn btn-primary dark-mode-black" @onclick="@(_=>StateContainer.Property = "dark-mode-black")">ブラック</button>
        </li>

    </ul>
</div>

@code {

    [Parameter]
    public StateContainer StateContainer { get; set; }

    private bool collapseNavMenu = true;

    private string NavMenuCssClass => collapseNavMenu ? "collapse" : null;

    private void ToggleNavMenu()
    {
        collapseNavMenu = !collapseNavMenu;
    }
}
```


### 参考
[Blazor WebAssembly - Dark Mode - CSS Variables - Applying Themes](https://www.youtube.com/watch?v=a6W1e4QKYhw)
[ASP.NET Core Blazor 状態管理](https://docs.microsoft.com/ja-jp/aspnet/core/blazor/state-management?view=aspnetcore-5.0&pivots=webassembly)