Subscribe to access all episodes. View plans →
Published May 14, 2018
Elixir 1.5
Phoenix 1.3
In this episode let’s see how we can use virtual attributes in Ecto to solve a common problem - when we need to include fields that don’t map to columns in the database.
Here we have a signup form with email, full name, and password fields
And now instead of displaying a full name field, we want to break it up into two field - one for the first name and one for the last name.
Let’s open our signup template and change our form to use a first name and last name.
Template path: lib/teacher_web/templates/user/form.html.eex
...
<div class="form-group">
<%= label f, :first_name, class: "control-label" %>
<%= text_input f, :first_name, class: "form-control" %>
<%= error_tag f, :first_name %>
</div>
<div class="form-group">
<%= label f, :last_name, class: "control-label" %>
<%= text_input f, :last_name, class: "form-control" %>
<%= error_tag f, :last_name %>
</div>
...
Then we’ll go to our user.ex
module and add the first name and last name as virtual attributes.
lib/teacher/accounts/user.ex
...
schema "users" do
field :encrypted_password, :string
field :email, :string
field :full_name, :string
field :first_name, :string, virtual: true
field :last_name, :string, virtual: true
timestamps()
end
...
Now let’s go to our browser and if we try to signup we get an error.
This is because we’re now collecting the first and last name and submitting them to the server, but we’re not using them to build a full name - which as we can see in our changeset
function is a required field. Let’s fix that.
We’ll need to create a function that will get the first and last name from the changeset and combine them to create our full name that we can then persist to the database. Let’s define a private function named build_full_name
that will take the changeset as a parameter.
Then we can use the Ecto.Changeset.get_field
function to grab the first name and then the last name. Then we’ll use Ecto.Changeset.put_change
to update our changes with the full_name
key.
And for the value we’ll use string interpolation to build our name. put_change
will return our updated changeset, which is great because we can now include our new build_full_name
function in our existing pipeline.
So let’s go back to our changeset
function and include build_full_name
. We’ll also need to update our cast
function to include :first_name
and :last_name
.
lib/teacher/accounts/user.ex
...
def changeset(%User{} = user, attrs) do
user
|> cast(attrs, [:email, :encrypted_password, :first_name, :last_name])
|> build_full_name()
|> validate_required([:email, :encrypted_password, :full_name])
|> unique_constraint(:email)
|> update_change(:encrypted_password, &Bcrypt.hashpwsalt/1)
end
defp build_full_name(changeset) do
first_name = get_field(changeset, :first_name)
last_name = get_field(changeset, :last_name)
put_change(changeset, :full_name, "#{first_name} #{last_name}")
end
And if we go back to the browser and sign up - it works.
Let’s also check the database just to make sure. And our new user is in there with the full name in the proper format.
Augustin Riedinger
5 years agoIMHO a more valid use case would be to store the
first_name
/last_name
in the DB and to create a virtualfull_name
field because the other way around, there is a loss of information: if the full name isMarco Luis Rodriguez
, is it:Marco
-Luis Rodriguez
orMarco Luis
-Rodriguez
? Impossible to tell.