2018年9月10日

Phoenix_2_Controller_View_Template

首先 create 新的 project rumbl

$ mix phoenix.new rumbl --database mysql

修改 config/dev.exs 的 DB 設定,然後 create database

$ mix ecto.create

啟動 server

mix phoenix.server

或是

iex -S mix phoenix.server

網址在 http://localhost:4000

調整首頁 /web/templates/page/index.html.eex

<div class="jumbotron">
  <h2>Welcome to Rumbl.io</h2>
  <p class="lead">Rumble out loud.</p>
</div>

Create User

系統通常會以 User 開發為起點。首先定義 Rumbl.User module,包含欄位 id, name, username, password

/web/models/user.ex

defmodule Rumbl.User do
  defstruct [:id, :name, :username, :password]
end

為什麼要使用 struct 而不是 map 定義 User model?

使用 map,在撰寫 model 定義,並不會在編譯時發現有打錯字的狀況,另外也不能為所有欄位都填上預設值。

在 project folder 直接測試

$ iex -S mix
Generated rumbl app

# 可以使用 User
iex(1)> alias Rumbl.User
Rumbl.User

# 以 Map 定義 user
iex(2)> user = %{usernmae: "jose", password: "elixir"}
%{password: "elixir", usernmae: "jose"}

# 因為剛剛定義 user map 時,把 username 打錯了
iex(3)> user.username
** (KeyError) key :username not found in: %{password: "elixir", usernmae: "jose"}

# 如果用 User struct 就不會發生這個問題,還將其他欄位都填上預設值 nil
iex(3)> jose = %User{name: "Jose"}
%Rumbl.User{id: nil, name: "Jose", password: nil, username: nil}
iex(4)> jose.name
"Jose"

# 編譯時就會檢查欄位名稱
iex(5)>  chris = %User{nmae: "chris"}
** (KeyError) key :nmae not found in: %Rumbl.User{id: nil, name: nil, password: nil, username: nil} ....

# struct 就跟 map 一樣,差別只在於 struct 是一個擁有 __struct__ key 的 map
iex(5)> jose.__struct__
Rumbl.User

Working with Repositories

Repository 是一組儲存資料的 API,可快速建立測試資料的 data interface,待完成 view, template 後,就可以替換為完整的 database-backend repository。換句話說,可以將 data model 跟 database 完全分離。

修改 /lib/rumbl/repo.ex

defmodule Rumbl.Repo do
  #  use Ecto.Repo, otp_app: :rumbl

  # 所有 User 的資料
  def all(Rumbl.User) do
    [
      %Rumbl.User{id: "1", name: "Jose", username: "josie", password: "elixir"},
      %Rumbl.User{id: "2", name: "Bruce", username: "bruce", password: "pass"},
      %Rumbl.User{id: "3", name: "Chris", username: "chris", password: "phx"}
    ]
  end

  def all(_module), do: []

  # get user by id
  def get(module, id) do
    Enum.find all(module), fn map -> map.id == id end
  end

  # get user by a custom attribute
  def get_by(module, params) do
    Enum.find all(module), fn map ->
      Enum.all?(params, fn {key, val} -> Map.get(map, key) == val end)
    end
  end

end

將 /lib/rumbl.ex,關掉跟 Ecto 相關的 supervisor

#      supervisor(Rumbl.Repo, []),

檢查 User 的測試資料庫

$ iex -S mix
iex(1)> alias Rumbl.User
Rumbl.User
iex(2)> alias Rumbl.Repo
Rumbl.Repo

iex(3)> Repo.all User
[%Rumbl.User{id: "1", name: "Jose", password: "elixir", username: "josie"},
 %Rumbl.User{id: "2", name: "Bruce", password: "pass", username: "bruce"},
 %Rumbl.User{id: "3", name: "Chris", password: "phx", username: "chris"}]
 
iex(4)> Repo.all Rumbl.Other
[]

iex(5)> Repo.get User, "1"
%Rumbl.User{id: "1", name: "Jose", password: "elixir", username: "josie"}

