In part one of this series, we introduced the core generated components when bootstrapping a new Phoenix project. We used a button and a modal from the core components to lay the groundwork for a "create modal".
In this post, we will put a form onto the modal and create pets.
Let's get started!
Note: As in the last post, you can follow along with our companion repo.
Adding a Form to Our Phoenix Application
There is a simple form included in PetacularWeb.CoreComponents
. This expects two slots: an :inner_block
, which will be our form input components, and :actions
, which will be the submit buttons.
The :actions
slot is a named slot, meaning, we can render
the components we want to use as buttons inside of an <:actions>
tag, like this:
The input components are also in PetacularWeb.CoreComponents
and produce the correct kind of input based on the attrs
used to define them. We just need a text field for now, so we can define the text input like so:
If you don't define a type
attr, it defaults to a text field. The @create_form
is created from
a changeset (as we will see in a moment).
Phoenix's Component to_form
Let's take a moment to talk about forms and changesets. We used to pass changesets directly
to forms. But in Phoenix 1.7, a new to_form
function generates a Phoenix.HTML.Form
struct from the given changeset.
This seemingly small change is a massive improvement, so it's worth talking about. First, it
provides us with some indirection. Calling to_form
ensures that what the <.form
component
sees is a Phoenix.HTML.Form
struct, rather than a changeset. That means if we wanted to swap away from a changeset and use something else in the future (like a bare
map of params), we could — with no changes to the <.form
component itself. We just change
the places we call to_form
in mount
and handle_event
.
Next, the Phoenix.HTML.Form
handles change tracking better. Up until Phoenix 1.7, if a form field
changed, the whole form was re-rendered. Usually that's fine, but if you have a large complex
form or dropdowns with lots of options, it can be a big boon not to have to
re-render all of that each time you change a field.
Finally, it also simplifies the syntax. When we passed changesets through to forms, we used the :let
attribute to assign the changeset to a variable we could refer to. Now
we can omit that entirely. We can also refer to the value of a form field more simply; previously,
you would have done something like: value={Ecto.Changeset.fetch_field!(@changeset, :my_field)}
.
With the form struct, it becomes: value={@form[:my_field].value}
A full before and after might look like this.
Before:
And after:
Note how we don't need HTML.text_input
anymore, and we don't have to reach into the changeset.
Check out the official Phoenix docs for more information.
Using to_form
First, set up a form in mount
:
Now we can build a form and use the pre-built inputs. These accept the field from the form
(which is as easy as field={@create_form[:name]}
).
Saving the Form
If we save and refresh the form, we should see that we can click a button and enter a name into it. The final thing to do to get this working is to write an event handler for the form submission.
Here is the first gotcha. We are using changesets to validate our forms. Because live views
are stateful, we might be tempted to re-use the changeset in our assigns when we submit the
form. This is not a good idea. What's worse, Ecto.Changeset.cast
et al. will happily accept
a changeset as input, meaning it's a very easy trap to fall into without realizing you are
doing something wrong. The symptom might be errors in your changeset not
disappearing as you expect, for example.
This trap is harder to fall into if you use to_form
because the changeset is not in
the assigns — the form struct is. This is another good reason to use to_form
!
So every time we want to cast some params, we must create a fresh changeset. Let's do that:
In the case of an error, we add a flash that shows it to the user, and when we successfully complete, we need to do two things:
- Show the created pet.
- Close the modal.
To show the new pet, we first need to render all pets on the page. So we will fetch them all from the DB in mount and then add some code to render them:
Now that we can see them, we can refresh the list of pets when we add one successfully:
This is great. If we try adding a pet, we will see the pet gets created and appears on the page, but the modal remains open.
Closing the Modal with push_event
Once we have successfully added a pet, we want to close
the modal. There is already a "close the modal" button on the modal; the little X
in
the top right-hand corner. What if we could just target that?
Well, we can! There is a function called push_event
that lets us emit a JS event from the backend. We can add some JavaScript that listens for
that event and triggers a "click" event for the close button, effectively closing the modal.
First, we call push_event
in the event handler:
Now to react to the event, we have two options:
- Add a global event listener for the event.
- Use a hook.
Let's try the second approach. First, we'll wire up the hook in app.js
:
Now let's put the hook onto the close modal button in PetacularWeb.CoreComponents
(remember,
anything that has a hook also needs an ID). To help us later cater for multiple modals on one
page, let's use the required @id
attr as part of the id:
You can see all the changes in this commit. Fantastic, we now have a working create modal! 🎉
Wrapping Up
In the second part of this series, we successfully added a form to the modal that creates new pets.
Stay tuned for the third and final part, where we will edit an existing pet.
Until then, happy coding!
P.S. If you'd like to read Elixir Alchemy posts as soon as they get off the press, subscribe to our Elixir Alchemy newsletter and never miss a single post!