Phoenix+Elmでマルチページ

　Phoenix+Elmでマルチページを実現するための枠組みの基本を考えてみました。SPA（シングルページアプリケーション）ではなくマルチページです。Brunchのなじみが浅い私の個人的な備忘録なので、もっとベターな方法があると思いますが、ご指摘いただければ幸いです。


#１． ページ設計

　まず今回の実験のページ設計ですが、以下のように3枚のページ(html)を用意するつもりです。それぞれのパスでアクセスすれば、elmプログラムを含んだページを表示します。目標はシンプルです。


```
"/"           -- トップページ (Elm無し）
"/page1"      -- ページ１(Hello.elm)
"/page2"      -- ページ２(MyButtons.elm)
```



#２． Plug.Staticの設定

　それではプロジェクトを作成します。 

```
mix phx.new multi_pages --no-ecto
```

　後で述べますが、Elmプログラムをコンパイルした結果のjsファイルはテンプレートファイルから以下のようにして直接ロードします。

```
<script src="/js/mybuttons.js"></script>
```

　そのためPhoenixの設定でstaticファイルを読み込む設定を変えます。onlyをコメントアウトします。許可ファイルを追加しても良いですが、ここではコメントアウトしpriv/staticにあるファイルを全てpublicなものにします。[PhoenixでStatic Fileをサーブする - Qiita](https://qiita.com/sand/items/40a281e3f1f4467fd779)


```elixir:lib/multi_pages_web/endpoint.ex
#
  plug Plug.Static,
    at: "/", from: :multi_pages, gzip: false
    # only: ~w(css fonts images js favicon.ico robots.txt)
#
```




#３． routerの設定

 上の「１．ページ設計」で示した通りにrouter.exを変更します。

```elixir:lib/multi_pages_web/router.ex
#
  scope "/", MultiPagesWeb do
    pipe_through :browser # Use the default browser stack
    get "/", PageController, :index
    get "/page1", Page1Controller, :hello
    get "/page2", Page2Controller, :buttons
  end
#
```


#４． トップページの設定

　最初にトップページを変更します。ヘッダーに他のページ(page1, page2)へのリンクを貼ります。デフォルトのレイアウト(app.html.eex)を変更します。


```html:lib/multi_pages_web/templates/layout/app.html.eex
#
      <p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
      <p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
      <a href="/page1">ページ１</a> /
      <a href="/page2">ページ２</a>

      <main role="main">
        <%= render @view_module, @view_template, assigns %>
      </main>

    </div> <!-- /container -->

  </body>
</html>
```

　またこの時以下の行を削除しておきます。トップページではjsやelmは使わないので不要です。

```
    <script src="<%= static_path(@conn, "/js/app.js") %>"></script>
```

　またトップページのテンプレートは変更しません。
lib/multi_pages_web/templates/page/index.html.eex


　controllerもviewもデフォルトのままで変更しません。


　以上でトップページは終わりです。


#５． ページ１の設定

　ページ１を追加していきます。controllerを以下のように定義します。今回は実験ですのでレイアウトも変更しています。

```elixir:lib/multi_pages_web/controllers/page1_controller.ex
defmodule MultiPagesWeb.Page1Controller do
  use MultiPagesWeb, :controller

  def hello(conn, _params) do
    conn
    |> put_layout("layout_hello.html")
    |> render("hello.html")
  end
end
```

　viewも定義します。中身はデフォルトのままです。

```elixir:lib/multi_pages_web/views/page1_view.ex
defmodule MultiPagesWeb.Page1View do
  use MultiPagesWeb, :view
end
```

　テンプレートを追加するので、ディレクトリを作成します。

```
mkdir lib/multi_pages_web/templates/page1
```

　テンプレートを追加します。elmプログラムのためのプレースフォルダを設定し、elmプログラムを明示的に指定していることに注意してください。まあ、通常のelmの設定ですね。

```html:lib/multi_pages_web/templates/page1/hello.html.eex
<h1>Page1</h1>
<h3>Hello page</h3>

<div id="elm-hello-area"></div>
<script src="/js/vendor/hello.js"></script>
<script>
    Elm.Hello.embed(document.getElementById("elm-hello-area"));
</script>
```

　レイアウトはトップページのものをコピーし、リンクを以下のように変更します。

```html:lib/multi_pages_web/templates/layout/layout_hello.html.eex
#
      <a href="/">トップページ</a> /
      <a href="/page2">ページ２</a>
#
```

　以上でページ１の設定は終わりです。


#６． ページ２の設定

　ページ２の設定は、ページ１とほぼ同じです。ファイル名やモジュール名、関数名などが異なるだけです。

　controllerです。

```elixir:lib/multi_pages_web/controllers/page2_controller.ex
defmodule MultiPagesWeb.Page2Controller do
  use MultiPagesWeb, :controller

  def buttons(conn, _params) do
    conn
    |> put_layout("layout_buttons.html")
    |> render("buttons.html")
  end
end
```

　viewです。

```elixir:lib/multi_pages_web/views/page2_view.ex
defmodule MultiPagesWeb.Page2View do
  use MultiPagesWeb, :view
end
```

　テンプレートディレクトリを作成します。

```
mkdir lib/multi_pages_web/templates/page2
```

　テンプレートです。

```html:lib/multi_pages_web/templates/page2/buttons.html.eex
<h1>Page2</h1>
<h3>My Buttons page</h3>

<div id="elm-buttons-area"></div>
<script src="/js/vendor/mybuttons.js"></script>
<script>
    Elm.MyButtons.embed(document.getElementById("elm-buttons-area"));
</script>
```

　レイアウトです。

```html:lib/multi_pages_web/templates/layout/layout_buttons.html.eex
#
      <a href="/">トップページ</a> /
      <a href="/page1">ページ１</a>
#
```


　以上でページ２の設定を終わります。



#７． Elmクライアントの設定

　Brunchの設定をしていきます。

```
cd assets/
npm install --save-dev elm-brunch
```

　brunch-config.jsを変更します。今回の肝の部分です。

```js:assets/brunch-config.js
#
  // Phoenix paths configuration
  paths: {
    // Dependencies and current project directories to watch
    // (1)"elm"を追加
    watched: ["static", "css", "js", "elm", "vendor"],
    // Where to compile files to
    public: "../priv/static"
  },

  // Configure your plugins
  plugins: {
    babel: {
      // Do not use ES6 compiler in vendor code
      ignore: [/vendor/]
    },
    // (2)elmBrunchを追加
    elmBrunch: {
      elmFolder: "elm",
      mainModules: ["Hello.elm", "MyButtons.elm"], // Elmプログラム
      outputFolder: "../static/js/vendor"  // Elmの実行形をstaticに吐き出す
    }
  },
#
```

（※重要）outputFolderにはvendorサブディレクトリが指定されていることに注意してください。brunchにおいてはvendorにあるfileが優先的にロードされるようです。つまりElmのjsファイルが安全にロードされます。単にoutputFolder を "../static/js"と vendor抜きで指定すると、かなりの確率でElmのjsファイルのロードに失敗してしまいます。



　今回は２つのelmプログラムを使います。page1でHello.elmを、page2でMyButtons.elmを走らせます。elmプログラムをmainModulesで指定し、assets/static/jsにコンパイルするように指定しました。brunchはassets/static/ 下のファイルををそのままpriv/static/にコピーします。priv/static/は「２．Plug.Staticの設定」での設定によりpublicとなっており、テンプレートから以下のように指定することができます。

```
<script src="/js/vendor/hello.js"></script>
<script src="/js/vendor/mybuttons.js"></script>
```

　次にassets/static/jsディレクトリとasstes/elmディレクトリを作成し、必要なelmパッケージをインストールしておきます。


```
mkdir static/js
mkdir elm
cd elm
elm-package install elm-lang/html
```

　Hello.elmがpage1で走らせるelmプログラムです。

```elm:assets/elm/Hello.elm
module Hello exposing (..)
import Html exposing (text)

main =
  text "Hello, World!"
```


　MyButtons.elmがpage2で走らせるelmプログラムです。これはMyButtonsBody.elmをimportしています。動作を確認するためにわざとElmプログラムを分割してみました。そもそもelmコンパイラは、複数の分割ファイルをリンクしてくれますので、この点ではbrunchに頼る必要がありません。複数ファイルを一つの実行形にまとめるのはelmコンパイラであり、brunchではありません。ちなみにbrunchがまとめたapp.jsファイルは使わないで無視します。


```elm:assets/elm/MyButtons.elm
module MyButtons exposing (..)

import Html
import MyButtonsBody exposing (model, update, view)

main =
  Html.beginnerProgram { model = model, view = view, update = update }
```


　MyButtons.elmはMyButtonsBody.elmをimportしています。


```elm:assets/elm/MyButtonsBody.elm
module MyButtonsBody exposing (model, update, view)

import Html exposing (Html, button, div, text)
import Html.Events exposing (onClick)

-- MODEL
type alias Model = Int

model : Model
model =
  0

-- UPDATE
type Msg = Increment | Decrement

update : Msg -> Model -> Model
update msg model =
  case msg of
    Increment ->
      model + 1

    Decrement ->
      model - 1

-- VIEW
view : Model -> Html Msg
view model =
  div []
    [ button [ onClick Decrement ] [ text "-" ]
    , div [] [ text (toString model) ]
    , button [ onClick Increment ] [ text "+" ]
    ]
```


　以上でクライアント側のElmの設定は終わりです。



#８． プログラムの実行

　トップページです。レイアウトを変更してページ1とページ2のリンクを設けました。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/61915/77b5b4a7-fdea-c5bc-06d4-efd7a2d4dc20.png)


　ページ1です。リンクを変更しました。「Hello world!」はelmプログラムが走った結果です。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/61915/95e7b730-2678-3020-35be-3420dff94e88.png)

　ページ2です。リンクを変更しました。カウンターを増減させるボタンが表示されています。これもelmプログラムです。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/61915/1b1f9eac-b01e-3071-1382-9ae20b208150.png)


　以上です。