0

Làm quen với Elixir on Phoenix qua phương trình bậc 2

bài trước chúng ta đã lướt qua được cách cài đặt và khởi động được server của Elixir trên Phoenix. Đối với những tài liệu ở bài trước thì các bạn hoàn toàn có thể tìm hiểu được chi tiết qua các đề mục đó. Nhưng với mình thì việc tìm hiểu một cách nhanh nhất và hiệu quả nhất là bắt tay vào làm một trang web và tìm được những gì mình thực sự cần chứ không phải đọc một cách lan man. Chính vì vậy, ở bài này mình sẽ làm một trang web đơn về việc giải phương trình bậc 2. Trang web này có 1 trang chính giới thiệu rồi link tới trang điền thông số để tính toán.

I. Tạo trang introduction

Chúng ta đã có thể vào localhost và hiển thị trang chủ. Đó chính là trang default được định nghĩa sẵn.

Để có thể có một trang web như vậy thì ta cần phải làm các bước sau:

  1. Tạo controller có action nào đó
defmodule HelloPhoenix.PageController do
  use HelloPhoenix.Web, :controller

  def index(conn, _params) do
    render conn, "index.html"
  end

  def introduction(conn, _params) do
    render conn, "introduction.html"
  end
end

Ở đây mình đã tạo thêm action introduction trong PageController (/web/controllers/page_controller.ex). Trong đó hàm index chính là link tới trang chủ.

  1. Tạo file view tương ứng với action đó. View thì được đặt tại thư mục web/templates/controller với trang introduction thì sẽ là web/templates/page/introduction.html.eex
<h2>Lê Văn Ban</h2>
Chương trình tính giải phương trình bậc 2: <img src="<%= static_path(@conn, "/images/ptb2.png") %>">
Nhập vào các giá trị a,b,c rồi ấn nút Giải</br>
Chương trình mang tính chất giải trí và làm quen với Elixir on Phoenix</br>
</br>
Ấn <%= link "vào đây", to: log_path(@conn, :index), class: "btn btn-default" %> để đến chương trình giải phương trình bậc 2
  1. Tạo routes

Để chỉ đường dẫn trang web tương ứng với action nào thì chúng ta phải đặt chúng tại file web/router.ex

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

    get "/", PageController, :index
    get "introduction", PageController, :introduction
  end

Ta thêm get "introduction", PageController, :introduction vào trong scope "/" để link đường dẫn /introduction vào action trong PageController.

Ở view web/templates/page/introduction.html.eex các bạn thấy được ta sử dụng các hàm của Phoenix trong view chính là được đặt trong dấu <%= ... %> hoặc <% .. %> điều này cũng tương tự như trong rails. Nhưng với Phoenix thì ta chỉ sử dụng dấu " chứ không sử dụng được ' như rails

  • "<%= static_path(@conn, "/images/ptb2.png") %>" => OK
  • "<%= static_path(@conn, '/images/ptb2.png') %>" => EROR Để sử dụng đường dẫn static_path này với các file ảnh, js hay css thì ta thường đặt trong thư mục web/static và phải định nghĩa chúng ở trong file lib/hello_phoenix/endpoint.ex
plug Plug.Static,
    at: "/", from: :hello_phoenix, gzip: false,
    only: ~w(css fonts images js favicon.ico robots.txt framgia.jpg)

Với file ptb2.png thì được đặt trong thư mục images nên không cần phải thêm. Còn file framgia.jpg thì đặt ở trong thư mục asset nên ta phải thêm vào danh sách trên. Nó cũng gần giống với asset preconpile của rails.

Đối với Phoenix thì routes có khác biệt một chút so với rails.

  • Rails sử dụng hàm: introduction_page_path
  • Phoenix sử dụng hàm: page_path(@conn, :introduction) Như vậy với Phoenix thì thường sẽ là dùng tên controller để gọi hàm và truyền tên action vào chứ không sinh sẵn path như rails.

II. Tạo controller, model, view

Với rail thì ta có rails generate controller (model) còn với Phoenix thì ta có thử sử dụng

  • mix phoenix.gen.html tạo ra cả html, controller, model với đủ 7 action theo chuẩn
  • mix phoenix.gen.model chỉ tạo model

Tham khảo tại đây

Ví dụ khi tạo controller với model và view Log để lưu lại các phương trình bậc 2 thì ta sử dụng lệnh sau

mix phoenix.gen.html Log logs a:integer b:integer c:integer result:text

