Form Validation with Schemaless Changesets

Hey Folks,

I’ve got a marketing site that has a contact us form to reach out for potential requests of work/services.

The website has Ecto dependencies enabled and an empty PostgreSQL instance attached. However, the first iteration of the marketing site is mostly static content and therefore no Ecto models or an overarching schema have been created yet.

Because little thought has been put into the long term schema, for the sake of simplicity and reduction of future issues, I created a form from an atom instead of a @changeset.

<section class="submit-service-request-page">
    <%= service_request_form = form_for :service_request, "#", [phx_change: :validate, phx_submit: :submit] %>
        <label>
            All fields required <span class="light-red-text">*</span>
        </label>

        <%= text_input service_request_form, :preferred_name, placeholder: "Preferred Name" %>
        <%= error_tag service_request_form, :preferred_name %>

        <%= email_input service_request_form, :email, placeholder: "Your Email" %>
        <%= error_tag service_request_form, :email %>

        <%= select service_request_form, :choose_service, @service_options, selected: [1] %>
        <%= error_tag service_request_form, :choose_service %>

        <%= textarea service_request_form, :brief_description, placeholder: "Describe the Job" %>
        <%= error_tag service_request_form, :brief_description %>

        <div class="text-align-center">
            <%= submit "Send Request" %>
        </div>
    </form>
</section>

In the live_view I handle the validation event as such:

  def handle_event("validate", %{"service_request" => validate_params}, socket) do
    data  = %{}
    types = %{brief_description: :string, choose_service: :string, email: :string, preferred_name: :string}

    service_request =
      {data, types}
      |> cast(validate_params, Map.keys(types))
      |> validate_required([:brief_description, :choose_service, :email, :preferred_name])
      |> validate_format(:email, ~r/@/)
      |> validate_length(:brief_description, min: 10)

    {:noreply, assign(socket, changeset: service_request)}
  end

I know that the Ecto changeset is being cast properly and that the validations are working as when I log them I get the following:

Ecto.Changeset<action: nil, changes: %{brief_description: "asfa"}, 
errors: [brief_description: {"should be at least %{count} character(s)", [count: 10, validation: :length, kind: :min, type: :string]}, 
choose_service: {"can't be blank", [validation: :required]}, 
email: {"can't be blank", [validation: :required]}, 
preferred_name: {"can't be blank", [validation: :required]}], data: %{}, valid?: false>

However, my forms UI doesn’t automatically update like it would if it were connected to an Ecto model/schema. How do I get the updated @changeset to my service request form? I feel like I’m missing something obvious here. I’ve looked at the following post on these forums and the linked blog, but it didn’t help with this specific issue.

Thanks for the help,
Scott

1 Like

You’re probably missing setting an action on the changeset. For schemaless ones people usually do that with Ecto.Changeset.apply_action/2.

2 Likes

You have a changeset in @changeset (based on your handle_event), what happens if you pass that?

I tried that but couldn’t get access to the changeset in the .leex template, I keep getting assign @changeset not available in eex template. What method would I use to pass that in? mount/3? I don’t have a render method as I just use a .leex with the same name. @al2o3cr Can you provide a quick example of what that would look like in the live_view?

You’ll need setup similar to this in any place that renders that leex file; I’d start by looking at the code that renders the template initially.

2 Likes

Ok I’ve got that in there and have verified that I have access to the @changeset within the .leex template. However this line:

%= service_request_form = form_for @changeset, "#", [phx_change: :validate, phx_submit: :submit] %>

Now throws the following error:

non-struct data in changeset requires the :as option to be given

When a changeset uses a struct, the FormData implementation in phoenix_ecto inflects a name for the fields from the name of the struct’s module.

When there isn’t a struct, it can’t. You should pass a value in as: to form_for that reflects what you’re expecting in the resulting params; based on your previous code, that would be as: :service_request.

7 Likes

Hey, @LostKobrakai thanks for the suggestion I eventually went with, Map.put/2 but I’ll keep your solution in mind.

Ok that worked! thanks so much @al2o3cr