How to validate an array of strings for the upload files in Phoenix LiveView

Hello Guys,
I was doing the course of the PraProg Phoenix LiveView Pro and I was trying to do a validation for the upload files.
For example If I try to put just the name of the desks and not put any file for upload, I would like to show the validation for upload a file.

I tried to add a validate_required for the photo_urls on the changeset:

 defmodule LiveViewStudio.Desks.Desk do
  use Ecto.Schema
  import Ecto.Changeset

  schema "desks" do
    field :name, :string
    field :photo_urls, {:array, :string}, default: []

    timestamps()
  end

  @doc false
  def changeset(desk, attrs) do
    desk
    |> cast(attrs, [:name, :photo_urls])
    |> validate_required([:name, :photo_urls])
  end
end

But if I put just the name for the desks, the photo_urls are not validated.
as you can see the log:

[debug] QUERY OK db=2.1ms queue=1.3ms idle=1529.6ms
INSERT INTO "desks" ("name","photo_urls","inserted_at","updated_at") VALUES ($1,$2,$3,$4) RETURNING "id" ["Testing", [], ~N[2021-04-19 17:20:41], ~N[2021-04-19 17:20:41]]

But on the application this values are not showed. It shows only the those which have photo_urls.
But If I try to list all Desks, like:
iex> Desks.list_desks()
The empty photo_urls are recorded:

  %LiveViewStudio.Desks.Desk{
    __meta__: #Ecto.Schema.Metadata<:loaded, "desks">,
    id: 17,
    inserted_at: ~N[2021-04-16 21:11:22],
    name: "Testing",
    photo_urls: [],
    updated_at: ~N[2021-04-16 21:11:22]
  },

How can I validate an empty array of strings for the :photo_urls?

I didn’t get your goal, are you after validating photo_urls are empty?

I was reading into the Ecto.Changeset.validate_required, I don’t think it works on lists but single value fields instead.

https://hexdocs.pm/ecto/Ecto.Changeset.html#validate_required/3

What I want is to validate the array of string which in this case is the :photo_urls.

In that case you would have to define your own validation in your schema. Keep in mind you would be only validating the string url for those files at that point, not the files themselves.

You can iterate over the urls and use validate_format maybe if you are expecting a certain file path or minimum number of urls.

https://hexdocs.pm/ecto/Ecto.Changeset.html#validate_format/4

sorry but on Docs I don’t see any validate_pattern?

my apologies, different language mix up there! I have corrected my post

https://hexdocs.pm/ecto/Ecto.Changeset.html#validate_format/4

1 Like

don’t worry. I will see.
Thank you for reply @cenotaph !

I know, in general, the PragProg course doesn’t cast the :photo_urls because you build them explicitly during the live upload process.

I’m guessing you want to add extra info for the person’s experience using the upload, in which case you could try some type of logic in your template like:

<%= if @uploads.desk.entries == [] do %>
  # render your error
  # disable the upload button, etc.
<% end %>

Something like that may be what you’re after? And you can adjust your conditional accordingly.

Yes I know in the course it doesn’t cast the :photo_urls.
But I was testing to put just the :name and no files for uploads.
And then I notice it was saving on the database, but they don’t are showing.
So I was trying to do a validate in the schema.
For now your help make more sense.
But it would be nice to do this on the schema.
I will review some books, I think I saw something which is possible creating a custom validation.
I just don’t know how to do it now.
thank you for reply @f0rest8!

1 Like

I was thinking to try a custom validation(something like this):

  @doc false
  def changeset(desk, attrs) do
    desk
    |> cast(attrs, [:name, :photo_urls])
    |> validate_required([:name])
    |> validate_empty_photo_urls(:photo_urls)
  end

  def validate_empty_photo_urls(changeset, field)  do
    validate_change(changeset, field, fn _field, value ->
      cond do
        is_nil(value) -> "must insert a photo!"
      end
    end)
  end

But not work’s!
Can anyone help me with this custom validation?

It invokes the validator function to perform the validation only if a change for the given field exists and the change value is not nil. The function must return a list of errors (with an empty list meaning no errors).

  • when the value is nil, the validator function won’t even be called; I believe you defined :photo_urls as an array of strings with the default value being empty list;
  • you must return a list of errors, [photo_urls: "must insert a photo!"], in the error case
  • [] in the no error case