Khi đó các file sẽ được tạo tự động và ta chỉ cần vào chỉnh sửa và điều hướng theo ý của mình là xong.

Với rails thì ta hay sử dụng f.number hay f.lable còn với Phoenix thì lại sử dụng như sau:

<%= form_for @changeset, @action, fn f -> %>
    <%= label f, :a, class: "control-label" %>
    <%= number_input f, :a, class: "form-control" %>
<% end %>

Điểm giống nhau giữa 2 cái là đều có form_for với end để bắt đầu và kết thúc một block.

Trong rails ta có active-record để gọi vào database lấy các đối tượng qua model với phoenix ta có Ecto Trong file web/web.ex có định nghĩa như sau:

  def model do
    quote do
      use Ecto.Schema

      import Ecto
      import Ecto.Changeset
      import Ecto.Query
    end
  end

  def controller do
    quote do
      use Phoenix.Controller

      alias HelloPhoenix.Repo
      import Ecto
      import Ecto.Query

      import HelloPhoenix.Router.Helpers
      import HelloPhoenix.Gettext
    end
  end

Như vậy ở trong view ta sử dụng Repo để gọi các hàm tương ứng lấy dữ liệu từ trong database ra. VD:

logs = Repo.all(Log) # Lấy tất cả
log = Repo.get(Log, 2) # Log với id = 3

Chi tiết tại ecto

Model Log

defmodule HelloPhoenix.Log do
  use HelloPhoenix.Web, :model

  schema "logs" do
    field :a, :integer
    field :b, :integer
    field :c, :integer
    field :result, :string

    timestamps()
  end

  @doc """
  Builds a changeset based on the `struct` and `params`.
  """
  def changeset(struct, params \\ %{}) do
    struct
    |> cast(params, [:a, :b, :c, :result])
    |> validate_required([:a, :b, :c, :result])
    |> validate_exclusion(:a, [0], message: "a phải khác 0")
  end
end

Ở phoenix ta thấy ngay cả cấu trúc schema của bảng ở đây, và ta cũng định nghĩa luôn các attributes được cài đặt và validate trong hàm changeset. => Để tạo một đối tương log mới tương tự với Log.new(a: 1,...) như rails thì với Phoenix ta dùng phương thức sau:

changeset = Log.changeset(%Log{}, %{a: 1, b: 0, c: 0})
Repo.insert(changeset) # Lưu object đó vào database

Ví dụ với hàm create trong log_controller:

def create(conn, %{"log" => log_params}) do
    changeset = Log.changeset(%Log{}, log_params)
    {a, _} = Integer.parse(log_params["a"])
    {b, _} = Integer.parse(log_params["b"])
    {c, _} = Integer.parse(log_params["c"])
    delta = b * b - 4 * a * c
    case delta do
      del when del < 0 ->
        log_params = Map.merge(log_params, %{"result" => "Phương trình vô nghiệm"})
      del when del == 0 ->
        log_params = Map.merge(log_params, %{"result" => "Phương trình có nghiệm kép x1 = x2 = #{-b / (2 * a)}"})
      _ ->
        :math.sqrt(delta)
        log_params = Map.merge(log_params, %{"result" => "Phương trình vô nghiệm"})
    end
    changeset = Log.changeset(%Log{}, log_params)
    case Repo.insert(changeset) do
      {:ok, _log} ->
        conn
        |> put_flash(:info, "Log created successfully.")
        |> redirect(to: log_path(conn, :index))
      {:error, changeset} ->
        render(conn, "new.html", changeset: changeset)
    end
  end

Như các bạn thấy thì log_params tương tự như params ở rails. Và nó chỉ chứ các giá trị được gửi từ form lên. Nó là một dạng Map nó gần giống như hash trong ruby, nó cũng có key và value nhưng cách sử dụng không được linh hoạt bằng ruby.

Để thay đổi hay thêm giá trị của 1 key nào đó ta thường dùng Map.merge(params1, params2) Khi các key của params2 trùng với params1 thì nó sẽ được lấy giá trị của params2.

Như vậy ta đã làm quen với các khái niệm model, controller, view, routes, kiểu dữ liệu map và case when...

Qua bài viết này các bạn có thể tạo được một trang web cơ bản thêm, sửa, xóa, thống kê với database, thêm ảnh trong asset và gọi ra trên view... Bài viết sau sẽ đi sâu thêm vào các phần. Hi vọng các bạn sẽ theo dõi 😄

Video demo

Source code: tại đây

III. Tài liệu tham khảo


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí