Elm で描画した要素の中に JavaScript のコンポーネントを埋め込みたくなることがたまにあります。この記事では、よくある例としてエディタを埋め込むサンプルを作ってみます。今回使うライブラリは CodeMirror です。
というわけで出来たのがこちら。
これだけだと分かりにくいですが、エディタはちゃんと Elm の中にいます。下手に DOM をいじると Virtual DOM がエラーを出しますが、ポイントさえ押さえれば特に問題ないようです。
Elm 側のコード
#editor
要素を元に CodeMirror に初期化してもらいます。 CodeMirror 特有の事情としては、この要素のすぐ後にエディタの要素が挿入されるため、その場所に他の要素は置かないようにします。
view : Model -> Html Msg
view model =
div [ class "main" ]
[ div [ class "editor" ]
[ textarea
[ id "editor"
, style "display" "none"
, onInput Input
]
[ text "Hello, press `Ctrl+S` to see the result." ]
]
, pre [ class "result" ] [ text model.lastSaved ]
]
この例では初期化する前に一瞬汚い textarea が見えないように非表示にしていますが、編集ボタンを押したらエディタに切り替えるような UI にも出来そうです。
面白いところは onInput
が普通に効くところで、テキストを同期するだけであれば Port すら不要のようです。
とは言え、これだけだとつまらないので保存(Ctrl+S
)した時にメッセージを受け取れるようにしてみます。
port save : (String -> msg) -> Sub msg
JavaScript 側のコード
エディタの初期化だけちょっとトリッキーです。 Elm で描画されたタイミングを知る方法がないので、要素が出てくるまで setInterval
で待っています。まあこれだと失敗した時に無限ループしてしまうので、ちゃんと書くならもう少し工夫が必要そうですが。
var app = Elm.Main.init({
node: document.getElementById("app")
});
var initializer = setInterval(function () {
var textarea = document.getElementById("editor");
if (!textarea) {
return;
}
var editor = CodeMirror.fromTextArea(textarea, {
lineNumbers: true
});
editor.setOption("extraKeys", {
["Ctrl-S"]: function (cm) {
app.ports.save.send(editor.doc.getValue());
},
["Cmd-S"]: function (cm) {
app.ports.save.send(editor.doc.getValue());
}
});
clearInterval(initializer);
}, 10);
ショートカットキーで保存したら Elm 側にメッセージを送るようにしています。利便性のためにテキストを送っていますが、 onInput
で既に取得しているので実はなくてもいけます。
感想
もっとトラブるかと思ったのですが、実際には意図的にエラーを起こすまでは平穏無事でした。エディタやその先祖の要素を消すとまた textarea に戻ってしまうというトラブルがあるのですが、実際にはそんなにポンポン初期化を繰り返すようなアプリは多分あまりないと思うので、そこまで心配する必要もなさそうです。
将来的には WebComponents を使うともっとスマートに出来そうです。興味のある方は先駆者の動画もご覧ください。