I kind of wanted to title this post, “how to create a form that doesn’t use a model” but Ecto decided to drop “Model” in favor of “Schema”; same thing, different mindset.

Sometimes you just want to collect some data from a user but have no plans of persisting it to the database. You can use embedded_schema to accomplish this. You’ll get to mostly work with Phoenix/Ecto as usual except without the migration, tables, and saving.

In this post we’ll be creating a Phoenix 1.4 app with a contact form.

Create the Phoenix 1.4 app

The following command will

  • create a new Phoenix app called Contact
  • install its dependencies
  • create the database
  • open your browser to http://localhost:4000
  • start the webserver

A couple things to note:

  1. You will told the directory already exists and asked if you want to continue: Type y and press Enter
  2. You will be asked if you want to “Fetch and install dependencies?”: Type y and press Enter
  3. The browser will likely tell you the page isn’t found since it tried to load the site before the webserver finished starting. Simply wait a second or two and then reload the page.
mkdir contact ; cd contact ; mix phx.new . --app contact ; mix ecto.create ; open http://localhost:4000 ; mix phx.server

You should be greeted with the Phoenix welcome screen.

Phoenix Welcome

Create the Ecto.Schema

Create a new file \lib\contact\message.ex with the following contents:

defmodule Contact.Message do
  use Ecto.Schema
  import Ecto.Changeset

  embedded_schema do
    field :name
    field :email
  end

  def changeset(message, attrs) do
    message
    |> cast(attrs, [:name, :email])
    |> validate_required([:name, :email])
  end
end

Notice we used embedded_schema here. This let’s Ecto know that we don’t plan on saving this schema to the database.

Creating the form

Open \lib\contact_web\templates\page\index.html.eex and replace with the following:

<div class="section">
   <p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
  <%= form_for @changeset, Routes.page_path(@conn, :contact), [method: "post"], fn f -> %>
  <%= if @changeset.action do %>
  <div class="alert alert-danger">
      <p>Oops, you might have missed a field! Please check the errors below.</p>
  </div>
  <% end %>
  <div class="field">
      <%= label f, :name %>
      <div class="control">
          <%= text_input f, :name, [class: "input is-large", placeholder: "Your name"] %>
          <%= error_tag f, :name %>
      </div>
  </div>
  <div class="field">
      <%= label f, :email, class: "label is-large" %>
      <div class="control">
          <%= text_input f, :email, [class: "input is-large", type: "email", placeholder: "Your email"] %>
          <%= error_tag f, :email %>
      </div>
  </div>
  <div class="control">
      <%= submit "Send",  class: "button is-primary is-large" %>
  </div>
  <% end %>     
</div>          

Hooking up the controller

Open lib\contact_web\controllers\page_controller.ex and replace with the following:

defmodule ContactWeb.PageController do
  use ContactWeb, :controller
  alias Contact.Message

  def index(conn, _params) do
    changeset = Message.changeset(%Message{}, %{})
    render(conn, "index.html", changeset: changeset)
  end

  def contact(conn, %{"message" => message_params}) do
    changeset =
      %Message{}
      |> Message.changeset(message_params)

    if changeset.valid? do
      # Do something with message_params

      conn
      |> put_flash(:info, "Message sent!")
      |> redirect(to: Routes.page_path(conn, :index))
    else
      changeset = %{changeset | action: :index}
      render(conn, "index.html", changeset: changeset)
    end
  end
end