I’m trying to get some form styling going with tailwind in a (live) view. The approach below works, however the heex template seems to get really cluttered with repetitive code, especially as I introduce error stylings. As tailwind seems generally common within the phoenix world, I thought there might be a tailwind form field renderer already, but I was unable to find one.
<div class="col-span-6">
<%= label f, :email, class: ["block text-sm font-medium text-gray-700"] %>
<div class="mt-1 sm:mt-0 sm:col-span-2 relative rounded-md shadow-sm">
<%= email_input f, :email, required: true, autocomplete: false, value: @card.email, class: ["mt-1 block w-full shadow-sm sm:text-sm rounded-md"] ++ [(if f.errors[:email], do: "pr-10 border-red-300 text-red-900 placeholder-red-300 focus:outline-none focus:ring-red-500 focus:border-red-500", else: "focus:ring-indigo-500 focus:border-indigo-500 shadow-sm border-gray-300")] %>
<%= if f.errors[:email] do %>
<div class="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-red-500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
</div>
<% end %>
</div>
<%= if message = f.errors[:email] do %>
<p class="mt-2 text-sm text-red-600" id="email-error"><%= translate_error(message) %></p>
<% end %>
</div>
So I was thinking, coming from python webframeworks, I should probably be able to create a FormFieldComponent and hand it the form, field(name, type) and instance to render properly. Without taking fieldsets into account that would probably lead to a template with a bunch of live_component tags.
Is this a good approach, or is there a more common way to do this?
Edit: It seems petal.build kind of solved this problem, although I’m not sure they fully adopted tailwind’s styling (e.g. svg elements seem missing from formfield errors)
1 Like
I would choose another approach and simply collect all these in Tailwind components. In your assets/css there should be an app.css file with components. You can add new ones to it such as:
.form-label {
@apply
block
text-sm
font-medium
text-gray-700
}
As for your svg and error objects, it’s possible to utilise what Phoenix already provides. I’ve made an svg helper:
defmodule ProjectName.IconHelper do
@moduledoc """
Give some icons to be used on templates.
"""
use Phoenix.HTML
alias ProjectName.Router.Helpers, as: Routes
def icon_tag(name, opts \\ []) do
id = Keyword.get(opts, :id, "")
classes = Keyword.get(opts, :class, "")
content_tag(:svg, id: id, class: classes) do
tag(:use, href: Routes.static_path(ProjectName.Endpoint, "/svg/#{name}.svg"))
end
end
end
As well as an error helper based on the one Phoenix provides:
def error_label_style(form, field) do
form.errors
|> Keyword.get_values(field)
|> Enum.map(fn _error ->
"invalid-feedback-label"
end)
end
with the component
.invalid-feedback-label {
@apply
text-red-500
}
I hope this was at all helpful and that I didn’t misunderstand you too bad.
My team uses Tailwind and really enjoy it. We use a mix of components and just writing Tailwind directly into the HTML depending on whether we’re going to use this styling a lot (then it goes into a component) or if it’s single use (then it goes into the html).
2 Likes
You want to compartmentalize your forms and styling into their own function components. For example, you templates should look something like this:
<.form let={f} for={@changeset}>
<.input f={f} field={:email} type="email" value={@card.email} />
<.input f={f} field={:title} type="text />
</.form>
and you can define input functions which wraps all your styling and error tags:
def input(%{type: "email"} = assigns) do
~H"""
<%= label @f, @field, class: ["block text-sm font-medium text-gray-700"] %>
<div class="mt-1 sm:mt-0 sm:col-span-2 relative rounded-md shadow-sm">
<%= email_input @f, @field, required: true, autocomplete: false, value: @value, class: ["mt-1 block w-full shadow-sm sm:text-sm rounded-md"] ++ [(if f.errors[:email], do: "pr-10 border-red-300 text-red-900 placeholder-red-300 focus:outline-none focus:ring-red-500 focus:border-red-500", else: "focus:ring-indigo-500 focus:border-indigo-500 shadow-sm border-gray-300")] %>
<%= if @f.errors[@field] do %>
<div class="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-red-500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
</div>
<% end %>
</div>
"""
end
def input(%{type: "text"} = assigns) do
...
end
13 Likes
I think, in addition to this, better to define some @apply
directives for invalid-feedback
, form-control
etc. Otherwise, repeating those long classes for email_input
, text_input
etc. might be a lot of duplicate code - and - changing a border color or something might be difficult.
Ofcourse, that part goes into the tailwind
part of it - and - phoenix
form markup has nothing to do with it.
To each their own. With tailwind I never touch css files
No problem going that direction if you want to, but I found function components are enough to avoid duplication that I care about.
3 Likes
Not even
@label_class "block text-sm font-medium text-gray-700"
?
I mean - for the label class, if I want to add dark:text-gray-200
? This is tangential to the question and context. But, just want to pick your thought process.
1 Like
yeah to be clear, I’m saying I can de-dup within elixir rather than using css rules, for example using your label_class example and the upcoming 0.18 declarative assigns:
attr :type, :string, required: true
attr :label_class, :string, default: "block text-sm font-medium text-gray-700"
Or, you could support an optional <:label>
slot which the caller can use to pass whatever they wanted, so you have caller customization options depending on what you want.
4 Likes
Thanks for all the replies (everyone!) this is kind of what I ended up doing
1 Like
@chrismccord Is this the still best way to write an input function component? Or in view of Phoenix LiveView Tailwind Variants · Fly - using plugins a more elegant solution can be written?
3 Likes
Yes your function component would make use of this internally but the above comment still applies
4 Likes