Created
March 31, 2023 10:41
-
-
Save filipecabaco/d3b2de890d546db59838b80b90d7be6b to your computer and use it in GitHub Desktop.
Simple component to build forms from a given schema
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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={[email protected]?}><%= 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