iex(6)> Repo.get_by User, name: "Bruce"
%Rumbl.User{id: "2", name: "Bruce", password: "pass", username: "bruce"}

撰寫 Controller & View

修改 /web/router.ex,將 /users 及 /users/:id 對應到 UserController 的 :index 及 :show 兩個 functions

  scope "/", Rumbl do
    pipe_through :browser # Use the default browser stack

    get "/users", UserController, :index
    get "/users/:id", UserController, :show
    get "/", PageController, :index
  end

新增 /web/controllers/user_controller.ex

defmodule Rumbl.UserController do
  use Rumbl.Web, :controller

  def index(conn, _params) do
    users = Repo.all(Rumbl.User)
    render conn, "index.html", users: users
  end

end

/web/views/user_view.ex

defmodule Rumbl.UserView do
  use Rumbl.Web, :view

  alias Rumbl.User
  def first_name(%User{name: name}) do
    name
    |> String.split(" ")
    |> Enum.at(0)
  end

end

/web/templates/user/index.html.eex

<h1>Listing Users</h1>
<table class="table">
    <%= for user <- @users do %>
    <tr>
        <td><b><%= first_name(user) %></b> (<%= user.id %>)</td>
        <td><%= link "View", to: user_path(@conn, :show, user.id) %></td>
    </tr>
    <% end %>
</table>

因為剛剛有修改 /lib/rumbl/Repo.ex,因為 lib 目錄更新程式後不會自動 reload,記得要重新啟動 phoenix.server,修改後的程式才會有作用。

mix phoenix.server

Using Helpers

link function 封裝了很多有用的 functions,可用來處理很多 HTML structures。

link 的第二個參數是 keyword list

iex(1)> Phoenix.HTML.Link.link("Home", to: "/")
{:safe, [60, "a", [[32, "href", 61, 34, "/", 34]], 62, "Home", 60, 47, "a", 62]}

iex(2)> Phoenix.HTML.Link.link("Delete", to: "/", method: "delete")
{:safe,
 [60, "a",
  [[32, "data-csrf", 61, 34,
    "DSMgIAMOCzQqQTczRR0/ElEUFDIJJgAA8KZynKSYp9CZ0/MYaNCwDA==", 34],
   [32, "data-method", 61, 34, "delete", 34], [32, "data-to", 61, 34, "/", 34],
   [32, "href", 61, 34, "#", 34], [32, "rel", 61, 34, "nofollow", 34]], 62,
  "Delete", 60, 47, "a", 62]}

HTML helper 會放在每個 view 的最上層,Phoenix.HTML 負責處理 HTML functions in views

/web/web.ex 裡面 view 的區塊內容為

  def view do
    quote do
      use Phoenix.View, root: "web/templates"

      # Import convenience functions from controllers
      import Phoenix.Controller, only: [get_csrf_token: 0, get_flash: 2, view_module: 1]

      # Use all HTML functionality (forms, tags, etc)
      use Phoenix.HTML

      import Rumbl.Router.Helpers
      import Rumbl.ErrorHelpers
      import Rumbl.Gettext
    end
  end

ref: Phoenix.HTML Helpers for working with HTML strings and templates.

不能把自己做的 function 直接寫在 web.ex,要用 import 的方式處理

Showing a User

剛剛在 /web/router.ex 有定義這個 url route

get "/users/:id", UserController, :show

修改 /web/controllers/user_controller.ex,增加 show function,注意參數的部分為 %{"id" => id},這是從網址來的

defmodule Rumbl.UserController do
  use Rumbl.Web, :controller

  def index(conn, _params) do
    users = Repo.all(Rumbl.User)
    render conn, "index.html", users: users
  end

  def show(conn, %{"id" => id}) do
    user = Repo.get(Rumbl.User, id)
    render conn, "show.html", user: user
  end
end

新增 /web/templates/user/show.html.eex

<h1>Showing User (<%= @user.id %>)</h1>
<b><%= first_name(@user) %></b> (<%= @user.id %>)'s username is <b><%= @user.username %></b>

http://localhost:4000/users/1


Naming Conventions

