defmodule ChirpWeb.Components do
@moduledoc """
Provides core UI components.
The components in this module use Tailwind CSS, a utility-first CSS framework.
See the [Tailwind CSS documentation](https://tailwindcss.com) to learn how to
customize the generated components in this module.
Icons are provided by [heroicons](https://heroicons.com), using the
[heroicons_elixir](https://github.com/mveytsman/heroicons_elixir) project.
"""
use Phoenix.Component
import ChirpWeb.Gettext, warn: false
alias Phoenix.LiveView.JS
@doc """
Renders a modal.
## Examples
<.modal id="confirm-modal">
Are you sure?
<:confirm>OK
<:cancel>Cancel
<.modal>
JS commands may be passed to the `:on_cancel` and `on_confirm` attributes
for the caller to reactor to each button press, for example:
<.modal id="confirm" on_confirm={JS.push("delete")} on_cancel={JS.navigate(~p"/posts")}>
Are you sure you?
<:confirm>OK
<:cancel>Cancel
<.modal>
"""
attr :id, :string, required: true
attr :show, :boolean, default: false
attr :on_cancel, JS, default: %JS{}
attr :on_confirm, JS, default: %JS{}
slot :inner_block, required: true
slot :title
slot :subtitle
slot :confirm
slot :cancel
def modal(assigns) do
~H"""
"""
end
@doc """
Renders flash notices.
## Examples
<.flash kind={:info} flash={@flash} />
<.flash kind={:info} phx-mounted={show("#flash")}>Welcome Back!
"""
attr :id, :string, default: "flash", doc: "the optional id of flash container"
attr :flash, :map, default: %{}, doc: "the map of flash messages to display"
attr :title, :string, default: nil
attr :rest, :global
attr :kind, :atom, doc: "one of :info, :error used for styling and flash lookup"
attr :autoshow, :boolean, default: true, doc: "whether to auto show the flash on mount"
attr :close, :boolean, default: true, doc: "whether the flash can be closed"
slot :inner_block, doc: "the optional inner block that renders the flash message"
def flash(assigns) do
~H"""
"""
end
@doc """
Renders a simple form.
## Examples
<.simple_form :let={f} for={:user} phx-change="validate" phx-submit="save">
<.input field={{f, :email}} label="Email"/>
<.input field={{f, :username}} label="Username" />
<:actions>
<.button>Save
<:actions>
"""
attr :for, :any, default: nil, doc: "the datastructure for the form"
attr :as, :any, default: nil, doc: "the server side parameter to collect all input under"
attr :rest, :global, doc: "the arbitrary HTML attributes to apply to the form tag"
slot :inner_block, required: true
slot :actions, doc: "the slot for form actions, such as a submit button"
def simple_form(assigns) do
~H"""
<.form :let={f} for={@for} as={@as} {@rest}>
<%= render_slot(@inner_block, f) %>
<%= render_slot(action, f) %>
"""
end
@doc """
Renders a button.
## Examples
<.button>Send!
<.button phx-click="go" class="ml-2">Send!
"""
attr :type, :string, default: nil
attr :class, :string, default: nil
attr :rest, :global, doc: "the arbitrary HTML attributes to apply to the button tag"
slot :inner_block, required: true
def button(assigns) do
~H"""
"""
end
@doc """
Renders an input with label and error messages.
A `%Phoenix.HTML.Form{}` and field name may be passed to the input
to build input names and error messages, or all the attributes and
errors may be passed explicitly.
## Examples
<.input field={{f, :email}} type="email" />
<.input name="my-input" errors={["oh no!"]} />
"""
attr :id, :any
attr :name, :any
attr :label, :string, default: nil
attr :type, :string,
default: "text",
doc: ~s|one of "text", "textarea", "number" "email", "date", "time", "datetime", "select"|
attr :value, :any
attr :field, :any, doc: "a %Phoenix.HTML.Form{}/field name tuple, for example: {f, :email}"
attr :errors, :list
attr :rest, :global, doc: "the arbitrary HTML attributes for the input tag"
slot :inner_block
slot :option, doc: "the slot for select input options"
def input(%{field: {f, field}} = assigns) do
assigns
|> assign(field: nil)
|> assign_new(:name, fn -> Phoenix.HTML.Form.input_name(f, field) end)
|> assign_new(:id, fn -> Phoenix.HTML.Form.input_id(f, field) end)
|> assign_new(:value, fn -> Phoenix.HTML.Form.input_value(f, field) end)
|> assign_new(:errors, fn -> translate_errors(f.errors || [], field) end)
|> input()
end
def input(%{type: "checkbox"} = assigns) do
~H"""
"""
end
def input(%{type: "select"} = assigns) do
~H"""
"""
end
## JS Commands
def show(js \\ %JS{}, selector) do
JS.show(js,
to: selector,
transition:
{"transition-all transform ease-out duration-300",
"opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95",
"opacity-100 translate-y-0 sm:scale-100"}
)
end
def hide(js \\ %JS{}, selector) do
JS.hide(js,
to: selector,
time: 200,
transition:
{"transition-all transform ease-in duration-200",
"opacity-100 translate-y-0 sm:scale-100",
"opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"}
)
end
def show_modal(js \\ %JS{}, id) when is_binary(id) do
js
|> JS.show(to: "##{id}")
|> JS.show(
to: "##{id}-bg",
transition: {"transition-all transform ease-out duration-300", "opacity-0", "opacity-100"}
)
|> show("##{id}-container")
|> JS.focus_first(to: "##{id}-content")
end
def hide_modal(js \\ %JS{}, id) do
js
|> JS.hide(
to: "##{id}-bg",
transition: {"transition-all transform ease-in duration-200", "opacity-100", "opacity-0"}
)
|> hide("##{id}-container")
|> JS.hide(to: "##{id}", transition: {"block", "block", "hidden"})
|> JS.pop_focus()
end
@doc """
Translates an error message using gettext.
"""
def translate_error({msg, opts}) do
# When using gettext, we typically pass the strings we want
# to translate as a static argument:
#
# # Translate "is invalid" in the "errors" domain
# dgettext("errors", "is invalid")
#
# # Translate the number of files with plural rules
# dngettext("errors", "1 file", "%{count} files", count)
#
# Because the error messages we show in our forms and APIs
# are defined inside Ecto, we need to translate them dynamically.
# This requires us to call the Gettext module passing our gettext
# backend as first argument.
#
# Note we use the "errors" domain, which means translations
# should be written to the errors.po file. The :count option is
# set by Ecto and indicates we should also apply plural rules.
if count = opts[:count] do
Gettext.dngettext(ChirpWeb.Gettext, "errors", msg, msg, count, opts)
else
Gettext.dgettext(ChirpWeb.Gettext, "errors", msg, opts)
end
end
@doc """
Translates the errors for a field from a keyword list of errors.
"""
def translate_errors(errors, field) when is_list(errors) do
for {^field, {msg, opts}} <- errors, do: translate_error({msg, opts})
end
end