Skip to content

Instantly share code, notes, and snippets.

@filipecabaco
Created March 31, 2023 10:41
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save filipecabaco/d3b2de890d546db59838b80b90d7be6b to your computer and use it in GitHub Desktop.
Save filipecabaco/d3b2de890d546db59838b80b90d7be6b to your computer and use it in GitHub Desktop.
Simple component to build forms from a given schema
defmodule AppWeb.Component.FormFromSchema do
use AppWeb, :live_component
attr :changeset, :any, required: true
attr :event_suffix, :string, default: ""
attr :schema, :any, required: true
attr :uploads, :any, default: nil
attr :visibility_rules, :any, default: %{}
attr :field_type_rules, :any, default: %{}
def render(assigns) do
~H"""
<div class="form-container">
<.simple_form :let={f} for={%{}} as={@schema} phx-change={"validate-#{assigns[:event_suffix]}"} phx-submit={"submit-#{assigns[:event_suffix]}"}>
<%= generate_inputs(f, @schema, assigns) %>
<%= if @uploads do %>
<label
for={@uploads.attachments.ref}
class="w-full min-h-[12rem] bg-slate-100 rounded-xl shadow flex flex-wrap p-2 gap-2 justify-center items-center cursor-pointer"
phx-drop-target={@uploads.attachments.ref}
>
<.live_file_input upload={@uploads.attachments} class="hidden" />
<%= if @uploads.attachments.entries == [] do %>
<label class="cursor-pointer">Drag and drop or Click to upload files</label>
<% end %>
<%= for entry <- @uploads.attachments.entries do %>
<div class="flex flex-col align-center justify-center bg-white rounded shadow p-2 w-[128px] h-[128px]">
<%= if entry.progress < 100 do %>
<div class="w-[64px] bg-gray-200 rounded-full h-1.5 mb-4 dark:bg-gray-700">
<div class="bg-blue-600 h-1.5 rounded-full dark:bg-blue-500" style={"width: #{entry.progress}%"}></div>
</div>
<% else %>
<%= if String.contains?(entry.client_type, "image/") do %>
<.live_img_preview entry={entry} class="max-h-[64px]" />
<% else %>
<Heroicons.document class="max-h-[64px]" />
<% end %>
<div class="truncate"><%= entry.client_name %></div>
<% end %>
</div>
<% end %>
</label>
<% end %>
<:actions>
<.button disabled={!@changeset.valid?}><%= dgettext("application", "Save") %></.button>
<.button type="button" phx-click="modal" phx-value-action="close"><%= gettext("Close") %></.button>
</:actions>
</.simple_form>
</div>
"""
end
defp generate_inputs(f, schema, assigns) do
visibility_rules = Map.get(assigns, :visibility_rules)
field_type_rules = Map.get(assigns, :field_type_rules)
assigns = assign(assigns, :form, f)
assigns =
schema.__schema__(:fields)
|> Enum.reject(&(&1 |> Atom.to_string() |> String.contains?("id")))
|> Enum.reject(&(&1 |> Atom.to_string() |> String.contains?("_at")))
|> Enum.map(fn field ->
type = schema.__schema__(:type, field)
visibility_rule = Map.get(visibility_rules, field)
field_type_rule = Map.get(field_type_rules, field)
generate_input(assigns, field, type, visibility_rule, field_type_rule)
end)
|> then(&assign(assigns, :inputs, &1))
|> assign(:errors, assigns.changeset.errors |> Enum.map(fn {field, {msg, _}} -> "#{field_to_label(field)} #{msg}" end))
~H"""
<%= for input <- @inputs do %>
<%= input %>
<% end %>
<%= for error <- @errors do %>
<.error><%= error %></.error>
<% end %>
"""
end
defp generate_input(assigns, field, type, visibility_rule, field_type_rule) do
changeset = assigns.changeset
assigns =
assigns
|> assign(:field, field)
|> assign(:type, type)
|> assign(:visibility_rule, visibility_rule)
|> assign(:value, changeset.changes[field])
|> assign(:label, field_to_label(field))
|> assign(:field_type_rule, field_type_rule)
~H"""
<%= if(@visibility_rule && @visibility_rule.(assigns.changeset) || @visibility_rule == nil) do %>
<.input_field form={@form} label={@label} field={@field} type={@type} value={@value} field_type_rule={@field_type_rule} />
<% end %>
"""
end
defp input_field(%{type: :integer} = assigns) do
~H"""
<.input field={{@form, @field}} label={@label} phx-debounce="blur" value={@value} inputmode="numeric" type={@field_type_rule || "text"} />
"""
end
defp input_field(%{type: :date} = assigns) do
~H"""
<.input field={{@form, @field}} label={@label} phx-debounce="blur" value={@value} type={@field_type_rule || "date"} />
"""
end
defp input_field(%{type: :boolean} = assigns) do
~H"""
<.input field={{@form, @field}} label={@label} phx-debounce="blur" value={@value} type={@field_type_rule || "checkbox"} />
"""
end
defp input_field(%{type: {:parameterized, Ecto.Enum, %{mappings: mappings}}} = assigns) do
assigns =
mappings
|> Enum.map(fn {key, label} -> {Gettext.dgettext(AppWeb.Gettext, "application", String.capitalize(label)), key} end)
|> then(&assign(assigns, :options, &1))
~H"""
<.input field={{@form, @field}} options={@options} label={@label} value={@value} type={@field_type_rule || "select"} />
"""
end
defp input_field(%{type: _} = assigns) do
~H"""
<.input field={{@form, @field}} label={@label} phx-debounce="blur" value={@value} type={@field_type_rule || "text"} />
"""
end
defp field_to_label(field) do
field
|> Atom.to_string()
|> String.capitalize()
|> String.replace("_", " ")
|> then(&Gettext.dgettext(AppWeb.Gettext, "application", &1))
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment