Phoenix: partial templates and passing optional parameters

Beginner here.

Elixir: 1.10.4
Pheonix: 1.5.4

I am experimenting with templates and tailwind css in Phoenix.

Here the render function I am using in my template to display an input_text:

<%= render "_text_input.html", f: f, field: :alpha2 %>

And here the partial template:

<div class="col-span-6 sm:col-span-6 lg:col-span-2">

  <label for="#{Atom.to_string(@field)}" class="block text-sm font-medium leading-5 text-gray-700">

    <%= label @f, @field %>

  </label>

  <%= text_input @f, @field, class: "mt-1 form-input block w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:shadow-outline-blue focus:border-blue-300 transition duration-150 ease-in-out sm:text-sm sm:leading-5 uppercase ", minlength: "2", maxlength: "2" %>

  <%= error_tag @f, @field %>

</div>

What I would like to acheive is modify the <%= form_for @changeset, @action, fn f -> %> so that I can

a) add additional class attributes to partial, e.g. the uppercase class attribute, or the lg:col-span-2 .
b) add optional parameters such as , minlength: "2" that are only valid is very specific cases, where the value of the minlength or maxlength can be passed as a parameter.

This was I can have a standard look and feel for an input field, and when I need a <%= text_input f, :alpha2 %> for example I cann call my partial template for the test input field, with appropriate class parameters modifying the look and action of the field.

Perhaps this has been handled in a blog post that someone could point me to, but so far I have not been able to find something that would help me with this specific issue. Perhaps my idea for a solution here is too simplistic, and other solutions would make more sense here. Any help appreciated.

You could create an additional assign options:

<%= render "_text_input.html", f: f, field: :alpha2, options: %{class: "uppercase"} %>

Then, simply use @options[:class]. @options[:minlength] will return nil if you didn’t pass it.

You could also pass them directly as assigns like you do with :f, :field and instead use assigns[:assign_key] and it will work the same way, but using a specific assign feels more explicit.

1 Like

Thanks!

Ok, I have gotten this far:

<%= render "_text_input.html", f: f, field: :alpha2, options: %{class: "uppercase"} %>

And here the partial template:

<div class="col-span-6 sm:col-span-6 lg:col-span-2">
  <label for="#{Atom.to_string(@field)}" class="block text-sm font-medium leading-5 text-gray-700">
    <%= label @f, @field %>
  </label>
  <%= text_input @f, @field, class: "mt-1 form-input block w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:shadow-outline-blue focus:border-blue-300 transition duration-150 ease-in-out sm:text-sm sm:leading-5 " <> @options[:class], minlength: "2", maxlength: "2" %>
  <%= error_tag @f, @field %>
</div>

The change here is

sm:text-sm sm:leading-5 " <> @options[:class]

But I am still at a loss how to add the optional parameters. The following does not seem to work:

<div class="col-span-6 sm:col-span-6 lg:col-span-2">
  <label for="#{Atom.to_string(@field)}" class="block text-sm font-medium leading-5 text-gray-700">
    <%= label @f, @field %>
  </label>
  <%= text_input @f, @field, class: "mt-1 form-input block w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:shadow-outline-blue focus:border-blue-300 transition duration-150 ease-in-out sm:text-sm sm:leading-5 " <> @options[:class], minlength: @options[:minlength], maxlength: @options[:maxlength] %>
  <%= error_tag @f, @field %>
</div>

Where I am also missing something is with the following:

<label for="#{Atom.to_string(@field)}" class="block text-sm font-medium leading-5 text-gray-700">

Here the HTML that is generated is

<label for="#{Atom.to_string(@field)}" class="block text-sm font-medium leading-5 text-gray-700">

My general understanding of what is going on in the partial template is missing some important step that I currently am not able to identify.

Ok, I think I have found the cause of one of the issues I am having. Executing code in the temlate needs to be done differently:

