Search code examples
elixirphoenix-frameworkecto

Ecto custom type in Phoenix Form?


I'm using an Ecto custom type in one of my Phoenix application's schemas, like described here (specifically, making use of Postgres Ranges to specify a range of times, like "between 12:00-4:00"). I'm able to insert/retrieve from the database without a problem, but I'm having trouble coming up with a good way to present a form for the user using changesets and Phoenix forms.

So with a schema looks like this (TimeRange is the custom type):

  @primary_key false
  @foreign_key_type :binary_id
  schema "person_messaging_settings" do
    field :can_receive_email, :boolean, null: false
    field :can_receive_sms, :boolean, null: false
    field :allowed_hours, MyApp.Ecto.TimeRange
    belongs_to :person, Person
    timestamps()
  end

I can use inputs_for for the belongs_to association, and ideally I could do something like this in my EEX template:

<%= form_for @changeset, Routes.settings_path(@conn, :update), fn f -> %>

  <!-- other field values -->

  <%= inputs_for f, :allowed_hours, fn ah -> %>
    <%= time_select ah, :upper %>
    <%= time_select ah, :lower %>
  <% end %>
<% end %>

But this complains because inputs_for is strictly for associations.


Solution

  • Here's a raw untested idea with virtual fields.

    Schema file:

    schema "person_messaging_settings" do
      # ...
      field :allowed_hours_from, :time, virtual: true
      field :allowed_hours_to, :time, virtual: true
    end
    
    def changeset do
      record
      |> cast(attrs, [..., :allowed_hours_from, :allowed_hours_to])
      |> set_allowed_hours()
      |> validate_required([..., :allowed_hours])
    end
    
    defp set_allowed_hours(changeset) do
      case {get_field(changeset, :allowed_hours_from), get_field(changeset, :allowed_hours_to)} do
        {nil, nil} -> changeset
        {nil, _}   -> changeset
        {_, nil}   -> changeset
        {from, to} -> put_change(changeset, :allowed_hours, "#{from}-#{to}")
      end
    end
    

    And the form:

    <%= form_for @changeset, Routes.settings_path(@conn, :update), fn f -> %>
    
      <!-- other field values -->
    
      <%= time_select f, :allowed_hours_from %>
      <%= time_select f, :allowed_hours_to %>
    <% end %>
    

    Although I don't know how you would populate the two time_selects when editing a saved timerange (decomposing the :allowed_hours). Perhaps somebody else does. Or you render a regular html input with the correct name and value.

    Edit 3... Or would this work?

    <%= time_select f, :allowed_hours_from, value: something(f.data.allowed_hours) %>