247 lines
6.1 KiB
Elixir
247 lines
6.1 KiB
Elixir
defmodule Web.CoreComponents do
|
|
@moduledoc """
|
|
Provides core UI components.
|
|
"""
|
|
use Phoenix.Component
|
|
|
|
alias Phoenix.HTML.FormField
|
|
|
|
attr :id, :any, default: nil
|
|
attr :name, :any
|
|
attr :label, :string, default: nil
|
|
attr :value, :any
|
|
attr :type, :string, default: "text", values: ~w[text password textarea]
|
|
attr :field, FormField
|
|
attr :errors, :list, default: []
|
|
attr :rest, :global, include: ~w[disabled form pattern placeholder readonly required]
|
|
|
|
def input(%{field: %FormField{} = field} = assigns) do
|
|
errors =
|
|
if Phoenix.Component.used_input?(field) do
|
|
field.errors
|
|
else
|
|
[]
|
|
end
|
|
|
|
assigns
|
|
|> assign(
|
|
field: nil,
|
|
id: assigns.id || field.id,
|
|
errors: Enum.map(errors, &translate_error/1)
|
|
)
|
|
|> assign_new(:name, fn -> field.name end)
|
|
|> assign_new(:value, fn -> field.value end)
|
|
|> input()
|
|
end
|
|
|
|
def input(%{type: "textarea"} = assigns) do
|
|
~H"""
|
|
<div>
|
|
<.label for={@id}>{@label}</.label>
|
|
<textarea id={@id} name={@name}>{Phoenix.HTML.Form.normalize_value(@type, @value)}</textarea>
|
|
<.error :for={error <- @errors}>{error}</.error>
|
|
</div>
|
|
"""
|
|
end
|
|
|
|
def input(assigns) do
|
|
~H"""
|
|
<div>
|
|
<.label for={@id}>{@label}</.label>
|
|
<input
|
|
id={@id}
|
|
type={@type}
|
|
name={@name}
|
|
value={Phoenix.HTML.Form.normalize_value(@type, @value)}
|
|
{@rest}
|
|
/>
|
|
<.error :for={error <- @errors}>{error}</.error>
|
|
</div>
|
|
"""
|
|
end
|
|
|
|
attr :for, :string, default: nil
|
|
slot :inner_block, required: true
|
|
|
|
def label(assigns) do
|
|
~H"""
|
|
<label for={@for}>
|
|
{render_slot(@inner_block)}
|
|
</label>
|
|
"""
|
|
end
|
|
|
|
slot :inner_block, required: true
|
|
|
|
def error(assigns) do
|
|
~H"""
|
|
<p>
|
|
<.icon name="hero-exclamation-circle-mini" class="h-5 w-5 flex-none" />
|
|
{render_slot(@inner_block)}
|
|
</p>
|
|
"""
|
|
end
|
|
|
|
attr :name, :string, required: true
|
|
attr :class, :string, default: nil
|
|
|
|
def icon(%{name: "hero-" <> _} = assigns) do
|
|
~H"""
|
|
<span class={[@name, @class]} />
|
|
"""
|
|
end
|
|
|
|
def translate_error({msg, opts}) do
|
|
Enum.reduce(opts, msg, fn {key, value}, acc ->
|
|
String.replace(acc, "%{#{key}}", fn _ -> to_string(value) end)
|
|
end)
|
|
end
|
|
|
|
def translate_errors(errors, field) when is_list(errors) do
|
|
for {^field, {msg, opts}} <- errors, do: translate_error({msg, opts})
|
|
end
|
|
|
|
attr :type, :string, default: "button", values: ~w[button submit]
|
|
attr :rest, :global
|
|
slot :inner_block, required: true
|
|
|
|
def button(assigns) do
|
|
~H"""
|
|
<button type={@type} {@rest}>
|
|
{render_slot(@inner_block)}
|
|
</button>
|
|
"""
|
|
end
|
|
|
|
attr :format, :string, required: true
|
|
attr :value, :any, default: nil
|
|
attr :formatter, :atom, default: :default
|
|
attr :timezone, :string, default: "America/New_York"
|
|
attr :global, :global
|
|
|
|
def timex(%{value: nil} = assigns) do
|
|
~H"""
|
|
<time datetime="">--</time>
|
|
"""
|
|
end
|
|
|
|
def timex(%{value: value, timezone: timezone} = assigns) do
|
|
assigns =
|
|
assign_new(assigns, :local_value, fn ->
|
|
case value do
|
|
%DateTime{} = datetime ->
|
|
datetime
|
|
|
|
%NaiveDateTime{} = naive ->
|
|
naive
|
|
|> DateTime.from_naive!("Etc/UTC")
|
|
|> DateTime.shift_zone!(timezone)
|
|
end
|
|
end)
|
|
|
|
~H"""
|
|
<time
|
|
datetime={Timex.format!(@local_value, "{ISO:Extended}")}
|
|
title={Timex.format!(@local_value, "{Mshort} {D}, {YYYY}, {h12}:{m} {AM} {Zabbr}")}
|
|
{@global}
|
|
>
|
|
{Timex.format!(@local_value, @format, timex_formatter(@formatter))}
|
|
</time>
|
|
"""
|
|
end
|
|
|
|
defp timex_formatter(formatter) do
|
|
Module.concat(Timex.Format.DateTime.Formatters, :string.titlecase("#{formatter}"))
|
|
end
|
|
|
|
@doc """
|
|
Renders markdown content as HTML.
|
|
|
|
## Examples
|
|
|
|
<.markdown content={@post.body} />
|
|
"""
|
|
attr :content, :string, required: true
|
|
attr :class, :string, default: ""
|
|
|
|
def markdown(assigns) do
|
|
~H"""
|
|
<div class={["markdown-content", @class]}>
|
|
{Phoenix.HTML.raw(Web.Markdown.to_html(@content))}
|
|
</div>
|
|
"""
|
|
end
|
|
|
|
@doc """
|
|
Renders pagination controls for navigating through a paginated list.
|
|
|
|
## Examples
|
|
|
|
<.pagination meta={@meta} path={~p"/blog"} schema={Schema.Post} />
|
|
"""
|
|
attr :meta, :map, required: true, doc: "the pagination metadata from Flop"
|
|
attr :path, :string, required: true, doc: "the base path for pagination links"
|
|
attr :schema, :atom, required: true, doc: "the schema module for Flop.Phoenix.build_path"
|
|
attr :class, :string, default: "mt-4", doc: "additional CSS classes"
|
|
|
|
def pagination(assigns) do
|
|
~H"""
|
|
<div class={["flex justify-between", @class]}>
|
|
<%= if @meta.has_previous_page? do %>
|
|
<.link
|
|
navigate={Flop.Phoenix.build_path(@path, Flop.to_previous_cursor(@meta), for: @schema)}
|
|
class="text-gray-500 hover:text-gray-800"
|
|
>
|
|
Newer posts
|
|
</.link>
|
|
<% else %>
|
|
<div></div>
|
|
<% end %>
|
|
|
|
<%= if @meta.has_next_page? do %>
|
|
<.link
|
|
navigate={Flop.Phoenix.build_path(@path, Flop.to_next_cursor(@meta), for: @schema)}
|
|
class="text-gray-500 hover:text-gray-800"
|
|
>
|
|
Older posts
|
|
</.link>
|
|
<% else %>
|
|
<div></div>
|
|
<% end %>
|
|
</div>
|
|
"""
|
|
end
|
|
|
|
attr :id, :string, required: true
|
|
attr :stream, Phoenix.LiveView.LiveStream, required: true
|
|
attr :class, :string, default: nil
|
|
attr :rest, :global
|
|
|
|
slot :col do
|
|
attr :label, :string
|
|
attr :class, :string
|
|
end
|
|
|
|
def table(assigns) do
|
|
~H"""
|
|
<table id={@id} class={["border-collapse", @class]} {@rest}>
|
|
<thead>
|
|
<tr>
|
|
<%= for col <- @col do %>
|
|
<th class="border p-2">{col[:label]}</th>
|
|
<% end %>
|
|
</tr>
|
|
</thead>
|
|
<tbody id={"#{@id}-stream"} phx-update="stream">
|
|
<%= for {dom_id, item} <- @stream do %>
|
|
<tr id={dom_id}>
|
|
<%= for col <- @col do %>
|
|
<td class={["border p-2", col[:class]]}>{render_slot(col, item)}</td>
|
|
<% end %>
|
|
</tr>
|
|
<% end %>
|
|
</tbody>
|
|
</table>
|
|
"""
|
|
end
|
|
end
|