hvanderheide

hvanderheide

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)

Showing Posts 1 to 10

MariaKir

MariaKir

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. :sweat_smile:
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).

chrismccord

chrismccord

Creator of Phoenix

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
Post #2
cvkmohan

cvkmohan

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.

chrismccord

chrismccord

Creator of Phoenix

To each their own. With tailwind I never touch css files :slight_smile:
No problem going that direction if you want to, but I found function components are enough to avoid duplication that I care about.

cvkmohan

cvkmohan

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.

chrismccord

chrismccord

Creator of Phoenix

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.

hvanderheide

hvanderheide OP

Thanks for all the replies (everyone!) this is kind of what I ended up doing

cvkmohan

cvkmohan

@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?

chrismccord

chrismccord

Creator of Phoenix

Yes your function component would make use of this internally but the above comment still applies

cvkmohan

cvkmohan

@chrismccord Now that Phoenix uses used_field and depracated phx-feedback-for - how do we use it to show a tick mark when the input is valid?
In the above snippet that you have given, when there is an error it shows an error icon - inside the textbox. I want to show a tick icon when the field is valid - and - the field stays normal when it is untouched.

Where Next? Top

Trending in Questions Top

stjefim
Hello! Suppose you are building workflow (order / task / payment) processing system with the following requirements: Each workflow con...
New
Blokh
Hey guys, I’ve got a huge CSV ( around 10 GB ) that needs to be processed hourly Do you guys have any suggestions what is the best prac...
New
roeland
Kia ora, We have been using elixir-google-api to connect to Google Drive. However, with the updates to Tesla due to CVEs this is now bro...
New
kszambelanczyk
Hello! Could someone please give me a help/sample code, how to delete a file from s3 using waffle/waffle_ecto from Phoenix app. I creat...
New
Onor.io
I have what I’ve heard referred to as a “lookup table” in my database. This is a way of assigning codes to common values. One common lo...
New
jaybe78
Hello, I’m developing a online persistent chat system (what’s app) like using elixir/dynamodb/aws for a mobile app(flutter). The diffic...
New
Trolleger
What approach to take when sending live updates to “random” users Hi! I have a question, I have a little chat app, and when I create a DM...
New

Other Trending Topics Top

garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
mcass19
ExRatatui lets you cook up rich terminal UIs in Elixir, powered by Rust’s ratatui via Rustler NIFs. Build interactive terminal applicatio...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge &amp; Solve. They are GUI (Emerge) and State management (S...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New
wintermeyer
There are three potential reasons for members of this forum to have a look at https://vutuv.de You are tired or annoyed of LinkedIn. Yo...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews