Correct Display of Errors in Phoenix.HTML with Bootstrap

Bootstrap requires since Bootstrap 4.0 is-invalid as field class to display the field error.
I found a solution presented in the following to display the errors again, but I don’t really know what’s the best way to integrate it. I add for each field with errors in a generic way the is_invalid class.

def password_input(form, field, opts \\ []) do
  opts = if !is_atom(form) && Keyword.has_key?(form.errors, field), do: Keyword.update(opts, :class, "is-invalid", &("#{&1} is-invalid")), else: opts
  ...

I wonder now how I can integrate this in the best way into the Phoenix.HTML helpers with touching them as least as possible, so I hopefully have the least trouble to stay up to date with the helpers.

In a way like this to just not import the functions I need to “override”?

1 Like

I haven’t tried it yet, but expect I’m going to be in the same boat as you with a side project I’m starting that is planned to use BS 4. There haven’t been updates to this in a while, but if the BS markup hasn’t changed, it may not need any.

2 Likes

Thanks for this great hint to your code. I adopted some of your stuff and solved other things a bit differently, that I want to share here, too. And I have some feedback to your idea.

I needed to remove your form = %Form{} match from

  [...]
  |> Enum.each(fn method ->
    def unquote(method)(form, field, opts \\ [])

since you can also call this function with an atom instead of a form. You might adapt this as well.

I did not want to change anything of my code, so I created a new ViewHelper:

defmodule MyApp.BootstrapHelpers do

  defmacro __using__(_opts) do
    quote do
      import Phoenix.HTML
      import Phoenix.HTML.Form, except: [text_input: 3, file_input: 3, email_input: 3, password_input: 3, textarea: 3, telephone_input: 3, number_input: 3, date_input: 3, date_select: 3, datetime_select: 3, select: 4, time_select: 3, time_input: 3, url_input: 3, multiple_select: 4, range_input: 3]
      import Phoenix.HTML.Link
      import Phoenix.HTML.Tag
      import Phoenix.HTML.Format
      import SportsConnected.BootstrapHelpers
    end
  end

  [:text_input, :file_input, :email_input, :password_input, :textarea, :telephone_input, :number_input, :date_input, :date_select, :datetime_select, :time_select, :time_input, :url_input, :range_input]
  |> Enum.each(fn method ->
    def unquote(method)(form, field, opts \\ []) when is_atom(field) do
      opts = add_is_invalid_to_invalid_input(form, field, opts)
      elem(Code.eval_string("Phoenix.HTML.Form.#{Atom.to_string(elem(__ENV__.function, 0))}(form, field, opts)", [form: form, field: field, opts: opts]), 0)
    end
  end)

  [:select, :multiple_select]
  |> Enum.each(fn method ->
    def unquote(method)(form, field, options, opts \\ []) when is_atom(field) do
      opts = add_is_invalid_to_invalid_input(form, field, opts)
      elem(Code.eval_string("Phoenix.HTML.Form.#{Atom.to_string(elem(__ENV__.function, 0))}(form, field, options, opts)", [form: form, field: field, options: options, opts: opts]), 0)
    end
  end)

  defp add_is_invalid_to_invalid_input(form, field, opts) do
    if !is_atom(form) && Keyword.has_key?(form.errors, field), do: Keyword.update(opts, :class, "is-invalid", &("#{&1} is-invalid")), else: opts
  end

end

Of course, you need to change the import in your web.ex from Phoenix.HTML to your new module.

1 Like

I’ve found this Gist does the trick pretty well:
https://gist.github.com/joshchernoff/f11b3b8126c917752d88d63fc728c2bb.

And a couple of related articles that I found helpful, that you might as well:

http://blog.plataformatec.com.br/2016/09/dynamic-forms-with-phoenix/

http://www.elfedyrodriguez.com/2018/03/31/testing_custom_form_helpers_in_phoenix.html

2 Likes

I follow the following:

  def input(form, field, opts \\ []) do
  ...
  opts =
      put_in(
        opts[:class],
        String.trim("form-control #{input_state_class(form, field)} #{opts[:class]}")
      )
  ...
  end

and

  def input_state_class(form, field) do
    cond do
      # The form was not yet submitted
      !form.source.action ->
        ""

      form.errors[field] ->
        "is-invalid"

      true ->
        "is-valid"
    end
  end