1 Like

I changed for this:

def changeset(desk, attrs) do
    desk
    |> cast(attrs, [:name, :photo_urls])
    |> validate_empty_photo_urls()
    |> validate_required([:name])
  end

  def validate_empty_photo_urls(changeset)  do
    photos = get_field(changeset, :photo_urls)

      if (photos == []) do
        put_change(changeset, :photo_urls, "must insert a photo!")
      else
        changeset
      end
  end

And when I try i can know receive an Ecto.ChangeError on the log and stop to submit and not save the data.
The log when I try is this:

[error] GenServer #PID<0.671.0> terminating
** (Ecto.ChangeError) value `"must insert a photo!"` for `LiveViewStudio.Desks.Desk.photo_urls` in `insert` does not match type {:array, :string}
    (ecto 3.5.6) lib/ecto/repo/schema.ex:889: Ecto.Repo.Schema.dump_field!/6
    (ecto 3.5.6) lib/ecto/repo/schema.ex:898: anonymous fn/6 in Ecto.Repo.Schema.dump_fields!/5
    (stdlib 3.14) maps.erl:233: :maps.fold_1/3
    (ecto 3.5.6) lib/ecto/repo/schema.ex:896: Ecto.Repo.Schema.dump_fields!/5
    (ecto 3.5.6) lib/ecto/repo/schema.ex:829: Ecto.Repo.Schema.dump_changes!/6
    (ecto 3.5.6) lib/ecto/repo/schema.ex:255: anonymous fn/15 in Ecto.Repo.Schema.do_insert/4
    (live_view_studio 0.1.0) lib/live_view_studio/desks.ex:55: LiveViewStudio.Desks.create_desk/3
    (live_view_studio 0.1.0) lib/live_view_studio_web/live/desks_live.ex:46: LiveViewStudioWeb.DesksLive.handle_event/3
    (phoenix_live_view 0.15.4) lib/phoenix_live_view/channel.ex:338: anonymous fn/3 in Phoenix.LiveView.Channel.view_handle_event/3
    (telemetry 0.4.2) /Users/romenigld/workspace/phoenix/phoenix_live_view/pragprog/my_code/live-view-studio/deps/telemetry/src/telemetry.erl:262: :telemetry.span/3
    (phoenix_live_view 0.15.4) lib/phoenix_live_view/channel.ex:203: Phoenix.LiveView.Channel.handle_info/2
    (stdlib 3.14) gen_server.erl:689: :gen_server.try_dispatch/4
    (stdlib 3.14) gen_server.erl:765: :gen_server.handle_msg/6
    (stdlib 3.14) proc_lib.erl:236: :proc_lib.wake_up/3
