Phoenix LiveView を使って、textarea に入力した Markdown のテキストを逐次プレビューするサンプルです。
Phoenix 1.5 になって簡単に LiveView を利用できるようになり、動的なページを作るのが本当に簡単になりました。
もちろん万能ではないですし弱点も少なくありません。 それでも従来の web ページを構築する要領で動的なページを構築できるのは大きな利点だと思います。
Earmark - a pure-Elixir Markdown converter
Markdown から HTML への変換にここでは Earmark を利用します。
Earmark は ex_doc がドキュメントを生成するときにも利用されているパッケージです。
as_html!/2
で簡単に変換することができます。
iex(1)> Earmark.as_html!(""" ...(1)> # 第一部 ...(1)> ## 第一章 ...(1)> ### 第一節 ...(1)> ...(1)> | a | b | ...(1)> |---|---| ...(1)> | 1 | 2 | ...(1)> ...(1)> - a ...(1)> - b ...(1)> """) |> IO.puts() <h1> 第一部 </h1> <h2> 第一章 </h2> <h3> 第一節 </h3> <table> <thead> <tr> <th style="text-align: left;"> a </th> <th style="text-align: left;"> b </th> </tr> </thead> <tbody> <tr> <td style="text-align: left;"> 1 </td> <td style="text-align: left;"> 2 </td> </tr> </tbody> </table> <ul> <li> a </li> <li> b </li> </ul>
MdPreviw - Markdownプレビュー app
アプリケーションを作成していきます。
Phoenix プロジェクトを用意する
LIveView を利用するプロジェクトを新たに作成します。
ここでは作業を簡単にするためデータベースを利用せず --no-ecto
を指定します。
$ mix phx.new md_preview --live --no-ecto $ cd md_preview
パッケージを追加する
mix.exs
を編集して、依存パッケージに Earmark を追加します。
defp deps do [ {:phoenix, "~> 1.5.3"}, {:phoenix_live_view, "~> 0.13.0"}, {:floki, ">= 0.0.0", only: :test}, {:phoenix_html, "~> 2.11"}, {:phoenix_live_reload, "~> 1.2", only: :dev}, {:phoenix_live_dashboard, "~> 0.2.0"}, {:telemetry_metrics, "~> 0.4"}, {:telemetry_poller, "~> 0.4"}, {:gettext, "~> 0.11"}, {:jason, "~> 1.0"}, {:plug_cowboy, "~> 2.0"}, {:earmark, "~> 1.4"} # 追加 ] end
パッケージを取得します。
$ mix deps.get
ルーティングを追加する
live/md_preview_web/router.ex
を編集して LiveView のルーティングを追加します。
scope "/", MdPreviewWeb do pipe_through :browser live "/", PageLive, :index live "/markdown", MarkdownLive # 追加 end
LiveView を書く
LiveView を書いていきます。
…とは言っても LiveView のドキュメントにある実装例ぐらい簡単なコードです。
textarea
の内容の更新に逐一反応しないように phx-debounce="1000"
を指定しています。
更新が止まってから 1,000 ミリ秒 = 1 秒経過してからイベントをサーバに送ります。
またレンダリング済みの HTML をエスケープせずに挿入するために Phoenix.HTML.raw/1
を利用しています。
defmodule MdPreviewWeb.MarkdownLive do use MdPreviewWeb, :live_view @impl true def mount(_, _, socket) do {:ok, assign(socket, body: "")} end @impl true def render(assigns) do ~L""" <div> <form phx-change="update"> <textarea name="markdown[source]" class="source" phx-debounce="1000"></textarea> </form> </div> <div class="preview"> <%= raw @body %> </div> """ end @impl true def handle_event("update", %{"markdown" => %{"source" => source}}, socket) do body = Earmark.as_html!(source) {:noreply, assign(socket, body: body)} end end
assets/css/app.scss
にスタイルを追加して見た目を少し整えました。
.source { height: 20vh; resize: none; font-family: monospace; } .preview { border: solid thin #e0e0e0; padding: 10px; }
app を動かす
サーバを起動し http://localhost:4000/markdown
にアクセスすると、記事の先頭の画像のように textarea に入力した Markdown のテキストがすぐに変換されて表示されます。
$ iex -S mix phx.server
レンダリングを非同期にする
イベント送信の負荷を減らすために、また編集途中の半端な状態でレンダリングされないように、入力が止まってから 1 秒ごにテキストをサーバに送信するようにしています。 つまりテキストの入力とレンダリング結果の表示のタイミングは同期していません。
このような関係にあるばあい、ブラウザからのテキストの送信とサーバからの結果の送信は分離してしまっても問題ありません。
MdPreviewWeb.MarkdownLive
のイベントハンドラを次のように編集します。
"update"
を受けたらすぐに自分に対して :render
メッセージを送り、LiveView のイベントハンドラから抜けます。
その後 :render
メッセージを受けとたら Markdown から HTML に変換して表示内容を更新します。
@impl true def handle_event("update", %{"markdown" => %{"source" => source}}, socket) do send(self(), {:render, source}) {:noreply, socket} end @impl true def handle_info({:render, source}, socket) do {:noreply, assign(socket, body: Earmark.as_html!(source))} end
このテクニックは The Pragmatic Studio の Phoenix LiveView コースで解説されています。 2020年6月27日現在 $0 で提供されています。 興味のある方はぜひ受講してみてください。
いつか読むはずっと読まない:Real-Time Phoenix
The Pragmatic Bookshelf の「Real-Time Phoenix」。今年の3月3日に正式に出版され「Hands-On with Phoenix LiveView」で LiveView に触れられています。