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
and255
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 thenew.html.heex
page and can move the content of the notification formform.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
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.