elixirphoenix-frameworkecto

Validate required for put_assoc


In my phoenix project, I have a posts and tags schema linked through a join table

schema "posts" do
    field :title, :string
    field :body, :string

    many_to_many :tags, App.Tag, join_through: App.PostsTags , on_replace: :delete
    timestamps()   
end?

How do i ensure that tags are present went using put_assoc?

def changeset(struct, params \\ %{}) do
    struct
    |> cast(params, [:title, :body])
    |> put_assoc(:tags, parse_tags(params), required: true)
    |> validate_required([:title, :body, :tags])
end

Both required: true on put_assoc and adding :tags to validate required do not work as I had imagined.


Solution

  • validate_length/3 can be used to check that the tags list is non empty:

    Post Schema:

    defmodule MyApp.Blog.Post do
      use Ecto.Schema
      import Ecto.Changeset
      alias MyApp.Blog.Post
    
    
      schema "blog_posts" do
        field :body, :string
        field :title, :string
        many_to_many :tags, MyApp.Blog.Tag, join_through: "blog_posts_tags"
    
        timestamps()
      end
    
      @doc false
      def changeset(%Post{} = post, attrs) do
        post
        |> cast(attrs, [:title, :body])
        |> put_assoc(:tags, attrs[:tags])
        |> validate_required([:title, :body])
        |> validate_length(:tags, min: 1)
      end
    end
    

    Tag Schema:

    defmodule MyApp.Blog.Tag do
      use Ecto.Schema
      import Ecto.Changeset
      alias MyApp.Blog.Tag
    
    
      schema "blog_tags" do
        field :name, :string
    
        timestamps()
      end
    
      @doc false
      def changeset(%Tag{} = tag, attrs) do
        tag
        |> cast(attrs, [:name])
        |> validate_required([:name])
      end
    end
    

    Validates successfully when tags is present:

    iex(16)> Post.changeset(%Post{}, %{title: "Ecto Many-to-Many", body: "Lots of words", tags: [%{name: "tech"}]})
    #Ecto.Changeset<action: nil,
     changes: %{body: "Lots of words",
       tags: [#Ecto.Changeset<action: :insert, changes: %{name: "tech"}, errors: [],
         data: #MyApp.Blog.Tag<>, valid?: true>], title: "Ecto Many-to-Many"},
     errors: [], data: #MyApp.Blog.Post<>, valid?: true>
    

    Produces an error when tags is empty:

    iex(17)> Post.changeset(%Post{}, %{title: "Ecto Many-to-Many", body: "Lots of words", tags: []})
    #Ecto.Changeset<action: nil,
     changes: %{body: "Lots of words", tags: [], title: "Ecto Many-to-Many"},
     errors: [tags: {"should have at least %{count} item(s)",
       [count: 1, validation: :length, min: 1]}], data: #MyApp.Blog.Post<>,
     valid?: false>
    

    Produces an error when tags is nil:

    iex(18)> Post.changeset(%Post{}, %{title: "Ecto Many-to-Many", body: "Lots of words"})
    #Ecto.Changeset<action: nil,
     changes: %{body: "Lots of words", title: "Ecto Many-to-Many"},
     errors: [tags: {"is invalid", [type: {:array, :map}]}],
     data: #MyApp.Blog.Post<>, valid?: false>