ElmでデスクトップPWAを作っている話

こんにちは。[Elm Advent Calendar 2019 \- Qiita](https://qiita.com/advent-calendar/2019/elm)の8日目担当の@gaaamiiです。あんまり勉強会参加やアウトプットできてないけど本当はElm書きたいんだ!という隠れElmユーザーとして、趣味でちょこちょことElmを書いています。

いまは[Nekobito](https://github.com/gaaamii/nekobito)というMarkdownエディタを作っています。この記事ではこのアプリケーションをつくる際に必要だった作業や考えたことをだらだらと書いていきたいと思います。

## PWA対応
PWA対応をしておくと、Chromeであればネイティブアプリっぽく使えるのでとても気分が良くなります。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/11316/5e69cf25-d170-6fc1-5bec-e1e3d16e5b22.png)

とはいえ正直ここはあまり書くことがありません。[create\-elm\-app](https://github.com/halfzebra/create-elm-app)を使うと、manifest.jsonやserviceWorker.jsなどの必要なものが勝手に作られます。助かる〜。

```
~/$ create-elm-app my-app
~/$ cd my-app
~/my-app$ tree .
.
├── README.md
├── elm-stuff
│   └── 0.19.1
│       ├── Main.elmi
│       ├── Main.elmo
│       ├── d.dat
│       ├── i.dat
│       ├── lock
│       └── o.dat
├── elm.json
├── public
│   ├── favicon.ico
│   ├── index.html
│   ├── logo.svg
│   └── manifest.json <- manifest.jsonが生成されてる！
├── src
│   ├── Main.elm
│   ├── index.js
│   ├── main.css
│   └── serviceWorker.js <- serviceWorkerのコードも生成されてる！助かる!
└── tests
    └── Tests.elm

```

## Native File System APIを利用してメモをファイルに保存する機能を実装する
趣味で作っているものがMarkdownメモ帳ということもあり、Native File System APIがChromeで使えるようになった[^1]ことは個人的には今年一番ビッグなブラウザ機能ニュースでした。

* [The Native File System API: Simplifying access to local files](https://web.dev/native-file-system/)

ブラウザアプリからローカルのファイル読み書きできるということはつまり、サーバーサイドの開発どころかBaaSさえ利用せずに、そこそこ使えるアプリケーションが提供できるということです。

[^1]: といってもまだ`chrome://flags`で`#native-file-system-api`をenabledにしないと使えませんが...

さて、そんな便利機能ですが、Elmからは利用できないのでJSで書くしかありません。なのでここに書くことはほとんどありません。1つだけ確かなのは、Portsがあってよかったということです。Elm側には以下のように書いて、syncSettingの実装はJavaScript側に `window.chooseFileSystemEntries`云々を書けばいいのです。

```elm
-- view
-- buttonをクリックするとOnSyncSettingが投げるようにしておき
button [ onClick OnSyncSetting ] []

-- update
-- OnSyncSettingでsyncSettingを呼び出し
OnSyncSetting ->
    (model, Cmd.batch [ syncSetting "sync setting" ])

-- port宣言を書く
port syncSetting : String -> Cmd msg
```

Nekobitoには、これを使った機能はまだちゃんと実装できていません。理想の挙動に持っていくまでだいぶ課題がありそうなのですが、少なくとも特定ディレクトリのファイル名一覧を表示するというところまではできたので、どうにかまともに使えるものにしていきたいです。


![](https://gyazo.com/79746c8a383d8e85741511f161f7bf23.gif)
特定ディレクトリのファイル名一覧を表示する様子

## 汎用的に使えそうなUIのモジュールをどう定義するか問題
Nekobitoの場合、この問題に遭遇したのはモーダルの見た目を実装しようと思ったときです。

先のファイル同期機能を作る際に、同期先のディレクトリを選べるようにするためのUIが必要です。モーダルでそれを表示しようとしたのですが、モーダルなので多少は使い回せるようにしたかった。Reactで書くと以下のように使えるものが欲しかったのです。

```jsx
<Modal title="ストレージの設定" onClose={handleClose}>
  <StorageSetting onSubmit={handleSubmit} />
</Modal>
```

最終的に、Modalモジュールの実装はこんな感じになりました[^2]。

[^2]: この実装をしている際、ちょっとよくわからなくなって[ababさんに考え方を教えてもらったりしました](https://twitter.com/ababupdownba/status/1199968216887377920)。ありがとうございました :pray: とはいえやはり子要素は渡したくて、この記事に書いているような形に落ち着きました。

```elm
module Modal exposing (view)

import Html exposing (Html, button, div, h2, i, span, text)
import Html.Attributes exposing (class)
import Html.Events exposing (onClick)


type alias Props msg =
    { visible : Bool
    , title : String
    , children : Html msg
    }


view : Props msg -> msg -> Html msg
view props msg =
    if props.visible then
        div [ class "app-modal" ]
            [ div [ class "app-modal__overlay" ]
                [ div [ class "app-modal__body" ]
                    [ h2 [ class "app-modal__body__header" ]
                        [ span [ class "app-modal__body__header__text" ]
                            [ text props.title ]
                        , button [ class "btn app-modal__body__header__button", onClick msg ]
                            [ i [ class "material-icons" ] [ text "close" ]
                            ]
                        ]
                    , props.children
                    ]
                ]
            ]

    else
        div [] []
```

使う側のコードはこんな感じです。

```elm
Modal.view { visible = True, title = "Sync with your local direcotry", children = viewSetting } CloseSettingModal

viewSetting : Html Msg
viewSetting =
    button [ class "app-storage-setting", onClick UpdateSetting ]
        [ text "更新" ]
```


少し混乱したのは、ModalモジュールのPropsの型です。型変数のmsgがないと、childrenの型が書けずに困りました。

しかしchildrenを渡せることはわかったので、Modalのように汎用的なUIは上記のような形で作っていけば良さそうです。

## CSS in JSっぽいことは諦めた
多少は調べてみたものの、いい感じにする方法がわからず諦めました。。時間ができたら再挑戦したい。

## 所感
やっぱElmはいいですね。型をしっかり書かされて、型さえ合ってればしっかり動いてしまうのがとても良いです。特に、APIレスポンスやJS界での処理の結果をElmのデータ型に変換するときなど、OkとErrの2パターンを書かないとそもそもコンパイル通らないって形になっているところなどが自分みたいな注意力が低いポンコツには嬉しいです :sob:

Nekobitoはまだまだろくな完成度になっていませんが、Elmが書きたくなったときにちょいちょいと機能追加や修正を繰り返して、自分が快適に使えるMarkdownメモ帳にしていきたいと思います。