Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
13 kB · 488 lines
Elixir
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489defmodule TrackerWeb.CoreComponents do @moduledoc """ Provides core UI components. """ use Phoenix.Component use Gettext, backend: TrackerWeb.Gettext
alias Phoenix.LiveView.JS alias TrackerWeb.Lens
@doc """ Renders a modal.
## Examples
<.modal id="confirm-modal"> This is a modal. </.modal>
JS commands may be passed to the `:on_cancel` to configure the closing/cancel event, for example:
<.modal id="confirm" on_cancel={JS.navigate(~p"/posts")}> This is another modal. </.modal>
""" attr :id, :string, required: true attr :show, :boolean, default: false attr :on_cancel, JS, default: %JS{} slot :inner_block, required: true
def modal(assigns) do ~H""" <dialog id={@id} phx-mounted={@show && show_modal(@id)} phx-remove={hide_modal(@id)} data-cancel={JS.exec(@on_cancel, "phx-remove")} > <div class="card"> <header> <button phx-click={JS.exec("data-cancel", to: "##{@id}")} type="button" aria-label={gettext("close")} rel="prev" ></button> </header> <div id={"#{@id}-content"}> {render_slot(@inner_block)} </div> </div> </dialog> """ end
@doc """ Renders flash notices.
## Examples
<.flash kind={:info} flash={@flash} /> <.flash kind={:info} phx-mounted={show("#flash")}>Welcome Back!</.flash> """ attr :id, :string, 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 :kind, :atom, values: [:info, :error], doc: "used for styling and flash lookup" attr :auto_dismiss, :boolean, default: true, doc: "whether the flash should auto-dismiss" attr :rest, :global, doc: "the arbitrary HTML attributes to add to the flash container"
slot :inner_block, doc: "the optional inner block that renders the flash message"
def flash(assigns) do assigns = assign_new(assigns, :id, fn -> "flash-#{assigns.kind}" end)
~H""" <div :if={msg = render_slot(@inner_block) || Phoenix.Flash.get(@flash, @kind)} id={@id} phx-click={JS.push("lv:clear-flash", value: %{key: @kind}) |> hide("##{@id}")} role="alert" class={[ "flash", @kind == :info && "flash-info", @kind == :error && "flash-error", @auto_dismiss && "flash-auto-dismiss" ]} {@rest} > <header :if={@title}> <strong>{@title}</strong> </header> <p>{msg}</p> <button type="button" class="close-button" aria-label={gettext("close")} > × </button> </div> """ end
@doc """ Shows the flash group with standard titles and content.
## Examples
<.flash_group flash={@flash} /> """ attr :flash, :map, required: true, doc: "the map of flash messages" attr :id, :string, default: "flash-group", doc: "the optional id of flash container"
def flash_group(assigns) do ~H""" <div id={@id} class="flash-stack"> <.flash kind={:info} title={gettext("Success!")} flash={@flash} /> <.flash kind={:error} title={gettext("Error!")} flash={@flash} /> <.flash id="client-error" kind={:error} title={gettext("We can't find the internet")} auto_dismiss={false} phx-disconnected={show(".phx-client-error #client-error")} phx-connected={hide("#client-error")} hidden > {gettext("Attempting to reconnect")} <span class="animate-spin">↻</span> </.flash>
<.flash id="server-error" kind={:error} title={gettext("Something went wrong!")} auto_dismiss={false} phx-disconnected={show(".phx-server-error #server-error")} phx-connected={hide("#server-error")} hidden > {gettext("Hang in there while we get back on track")} <span class="animate-spin">↻</span> </.flash> </div> """ end
@doc """ Renders a simple form.
## Examples
<.simple_form for={@form} phx-change="validate" phx-submit="save"> <.input field={@form[:email]} label="Email"/> <.input field={@form[:username]} label="Username" /> <:actions> <.button>Save</.button> </:actions> </.simple_form> """ attr :for, :any, required: true, doc: "the data structure for the form" attr :as, :any, default: nil, doc: "the server side parameter to collect all input under"
attr :rest, :global, include: ~w(autocomplete name rel action enctype method novalidate target multipart), 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}> <fieldset> {render_slot(@inner_block, f)} <div :for={action <- @actions}> {render_slot(action, f)} </div> </fieldset> </.form> """ end
@doc """ Renders a button.
## Examples
<.button>Send!</.button> <.button phx-click="go" class="ml-2">Send!</.button> """ attr :type, :string, default: nil attr :class, :string, default: nil attr :rest, :global, include: ~w(disabled form name value)
slot :inner_block, required: true
def button(assigns) do ~H""" <button type={@type} class={@class} {@rest}> {render_slot(@inner_block)} </button> """ end
@doc """ Renders an input with label and error messages.
A `Phoenix.HTML.FormField` may be passed as argument, which is used to retrieve the input name, id, and values. Otherwise all attributes may be passed explicitly.
## Types
This function accepts all HTML input types, considering that:
* You may also set `type="select"` to render a `<select>` tag
* `type="checkbox"` is used exclusively to render boolean values
* For live file uploads, see `Phoenix.Component.live_file_input/1`
See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input for more information. Unsupported types, such as hidden and radio, are best written directly in your templates.
## Examples
<.input field={@form[:email]} type="email" /> <.input name="my-input" errors={["oh no!"]} /> """ attr :id, :any, default: nil attr :name, :any attr :label, :string, default: nil attr :value, :any
attr :type, :string, default: "text", values: ~w(checkbox color date datetime-local email file month number password range search select tel text textarea time url week)
attr :field, Phoenix.HTML.FormField, doc: "a form field struct retrieved from the form, for example: @form[:email]"
attr :errors, :list, default: [] attr :checked, :boolean, doc: "the checked flag for checkbox inputs" attr :prompt, :string, default: nil, doc: "the prompt for select inputs" attr :options, :list, doc: "the options to pass to Phoenix.HTML.Form.options_for_select/2" attr :multiple, :boolean, default: false, doc: "the multiple flag for select inputs"
attr :rest, :global, include: ~w(accept autocomplete capture cols disabled form list max maxlength min minlength multiple pattern placeholder readonly required rows size step)
def input(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do errors = if Phoenix.Component.used_input?(field), do: field.errors, else: []
assigns |> assign(field: nil, id: assigns.id || field.id) |> assign(:errors, Enum.map(errors, &translate_error(&1))) |> assign_new(:name, fn -> if assigns.multiple, do: field.name <> "[]", else: field.name end) |> assign_new(:value, fn -> field.value end) |> input() end
def input(%{type: "checkbox"} = assigns) do assigns = assign_new(assigns, :checked, fn -> Phoenix.HTML.Form.normalize_value("checkbox", assigns[:value]) end)
~H""" <label> <input type="hidden" name={@name} value="false" disabled={@rest[:disabled]} /> <input type="checkbox" id={@id} name={@name} value="true" checked={@checked} {@rest} /> {@label} </label> <.error :for={msg <- @errors}>{msg}</.error> """ end
def input(%{type: "select"} = assigns) do ~H""" <label :if={@label} for={@id}>{@label}</label> <select id={@id} name={@name} multiple={@multiple} aria-invalid={@errors != [] && "true"} {@rest} > <option :if={@prompt} value="">{@prompt}</option> {Phoenix.HTML.Form.options_for_select(@options, @value)} </select> <.error :for={msg <- @errors}>{msg}</.error> """ end
def input(%{type: "textarea"} = assigns) do ~H""" <label :if={@label} for={@id}>{@label}</label> <textarea id={@id} name={@name} aria-invalid={@errors != [] && "true"} {@rest} >{Phoenix.HTML.Form.normalize_value("textarea", @value)}</textarea> <.error :for={msg <- @errors}>{msg}</.error> """ end
def input(assigns) do ~H""" <label :if={@label} for={@id}>{@label}</label> <input type={@type} name={@name} id={@id} value={Phoenix.HTML.Form.normalize_value(@type, @value)} aria-invalid={@errors != [] && "true"} {@rest} /> <.error :for={msg <- @errors}>{msg}</.error> """ end
@doc """ Generates a generic error message. """ slot :inner_block, required: true
def error(assigns) do ~H""" <small> {render_slot(@inner_block)} </small> """ end
@doc """ Renders a header with title. """ attr :class, :string, default: nil
slot :inner_block, required: true slot :subtitle slot :actions
def header(assigns) do ~H""" <hgroup class={@class}> <h1> {render_slot(@inner_block)} </h1> <p :if={@subtitle != []}> {render_slot(@subtitle)} </p> <div :if={@actions != []}> {render_slot(@actions)} </div> </hgroup> """ end
@doc """ Renders an inline status badge.
## Examples
<.badge variant={:danger}>Build problem</.badge> <.badge variant={:ok}>Healthy</.badge> """ attr :variant, :atom, default: :neutral, values: [:danger, :warn, :ok, :neutral] attr :rest, :global
slot :inner_block, required: true
def badge(assigns) do ~H""" <span class={"badge badge--#{@variant}"} {@rest}> {render_slot(@inner_block)} </span> """ end
@doc """ `Phoenix.Component.link/1`, with the channel lens on every internal target.
This shadows the imported `link/1` (see `TrackerWeb`) so a link cannot forget the lens and silently hand the next page a different channel than the one the visitor is looking at. External targets and links that state their own channel are left alone; see `TrackerWeb.Lens.decorate/1`. """ attr :navigate, :string, default: nil attr :patch, :string, default: nil attr :href, :any, default: nil attr :replace, :boolean, default: false attr :method, :string, default: "get" attr :csrf_token, :any, default: true attr :rest, :global, include: ~w(download hreflang referrerpolicy rel target type)
slot :inner_block, required: true
def link(assigns) do assigns |> assign(:navigate, Lens.decorate(assigns.navigate)) |> assign(:patch, Lens.decorate(assigns.patch)) |> assign(:href, Lens.decorate(assigns.href)) |> Phoenix.Component.link() end
@doc """ Renders a data list.
## Examples
<.list> <:item title="Title">{@post.title}</:item> <:item title="Views">{@post.views}</:item> </.list> """ slot :item, required: true do attr :title, :string, required: true end
def list(assigns) do ~H""" <dl> <div :for={item <- @item}> <dt><strong>{item.title}</strong></dt> <dd>{render_slot(item)}</dd> </div> </dl> """ end
## JS Commands
def show(js \\ %JS{}, selector) do JS.show(js, to: selector, time: 300) end
def hide(js \\ %JS{}, selector) do JS.hide(js, to: selector, time: 200) end
def show_modal(js \\ %JS{}, id) when is_binary(id) do js |> JS.show(to: "##{id}") |> JS.set_attribute({"open", ""}, to: "##{id}") |> JS.focus_first(to: "##{id}-content") end
def hide_modal(js \\ %JS{}, id) do js |> JS.remove_attribute("open", to: "##{id}") |> JS.hide(to: "##{id}") |> JS.pop_focus() end
@doc """ Translates an error message using gettext. """ def translate_error({msg, opts}) do if count = opts[:count] do Gettext.dngettext(TrackerWeb.Gettext, "errors", msg, msg, count, opts) else Gettext.dgettext(TrackerWeb.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}) endend