Background
I have a website Foxtail Consulting which I converted from Wordpress to a Phoenix website with --no-ecto
and hosted on a $5/mo Digital Ocean Droplet. I wanted to add a simple contact form that did not require a database. Because the website is built using Phoenix LiveView I decided to use a live_component
but needed a simple captcha to make any attempt at spamming the form fail (NOTE: I know this solution is not infallable but wanted to play around with live_components
).
Process
First I created a context
called contact
and created an embedded_schema
(No database remember).
defmodule MyApp.Contact.Message do
import Ecto.Changeset
use Ecto.Schema
embedded_schema do
field(:name, :string)
field(:email, :string)
field(:text, :string)
field(:answer, :integer)
end
def changeset(message, attrs \\ %{}) do
message
|> cast(attrs, [:name, :email, :text, :answer])
|> validate_required([:name, :email, :text, :answer])
|> validate_format(:email, ~r/^[^\s]+@[^\s]+$/)
|> validate_length(:email, max: 160)
end
end
I also created a Mailer
module and a Mail
module in the context
to handle sending the email directly to my inbox. For my website I am using Swoosh, an excellent email library with lots of adapters.
I then created a new directory under the live
folder called components
and created the component file itself.
live
|_ components
|_ message_component.ex
defmodule MyAppWeb.Components.MessageComponent do
use MyAppWeb, :live_component
alias MyApp.Contact.{Message, Mail}
@catcha_notations ["+", "x"]
@impl true
def render(assigns) do
~L"""
<%= live_flash(@flash, :success) %>
<%= live_flash(@flash, :error) %>
<%= f = form_for
@changeset,
"#",
phx_target: @myself,
phx_change: "validate",
phx_submit: "send_message"
%>
<%= text_input f, :name, [phx_debounce: "blur"] %>
<%= error_tag f, :name %>
<%= text_input f, :email, [phx_debounce: "blur"] %>
<%= error_tag f, :email %>
<%= textarea f, :text, [phx_debounce: "blur"] %>
<%= error_tag f, :text %>
<div>
<%= @v1 %> <%= @operation %> <%= @v2 %> =
<%= text_input f, :answer, [phx_debounce: "blur"] %>
</div>
<%= error_tag f, :answer %></span>
<%= submit "Send Message", phx_disable_with: "Saving ...." %>
</form>
"""
end
def mount(_assigns, socket) do
{:ok, socket}
end
@impl true
def update(assigns, socket) do
changeset = Message.changeset(%Message{})
{:ok,
socket
|> assign(assigns)
|> assign(:changeset, changeset)
|> assign_captcha()}
end
@impl true
def handle_event( "send_message",%{ "message" => message}, socket) do
changeset =
%Message{}
|> Message.changeset(message)
|> Map.put(:action, :validate)
%{"name" => name, "email" => email, "text" => message, "answer" => answer} = message
cond do
!captcha?(socket.assigns.operation, answer, socket) ->
{:noreply,
socket
|> clear_flash()
|> put_flash(:error, "Doh! No good at math? Try again")
|> assign(:changeset, changeset)
|> assign_captcha()
}
changeset.valid? && captcha?(socket.assigns.operation, answer, socket) ->
case Mail.send(name, email, message) do
{:ok, _msg} ->
{:noreply,
socket
|> put_flash(:success, "Your message is on its way!")
|> assign(:changeset, Message.changeset(%Message{}))
|> assign_captcha()}
{:error, _msg} ->
{:noreply,
socket
|> clear_flash()
|> put_flash(:error, "Whoops! Please check your details")
|> assign(:changeset, changeset)
|> assign_captcha()}
end
true ->
{:noreply,
socket
|> assign(:changeset, changeset)
|> assign_captcha()}
end
end
@impl true
def handle_event("validate", %{"message" => message}, socket) do
changeset =
%Message{}
|> Message.changeset(message)
|> Map.put(:action, :validate)
{:noreply,
socket
|> assign(:changeset, changeset)
|> clear_flash()
}
end
defp assign_captcha(socket) do
socket
|> assign(:v1, :rand.uniform(9))
|> assign(:v2, :rand.uniform(9))
|> assign(:operation, Enum.random(@catcha_notations))
end
defp captcha?(_, "", _), do: false
defp captcha?("x", answer, %{assigns: %{v1: v1, v2: v2}}) do
cond do
v1 * v2 != String.to_integer(answer) -> false
true -> true
end
end
defp captcha?("+", answer, %{assigns: %{v1: v1, v2: v2}}) do
cond do
v1 + v2 != String.to_integer(answer) -> false
true -> true
end
end
defp captcha?(_, _answer, _socket), do: false
end
There are some notable parts for the message_component.ex
. Ensure that you use use MyAppWeb, :live_component
in your file and in the form include phx_target: @myself
. This will ensure that the events captured by the parent LiveView
will be passed to the components handle
functions. Additionally, because the component is stateful when you embed it in the parent LiveView
be sure to add and id
.
<%= live_component( @socket, MayAppWeb.Components.MessageComponent, id: :message) %>
Apart from that the setup is pretty straight forward. When the user submits the form (assuming they have passed through the validation event) I get the answer provided together with the operation and test it using the values recorded in the assigns. If the simple sum is correct I attempt to send their message via email.
Feel free to test it out and send me a message from my website if you have any feedback - Foxtail Consulting