How to (Part 3): Swap Registration Flow to a Phoenix Live View With phx_gen_auth — Add a Phone Number

Geek Culture
Published in
5 min readMar 15, 2021


Sneak peek at Metamorphic’s phone number registration courtesy Phoenix Live View. © 2021 Moss Piglet.


I’m currently developing a social connection web application, called Metamorphic and have been sharing brief guides on how to implement certain features, with the hope that it helps others trying to do similarly.

For this part, I wanted to include a phone number in the registration flow (with support for international numbers). I also wanted to avoid overly complicated solutions while still delivering a smooth experience (albeit you can get, perhaps, the type of experience you are looking for with much more sophisticated options).


To make following along easier, we will assume you have read Part 1 and Part 2, where we swapped the “dead view” over to a Live View for registration and implemented a multi-step form, respectively.

That being said, the same basic prerequisites as Part 1 apply:

  • You have a working Elixir/Phoenix app setup with the live generator (or add the appropriate code — see the docs).
  • You have installed and setup phx_gen_auth (version 0.6.0) using mix phx.gen.auth Accounts Person people (you can substitute other names).
  • You are using phoenix_live_view 0.15.4.
  • You are using tailwindcss or understand your own CSS tooling.
  • Optional: You have setup email confirmation with Bamboo.

With a few new prerequisites:

  • You have a respective phone_number field.
  • Optional: You have transparent encryption at rest using cloak_ecto.
  • Optional: You have AlpineJS installed and configured properly.

Your phone_number field schema and migration might look something like this (the encryption is optional):

# your_app.accounts.personalias YourApp.Encrypted...
field :phone_number, Encrypted.Binary, redact: true
# your_app.repo.migrations.create_people_auth_tables.exs...
add :phone_number, :binary, null: false

Again, the encryption is optional (though a good practice to make standard).

Let’s Begin

So, at this point, we’ve got a working multi-step Live View registration page (per Part 2) and we need to now add a step for a phone number.

This means that we are going to be adding a new dependency to our application, creating a phone_number.ex module, and working in the following files: your_app.accounts.person.ex, your_app_web/templates/person_registration/new.html.leex, your_app_web/live/, and apps/your_app_web/assets/js/app.js.

If you’re working in an umbrella project, then you just have a few additional directories to work through on your file paths (the file path for the app.js file is an example of what it could look like in an umbrella project).

Step 1

Let’s start by adding our new dependency, ex_phone_number:

# mix.exsdefp deps do
{:ex_phone_number, "~> 0.2"},

Then, run mix deps.get to install.

Step 2

Let’s create our phone_number.ex module now:

You’ll notice that we added an extensions directory to keep our application nicely organized and easier to maintain in the future. You can do similar, find your own organizational structure that works for you, or go wild.

Step 3

Now that we’ve got our phone number “core” ready to go, let’s update our schema and implement the validations for our registration changeset:

# your_app.accounts.person.ex...
alias YourApp.Encrypted # Optional encryption
schema "people" do
field :phone_number, Encrypted.Binary, redact: true
def registration_changeset(person, attrs, opts \\ []) do
|> cast([... :phone_number, ...])
|> validate_phone_number()

We need to remember to cast our new field to our person changeset using the cast/4 function from Ecto.Changeset.

Then, we need to implement our new validate_phone_number/1— our updated your_app.accounts.person.ex:

Our validate_phone_number/1 function checks that someone has input a phone number and that it is not an excessively large input (max character length of 25). Then, our function calls another private function, maybe_accept_phone_number/1, that also takes our changeset as its sole argument and implements the functions from our phone_number.ex module.

We also add a handy error message at the end of our with statement (formatted for readability).

Okay, halfway there!

Step 4

Let’s update the number of steps in our form, making our :phone_number step 3 (again, you can change accordingly):

This might be located in your equivalent “your_app_web/live/person_registration_live/new.ex”.

Great! Nothing too complicated there, let’s move on to our person_registration/new.html.leex template.

Step 5

Let’s update our person_registration/new.html.leex template to include the following:

Again, I’ve stripped out a lot, but the key takeaways are:

  • <%= if @current_step > 1 and @current_step < 5 do %> This is from the Addendum of Part 2. By adding the additional condition to our if check, we remove the “Back” button on our final form step which fixes an issue with the “Submit” button. It was an edge-case I was experiencing as the form took on more steps and complexity (including an awesome passphrase generator). You can try to experiment without this change, and keep the “Back” button on the final step, but you may or may not run into trouble.
  • <%= if @current_step === 5 do %> Remember to update your final step accordingly (your app’s form may have more or less steps).

Optional takeaways for step 6 are:

  • id: "person-phone-number" This is necessary for our client-side hook. From the docs: “Note: when using phx-hook, a unique DOM ID must always be set”.
  • phx_hook: "PhoneNumber" This is necessary for the JavaScript interoperability with Phoenix Live View.

We also add <%= error_tag f, :phone_number %> to provide useful feedback for people using the form.

Step 6 (Optional)

This final step is optional, as the Live View functionality that we’ve implemented with our phone_number.ex functions will actually format and validate the phone number for us.

However, it currently requires that someone types in the “+country code” and the rest of their phone number. It’s not a deal breaker, and we won’t get into too much trouble providing a smoother flow, but we can easily eliminate the need for someone to type the “+” with their phone number (they will still need to enter their country code).

If you decide to forgo this step, then you will need to remove the phx_hook: "PhoneNumber" bit from your phone number field, otherwise Phoenix will go on a lonely search for a hook that does not exist.

With that said, let’s update our app.js file and be done:

Boom! Now, whenever someone types “1 222 222 3333” or “41044 668 18 00” or any other phone number with a country code into our phone number field, it will get the “+” added to it before sending it through the server-side Live View process.

It’s a small improvement over having to manually input the “+”, but, for such little effort, I felt it was worth it.

It also serves as a great starting point for implementing more sophisticated JavaScript into your flow to really customize the experience for people (perhaps combining with a wild package from Jack O’Connor).

If you aren’t using AlpineJS at all in your application, then your app.js might look more like:

let liveSocket = new LiveSocket("/live", Socket, {hooks: Hooks, ...})

You can learn more about client-side hooks with Phoenix Live View and JavaScript interoperability by reading the official documentation.


That’s it! We’ve added a working international phone number field to our multi-step form using Phoenix Live View and successfully extended our registration flow from Part 1 and Part 2.

Hope this helps anyone looking to implement a phone number field with Phoenix Live View.

Always open to improvements and thoughts.

❤ Mark



Geek Culture

Creator @ Metamorphic | Co-founder @ Moss Piglet