Ecto's Schemaless Changeset

Ecto Changesets  are used to validate and manipulate data server-side. Most of the work with changesets is done using schemas, nonetheless there are cases where we would want to validate a set of data without using one, e.g.: form validation. Although Ecto documentation describes how to implement a schemaless changeset, it doesn't explain how to use it with Liveview.

To start off we'll need a data source. In our app's lib/app_web/live  directory create our liveview file, let's call it nickname_live.ex, and implement mount and render callbacks of the liveview behaviour:

elixir

        defmodule AppWeb.NicknameLive do  
         use AppWeb, :live_view
        
          def mount(_params, _session, socket) do
            {:ok, assign(socket, form: to_form(%{}))}
          end

          def render(assigns) do
          ~H"""
          <.simple_form for={@form} phx-change="validate" phx-submit="save" class="flex flex-col items-center">
          <h1>Please provide your nickname :</h1>
            <.input field={@form[:nickname]} label="nickname" />
            <:actions>
              <.button>Create</.button>
            </:actions>
          </.simple_form>
          """
        end
        

So what's going on here? In mount we add form(it has to be the same as form's id in html) variable to the socket, it will hold whatever data we send from our form. to_form function transforms a map or Ecto changeset to a form component. The render function is responsible for generating our html.heex template. Let's now implement some logic:

elixir

        def handle_event("validate", params, socket) do
          types = %{nickname: :string}
  
          changeset =
            {%{}, types}
            |> Ecto.Changeset.cast(params, Map.keys(types))
            |> Ecto.Changeset.validate_required(:nickname)
            |> Ecto.Changeset.validate_length(:nickname, min: 3)
            |> Map.put(:action, :validate)
  
          {:noreply, assign(socket, form: to_form(changeset))}
        end
        

Above code receives params and simply performs Ecto validation and returns all errors generated, finally we update the form variable. Now when we run the server and try to type in the Nickname field... we will get an error.

** (ArgumentError) cannot generate name for changeset where the data is not backed by a struct. You must either pass the :as option to form/form_for or use a struct-based changeset

As it turns out, to_form function computes a name to nest the params based on schema name that is being used. As we don't use a schema we'll have to pass as: parameter that will serve as aforementioned name. It basically creates a map that we can pattern match against. Our mount and handle_event functions will now look like this:

elixir

          def mount(_params, _session, socket) do
          {:ok, assign(socket, form: to_form(%{}, as: :nickname))}
        end

 
        def handle_event("validate", %{"nickname"=> params}, socket) do
          types = %{nickname: :string}
  
          changeset =
            {%{}, types}
            |> Ecto.Changeset.cast(params, Map.keys(types))
            |> Ecto.Changeset.validate_required(:nickname)
            |> Ecto.Changeset.validate_length(:nickname, min: 3)
            |> Map.put(:action, :validate)
  
          {:noreply, assign(socket, form: to_form(changeset, as: :nickname))}
        end
      
        

Et voilà, now the compiler doesn't complain anymore, and we can see errors when typing in the Nickname field.