CRUD API & Phoenix Generators

DANA 325 R C T H S X A Q G O B L U M P K I D W E F V N

Objectives

not yet graded
  • Messages can be created, deleted and listed in one form or another. (3 Points)

  • Contact Me Form implemented via a modal or drawer component in the app layout (2 Points)

  • Form shows correct error messages for wrong input (see instructions below) (1 Point)

  • Adapted tests (and removed failing / unused tests) to allow for 100% test coverage on the Notification context and at least one test that triggers a form submission. (1 Point)

In the video you have been introduced to the unlimited power of the dark side of the force aka Phoenix Generators. A single bash command saved us from creating 11+ files or ~300+ lines of code that we otherwise would have to create ourselves. We are left with checking, fixing, expanding and improving what was automated.

In this lecture we would like to apply what we have been teasered with to create a contact me form.

Before doing anything else, commit your branch to git. If you mess up you can undo the following changes and revert back to here via git reset --hard.

Let's find a contact form on flowbite that we use to get a sense for what we are trying to achive.

At the very least we will need a column for the message. We could optionally provide fields for email and subject, so that we can contact the user back.

We will want to explore Ecto's Changeset section and add a few validate_* functions, such that:

  • Email field contains an @ symbol and is required
  • Subject field is optional and can have a maximum length of 30 characters
  • Message field is required and must have between 5 and 255 characters.

The proper way to use those validate functions is to pipe them into the changeset/2 function of our message.ex schema file:

def changeset(model, attrs) do
  model
  |> cast(attrs, [:x, :y])
  |> validate_required([:y])
  |> validate_number(:x, greater_than: 2, less_than: 10)
  |> validate_inclusion(:y, ["A", "B", "C"])
end

The example above would validate that x is a number between 2 and 10 and y is either "A", "B", or "C" and required

Because we haven't learned about transactional emails yet submitting the form will not send an email to us but rather populate a table that we can look up for entries.

This scenario really screams at us to use a Phx Generator:

mix phx.gen.html Notification Message messages email:string subject:string message:string

Go ahead and run it. Now lets plan ahead a bit:

  • we don't ever want to edit/update messages written by the users so those actions and their context functions should probably be removed. Delete is useful for spam control so lets keep that functionality.
  • we want our create form on our website, ideally in our app.html.heex layout, accessible via a drawer through a button click (see flowbite's show contact form hide/show button), or alternatively if you are not intersted in creating the drawer functional component, via the modal component you already created in the last homework. Because we show the contact form via the app layout we don't need the new.html.heex page and can move the content of the notification form form.html.heex directly into the app layout.
  • we probably want to fix/remove unused tests after we are done and have the functional contact me form
  • we need to tweak the router to only include actions we actually add.

When you compare and merge the form code from flowbite and the phx generate note that you will want to give priority to the phoenix components and simply adapt the styling of Flowbite:

<!-- from Flowbite -->
<form class="mb-6">

+

<.simple_form :let={f} for={@changeset} action={@action}>

=

<.simple_form :let={f} for={@changeset} action={@action} class="mb-6">

Another tip regarding making @changeset available in app.html.heex:

The original new action looked like:

def new(conn, _params) do
  changeset = Bucknell.change_course(%Course{})
  render(conn, :new, changeset: changeset)
end

As you can see we forward the changeset to the view. Now this time we will need to have the changeset available on all pages (as the app layout will be used by all pages). One way to achive this is to plug a function to the browser pipeline that will add that changeset to every route that is using said pipeline:

defmodule AppWeb.Router do
  use AppWeb, :router

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_live_flash
    plug :put_root_layout, html: {AppWeb.Layouts, :root}
    plug :protect_from_forgery
    plug :put_secure_browser_headers
    plug AppWeb.CustomPlug # <-- ADD THIS
  end

  # ...
end

You can now create the module:

defmodule AppWeb.CustomPlug do
  import Plug.Conn

  alias App.Notifications
  alias App.Notifiactions.Message

  def init(default), do: default

  def call(conn, _opts) do
    changeset = Notifications.change_message(%Message{})
    assign(conn, :message_changeset, changeset)
  end
end

The combination of init/1 and call/2 is recognized to be a plug.

Don't forget to change the assign from @changeset to @message_changeset in your app.html.heex template:

<.simple_form :let={f} for={@message_changeset} action={@action} class="mb-6">

I'll leave the finer details of the implementation up to you so check the objectives section for what you will earn points.

One more tip related to putting a form into a modal/drawer component. If you created your modal/drawer component correctly with a slot :inner_block attribute, all code between and will be passed into the component, including your form:

You might be ending up with something like this in your app.html.heex file:

<.button phx-click="open-modal">Contact me</.button>

<.modal ...>
  <div ...>
    <div ...>
      <.simple_form ...>
        ...
      </.simple_form>
    </div>
  </div>
</.modal>

Since we can (for now) only have one modal/drawer component at a time you can repurpose your previous modal or create a new function component drawer which opens a sidebar with the form.

Copyright © 2025 Alexander Fuchsberger, Bucknell University. All rights reserved.