loading...

Difference between "data" and "changes" fields in Ecto.Changeset

lessless profile image Yevhenii Kurtov Updated on ・3 min read

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

Form mockup

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

Posted on by:

lessless profile

Yevhenii Kurtov

@lessless

Software crafter by trade. Interested in startups, principles and socio-technical aspects of software development.

Discussion

markdown guide