Have you ever been confused about all the different fields in the Ecto Changeset structure? Today we gonna take a look at how two of them - data
and changes
complement each other when building user interfaces.
I believe that a gradual and iterative explanation is the best approach to learn new material, so we will be using tests to guide our implementation.
Problem: Hide or display form element depending on user input
Setting the scene
Let’s consider an example of a Feedback
entity that has a required property message
and optional sender_email
property. If a user doesn’t want to receive any followup on his message then email input shouldn’t be shown.
After a page is loaded this is easily achievable with JS but what about the initial page state, just after it was rendered?
On a markup level hiding email
block boils down to answering a question whether a class that sets a visibility property should be added to an enclosing tag or not.
With the following schema:
schema "feedbacks" do
field :message, :string
field :sender_email, :string
field :permission_to_contact, :boolean, virtual: true
end
that decision can be made based on a value of permission_to_contact
property.
To help us make the right choice Ecto.Changeset
struct exposes two fields: data
and changes
.
data
field represents original values and covers initial page load before any changes introduced by a user were made, while the changes
field comes into play on subsequent renders. It represents changes made by a user and permitted during a validation phase.
Using this two we can cover the next scenarios :
- Initial page load. Field is displayed based on a default contact settings ( “Yes” in our example)
- User selected “No” and form should be re-rendered
- User selected “Yes” and form should be re-rendered
Implementation
Setup
Add a view helper invocation that will control sender email block visibility
<div class="<%= hide_if_followup_denied(@changeset) %>">
Email:
<%= text_input f, :sender_email %>
</div>
Step 1: Initial page load
Test:
describe "hide_if_followup_denied/1" do
test "on initial submission (default is that user agrees to be contacted)" do
changeset = Ecto.Changeset.change(%Feedback{permission_to_contact: true})
assert FeedbackView.hide_if_followup_denied(changeset) == ""
end
end
Implementation:
def hide_if_followup_denied(_changeset) do
""
end
Please notice, that we opted out to set default value on schema instead of a view.
field :permission_to_contact, :boolean, virtual: true, default: true
Step 2: Consecutive page render when field should be shown
This scenario covers a situation when a form has to be re-rendered with a visible field. For example, a user agreed to be contacted but provided an invalid email.
Test:
test "on page re-render when user agrees to be contacted" do
changeset = Ecto.Changeset.cast(%Feedback{}, %{"permission_to_contact" => "true"}, [:permission_to_contact])
assert FeedbackView.hide_if_followup_denied
(changeset) == ""
end
Implementation:
… we don’t have to do anything as our code already satisfies all requirements at hand:)
Just following a classical Larry Wall’s advice to develop a bit of a (good) laziness here.
Step 3: Consecutive page render when field shouldn’t be shown
Now we are going to cover a situation when a user doesn’t want to be contacted back but still wants to provide feedback and occasionally submitted a form before entering a message. Hence form has to be rendered again but email
field should not be shown.
Test:
test "on page re-render when user do not want to be contacted" do
changeset = Ecto.Changeset.cast(%Feedback{}, %{"permission_to_contact" => "false"}, [:permission_to_contact])
assert FeedbackView.hide_if_followup_denied(changeset) == " hidden"
end
Final Implementation:
def hide_if_followup_denied(changeset) do
hide_on_inital_render = changeset.data.permission_to_contact == nil && changeset.changes[:permission_to_contact] == nil
hide_after_rerender = changeset.changes[:permission_to_contact] == false
if hide_on_inital_render || hide_after_rerender do
" hidden"
else
""
end
end
Closing thoughts
In this short example, we went through an exercise to see how data
and changes
fields of Ecto Changeset struct complements each other and enable developers to build clean UI logic.
Also, I hope that the decision to put a default value in schema made you skeptical for a moment.
We could definitely do it in a view itself, but I would argue that keeping it in schema which can be then converted to “view only” schema as described in the “Data Mapping and Validation Chapter” in “The Little Ecto Cookbook” is more beneficial in a sense that it keeps hidden_if_followup_denied
responsibilities as slim as possible.
Links
https://hexdocs.pm/ecto/Ecto.Changeset.html#module-the-ecto-changeset-struct - Changeset structure
Top comments (0)