Last message: %Phoenix.Socket.Message{event: "event", join_ref: "30", payload: %{"event" => "save", "type" => "form", "value" => "_csrf_token=MhYKJTM6IjQPJg0SCC0_Yz5REx91BwcaZlrjvbpbGNIzCky6k0dp402q&desk%5Bname%5D=testing"}, ref: "44", topic: "lv:phx-FnfspaamCei_owGE"}
State: %{components: {%{}, %{}, 1}, join_ref: "30", serializer: Phoenix.Socket.V2.JSONSerializer, socket: #Phoenix.LiveView.Socket<assigns: %{changeset: #Ecto.Changeset<action: :insert, changes: %{name: "testing", photo_urls: "must insert a photo!"}, errors: [], data: #LiveViewStudio.Desks.Desk<>, valid?: true>, desks: [], flash: %{}, live_action: nil, uploads: %{__phoenix_refs_to_names__: %{"phx-Fnfs0wpIbWDZCwDB" => :photo}, photo: #Phoenix.LiveView.UploadConfig<...>}}, changed: %{}, endpoint: LiveViewStudioWeb.Endpoint, id: "phx-FnfspaamCei_owGE", parent_pid: nil, root_pid: #PID<0.671.0>, router: LiveViewStudioWeb.Router, view: LiveViewStudioWeb.DesksLive, ...>, topic: "lv:phx-FnfspaamCei_owGE", transport_pid: #PID<0.660.0>, upload_names: %{}, upload_pids: %{}}
[debug] QUERY OK source="desks" db=1.9ms idle=1616.9ms
SELECT d0."id", d0."name", d0."photo_urls", d0."inserted_at", d0."updated_at" FROM "desks" AS d0 ORDER BY d0."id" DESC []

Know just need to insert the message on errors.

You were on the right track with the previous iteration.

The 2nd iteration makes no sense, honestly.

1 Like

Yes I notice I forget to add the [photo_urls: "must insert a photo!"].
I will stop now because I need to go out.
I will try this later.
Can you see the other solution I tried?
what do you think?

So what I do now for show the error like the error_to_string function?

error_to_string(Ecto.ChangeError)?

You started from this:

  def validate_empty_photo_urls(changeset, field)  do
    validate_change(changeset, field, fn _field, value ->
      cond do
        is_nil(value) -> "must insert a photo!"
      end
    end)
  end

which was quite close. The docs have this to say:

It invokes the validator function to perform the validation only if a change for the given field exists and the change value is not nil. The function must return a list of errors (with an empty list meaning no errors).

  def validate_empty_photo_urls(changeset, field)  do
    validate_change(changeset, field, fn _field, value ->
      if Enum.empty?(value) do
        [photo_urls: "must insert a photo!"]
      else
        []
      end
    end)
  end

It basically has the modifications in the bullet points I’ve mentioned previously. I haven’t tested this, so make sure you do.

1 Like

I will try, thank’s for reply @sfusato !

They are inserting into database like before.
and I tried to use the function add_error() like:

add_error(changeset, :photo_urls, "must insert a photo!")

but not work.

The most close was when it raises the Ecto.ChangeError. But this is not the correct way to do.
I get this idea here on Changeset - Elixir School.

Oh, right, since there was no change, my validator wasn’t actually called either since there’s no point in validating.

Let’s see. Remove the default value of the field:

field :photo_urls, {:array, :string}

Add the field to validate_required:

|> validate_required([:name, :photo_urls])

And let’s use it in combination with validate_length with a min of 1:

|> validate_length(:photo_urls, min: 1)

When I arrived in home I was thinking in to inspect the values.
So I do this:

defmodule LiveViewStudio.Desks.Desk do
  use Ecto.Schema
  import Ecto.Changeset

  schema "desks" do
    field :name, :string
    field :photo_urls, {:array, :string}, default: []

    timestamps()
  end

  @doc false
  def changeset(desk, attrs) do
    desk
    |> cast(attrs, [:name, :photo_urls])
    |> validate_required([:name])
    |> validate_empty_photo_urls()
  end

  def validate_empty_photo_urls(changeset)  do
    photos = get_field(changeset, :photo_urls)

    IO.inspect(photos, label: "Photos")

    if Enum.empty?(photos) do
      changeset = add_error(changeset, :photo_urls, "must insert a photo!")
      IO.inspect(changeset, label: "changeset")
    else
      changeset
    end
  end
end

So in the log shows me this:

Photos: []
changeset: #Ecto.Changeset<
  action: nil,
  changes: %{name: "test"},
  errors: [photo_urls: {"must insert a photo!", []}],
  data: #LiveViewStudio.Desks.Desk<>,
  valid?: false

And I can see it was working the validations and it wasn’t inserted in the database when I click on the button Upload.
So I was seeking why not it was showed the error and the thing is not having the error_tag for the :photo-urls.

So I add the error_tag on the beginning of the drag-and-drop div:

  <div class="drop" phx-drop-target="<%= @uploads.photo.ref %>">
      <div>
        <%= error_tag f, :photo_urls %>
        <img src="/images/upload.svg">
        <div>
          <label for="<%= @uploads.photo.ref %>">
            <span>Upload a file</span>
            <%= live_file_input @uploads.photo, class: "sr-only" %>
          </label>
          <span>or drag and drop</span>
        </div>
        <p>
          <%= @uploads.photo.max_entries %> photos max,
          up to <%= trunc(@uploads.photo.max_file_size / 1_000_000) %> MB each
        </p>
      </div>
    </div>

And now it’s working! :laughing:
Thank you for the help guys, it was very helpful!

4 Likes