こんにちは。Elm Advent Calendar 2019 - Qiitaの8日目担当の@gaaamiiです。あんまり勉強会参加やアウトプットできてないけど本当はElm書きたいんだ!という隠れElmユーザーとして、趣味でちょこちょことElmを書いています。
いまはNekobitoというMarkdownエディタを作っています。この記事ではこのアプリケーションをつくる際に必要だった作業や考えたことをだらだらと書いていきたいと思います。
PWA対応
PWA対応をしておくと、Chromeであればネイティブアプリっぽく使えるのでとても気分が良くなります。
とはいえ正直ここはあまり書くことがありません。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ことは個人的には今年一番ビッグなブラウザ機能ニュースでした。
ブラウザアプリからローカルのファイル読み書きできるということはつまり、サーバーサイドの開発どころかBaaSさえ利用せずに、そこそこ使えるアプリケーションが提供できるということです。
さて、そんな便利機能ですが、Elmからは利用できないのでJSで書くしかありません。なのでここに書くことはほとんどありません。1つだけ確かなのは、Portsがあってよかったということです。Elm側には以下のように書いて、syncSettingの実装はJavaScript側に window.chooseFileSystemEntries
云々を書けばいいのです。
-- view
-- buttonをクリックするとOnSyncSettingが投げるようにしておき
button [ onClick OnSyncSetting ] []
-- update
-- OnSyncSettingでsyncSettingを呼び出し
OnSyncSetting ->
(model, Cmd.batch [ syncSetting "sync setting" ])
-- port宣言を書く
port syncSetting : String -> Cmd msg
Nekobitoには、これを使った機能はまだちゃんと実装できていません。理想の挙動に持っていくまでだいぶ課題がありそうなのですが、少なくとも特定ディレクトリのファイル名一覧を表示するというところまではできたので、どうにかまともに使えるものにしていきたいです。
汎用的に使えそうなUIのモジュールをどう定義するか問題
Nekobitoの場合、この問題に遭遇したのはモーダルの見た目を実装しようと思ったときです。
先のファイル同期機能を作る際に、同期先のディレクトリを選べるようにするためのUIが必要です。モーダルでそれを表示しようとしたのですが、モーダルなので多少は使い回せるようにしたかった。Reactで書くと以下のように使えるものが欲しかったのです。
<Modal title="ストレージの設定" onClose={handleClose}>
<StorageSetting onSubmit={handleSubmit} />
</Modal>
最終的に、Modalモジュールの実装はこんな感じになりました2。
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 [] []
使う側のコードはこんな感じです。
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パターンを書かないとそもそもコンパイル通らないって形になっているところなどが自分みたいな注意力が低いポンコツには嬉しいです
Nekobitoはまだまだろくな完成度になっていませんが、Elmが書きたくなったときにちょいちょいと機能追加や修正を繰り返して、自分が快適に使えるMarkdownメモ帳にしていきたいと思います。
-
といってもまだ
chrome://flags
で#native-file-system-api
をenabledにしないと使えませんが... ↩ -
この実装をしている際、ちょっとよくわからなくなってababさんに考え方を教えてもらったりしました。ありがとうございました とはいえやはり子要素は渡したくて、この記事に書いているような形に落ち着きました。 ↩