この内容は Elm 0.18, 0.19 を対象にしています。
また、Elm Discourseの回答 が元ネタです。
結論
update
関数で DOM を更新した後に JavaScript を実行したい場合、
elm.app.ports.foo.subscribe(function (id) {
requestAnimationFrame(function () {
/* when this callback executes, the view should have rendered. */
})
})
のように ports を登録する必要があります。
問題提起
Elm の update
関数は、型 update : Model -> Msg -> (Model, Cmd Msg)
で示されるように、なんらかのイベント (Msg
で示されるもの) に応じて、更新後のDOM構造 (厳密にはDOM構造そのものではなく Model
) と副作用 (Cmd Msg
) を同時に指定します。
ports を用いて JavaScript を実行する場合、Cmd Msg
の部分にその処理を記述しますが、
更新後のDOMに依存する JavaScript コードを実行したい場合はどうしたらいいでしょうか?
画面更新と Cmd の処理タイミング
update
関数によって新しい DOM 構造が指定されると、Elm はその更新処理を animation-frame
に登録します。
その直後に、Cmd Msg
として指定された処理を行います。
animation-frame
はブラウザ画面のリフレッシュレートに応じた間隔で処理を呼び出すため、
特に工夫なく ports を使って JavaScript の処理を Cmd として登録した場合、画面の更新処理よりも先にその JavaScript が実行されてしまう可能性があります。
解決方法
画面の更新処理を animation-frame
に登録しているので、実行したい JavaScript も同じように animation-frame
に登録すれば、 animation-frame
がタスクキューとして実行順を保証してくれます。
具体的には、冒頭で「結論」として示したように
elm.app.ports.foo.subscribe(function (id) {
requestAnimationFrame(function () {
/* when this callback executes, the view should have rendered. */
})
})
のような形式で、実行したい処理を animation-frame
に登録する処理を記述すれば良いことになります。
elm-lang/dom
などは内部的に animation-frame
に処理を登録する実装になっているため、更新後に初めて作成される HTML タグに対して elm-lang/dom
で定義されているスクロールやフォーカスなどの処理を適用する際には、特に気にすることなく update
の返り値に更新後のModelとともに処理を登録すればうまく動きます。
危険🐐スクリプト芸との関わり
実は危険🐐スクリプト芸を使っても「DOM 構造に依存するJavaScriptを実行する」という需要を叶えることができます。
単純に、View に script
タグを直接埋め込むだけです。
someView : Model -> Html Msg
someView { showPart } =
div
[]
[ if showPart then
div
[]
[ anotherView
, node "script" [] [ text """ /* JavaScript code */ """ ]
]
else
text ""
]
ただ、この手法は「バグ」としてあつかわれており、次のバージョンの Elm では使えなくなる可能性が高いです。
非推奨な危険🐐スクリプト芸ではなく、animation-frame
を使った方法を今のバージョンでも使うことをオススメします。