<div class="col-span-6 sm:col-span-6 lg:col-span-2">
  <label for="<%= Atom.to_string(@field) %>" class="block text-sm font-medium leading-5 text-gray-700">
    <%= label @f, @field %>
  </label>
  <%= text_input @f, @field, class: "mt-1 form-input block w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:shadow-outline-blue focus:border-blue-300 transition duration-150 ease-in-out sm:text-sm sm:leading-5 " <> @options[:class], minlength: @options[:minlength], maxlength: @options[:maxlength] %>
  <%= error_tag @f, @field %>
</div>

<label for="<%= Atom.to_string(@field) %>" class="bl

The optional parameters also now seem to work:

<%= render "_text_input.html", f: f, field: :alpha2, options: %{class: "uppercase", minlength: "2", maxlength: "2"} %>

And here the partial template:

<div class="col-span-6 sm:col-span-6 lg:col-span-2">
  <label for="<%= Atom.to_string(@field) %>" class="block text-sm font-medium leading-5 text-gray-700">
    <%= label @f, @field %>
  </label>
  <%= text_input @f, @field, class: "mt-1 form-input block w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:shadow-outline-blue focus:border-blue-300 transition duration-150 ease-in-out sm:text-sm sm:leading-5 " <> @options[:class], minlength: @options[:minlength] , maxlength: @options[:maxlength] %>
  <%= error_tag @f, @field %>
</div>

Thanks.

This is going to produce markup with a <label> inside a <label>, which the HTML standard specifically disallows. Consider passing the class: option to Phoenix.HTML.Form.label instead.

You are right. Thanks for noticing this. Changed the partial template to

<div
  class="col-span-6 sm:col-span-6
    <%=  Map.get(@options, :label_class, "") %>
  ">
  <%= label @f, @field, class: "block text-sm font-medium leading-5 text-gray-700" %>
  <%= text_input @f, @field,
    class: "mt-1 form-input block w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:shadow-outline-blue focus:border-blue-300 transition duration-150 ease-in-out sm:text-sm sm:leading-5 "
    <>  Map.get(@options, :text_input_class, "") ,
    minlength: @options[:minlength] ,
    maxlength: @options[:maxlength]
  %>
  <%= error_tag @f, @field %>
</div>

This is just a style suggestion, but you could use interpolation like this:

Instead of:
class="col-span-6 sm:col-span-6 <%= Map.get(@options, :label_class, "") %> "

you can do:
class="col-span-6 sm:col-span-6 #{@options[:label_class]}"

When @options[:label_class] returns nil, that will translate to "".

String interpolation example:

iex> string = "world"
iex> "hello #{string}"
"hello world"
iex> string = nil
iex> "hello #{string}"
"hello "

Thanks.

Changed the partial template to

<div class="col-span-6 sm:col-span-6 
    #{@options[:label_class]}">
  <%= label @f, @field, class: "block text-sm font-medium leading-5 text-gray-700" %>
  <%= text_input @f, @field,
    class: "mt-1 form-input block w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:shadow-outline-blue focus:border-blue-300 transition duration-150 ease-in-out sm:text-sm sm:leading-5 
    #{@options[:text_input_class]}" ,
    minlength: @options[:minlength] ,
    maxlength: @options[:maxlength]
  %>
  <%= error_tag @f, @field %>
</div>

and it compiles and works.

I just want to add in the case no options are passed to the partial template, we need to pass at least an empty map, otherwise @options will raise an exception.

<%= render "_text_input.html", f: f, field: :alpha2, options: %{} %>
1 Like

Sorry for necrobump. Found this on google.

Today the real solution is explained here: Phoenix.Component — Phoenix LiveView v0.17.5

Example:

def field_label(assigns) do
  assigns = assign_new(assigns, :help, fn -> nil end)

  ~H"""
  <label>
    <%= @text %>

    <%= if @help do %>
      <span class="help"><%= @help %></span>
    <% end %>
  </label>
  """
end
3 Likes