當 Phoenix 在 controller 要 render templates 時,他會使用 controler module 名稱 Rumbl.UserController 參考到 view module 的名稱為 Rumbl.UserView,template 目錄在 /web/templates/user/。

在後面可知道如何 customize 這些 conventions


Nesting Templates,共用 template

新增 /web/templates/user/user.html.eex

<b><%= first_name(@user) %></b> (<%= @user.id %>)'s username is <b><%= @user.username %></b>

修改 /web/templates/user/show.html.eex

<h1>Showing User (<%= @user.id %>)</h1>
<%= render "user.html", user: @user %>

修改 /web/templates/user/index.html.eex

<h1>Listing Users</h1>
<table class="table">
    <%= for user <- @users do %>
    <tr>
        <td><%= render "user.html", user: user %></td>
        <td><%= link "View", to: user_path(@conn, :show, user.id) %></td>
    </tr>
    <% end %>
</table>

<%= render "user.html", user: user %> 這就是共用的部分

iex -S mix 測試 view

$ iex -S mix
iex(1)> user = Rumbl.Repo get Rumbl.User, "1"
** (SyntaxError) iex:1: syntax error before: get

iex(1)> user = Rumbl.Repo.get Rumbl.User, "1"
%Rumbl.User{id: "1", name: "Jose", password: "elixir", username: "josie"}
iex(2)> view = Rumbl.UserView.render("user.html", user: user)
{:safe,
 [[[[[[["" | "<b>"] | "Jose"] | "</b> ("] | "1"] | ")'s username is <b>"] |
   "josie"] | "</b>"]}
iex(3)> Phoenix.HTML.safe_to_string(view)
"<b>Jose</b> (1)'s username is <b>josie</b>"

每個 template 最後都會變成 render(template_name, assigns),因此 rendering template 就是 template name 及 function 的 pattern matching。

我們可以跳過整個 template 機制,自訂自己的 render clause,這也是 Rumbl.ErrorView 提供的自訂錯誤頁面的方式。

/web/views/error_view.ex

defmodule Rumbl.ErrorView do
  use Rumbl.Web, :view

  def render("404.html", _assigns) do
    "Page not found"
  end

  def render("500.html", _assigns) do
    "Internal server error"
  end

  # In case no render clause matches or no
  # template is found, let's render it as 500
  def template_not_found(_template, assigns) do
    render "500.html", assigns
  end
end

Phoenix.View module 也提供了 render view 的 functions,包含 a function to render 及 轉換 rendered template 為 string的 function。

Phoenix.View 會呼叫 view 的 render functions。

iex(5)> user = Rumbl.Repo.get Rumbl.User, "1"
%Rumbl.User{id: "1", name: "Jose", password: "elixir", username: "josie"}


iex(6)> Phoenix.View.render(Rumbl.UserView, "user.html", user: user)
{:safe,
 [[[[[[["" | "<b>"] | "Jose"] | "</b> ("] | "1"] | ")'s username is <b>"] |
   "josie"] | "</b>"]}


iex(7)> Phoenix.View.render_to_string(Rumbl.UserView, "user.html", user: user)
"<b>Jose</b> (1)'s username is <b>josie</b>"

Layouts

在 controller 呼叫 render 時,controller 會先 render the layout view,然後再依照 predefined markup 去 render the actual template

因為 layout 也是 view with templates,每個 template 會收到一些特定的變數,

查看 /web/templates/layout/app.html.eex 的內容有一段 <%= render @view_module, @view_template, assigns %>1

layouts 就是 HTML templates that embed an action’s HTML

  <body>
    <div class="container">
      <header class="header">
        <nav role="navigation">
          <ul class="nav nav-pills pull-right">
            <li><a href="http://www.phoenixframework.org/docs">Get Started</a></li>
          </ul>
        </nav>
        <span class="logo"></span>
      </header>

      <p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
      <p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>

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

    </div> <!-- /container -->
    <script src="<%= static_path(@conn, "/js/app.js") %>"></script>
  </body>

References

Programming Phoenix

Using MySQL with the Phoenix Framework