View Source LiveUI.Protocol.Utils (LiveUI v0.1.0)
Utility functions for modifying default LiveUI implementation.
Summary
Functions
Adds custom action to Index or Show view that points to user defined Phoenix.LiveComponent module.
Adds custom batch action to Index view that points to user defined Phoenix.LiveComponent module.
Format fields with CSS classes, functions or components.
Change field's default input type.
Don't show fields in Index or Show view.
Don't show fields in actions.
All fields are required in Ecto.Changeset unless they are marked as optional.
Functions
Adds custom action to Index or Show view that points to user defined Phoenix.LiveComponent module.
Custom component will be accessible with an action button and its content will be rendered inside the modal.
The action's URL is generated as /<namespace>/<resource>/<id>/<action-key> and is set automatically by LiveUI.Router.live_ui/3.
The component for Show view has access to @record assign that holds database record.
Add custom :api_key action to Index view:
defimpl LiveUI, for: MyApp.Member.Contact do
use LiveUI.Protocol
def index_view(record) do
super(record)
|> add_action(:api_key, "Get api key", MyAppWeb.Member.ContactLive.ApiKey)
end
endCustom component that generates the api key could use @current_user from the socket:
defmodule MyAppWeb.Member.ContactLive.ApiKey do
use Phoenix.LiveComponent
import LiveUI.Components.Core
def update(assigns, socket) do
{:ok, socket |> assign(assigns) |> assign(:api_key, false)}
end
def render(assigns) do
~H"""
<div>
<.p>Use this key to access our API</.p>
<.h3>Get api key</.h3>
<.p :if={@api_key}>
<LiveUI.Formatters.copy class="font-bold" field="api-key" value={@api_key} />
</.p>
<.p :if={!@api_key}>
<.button variant="outline" phx-click="get_api_key" phx-target={@myself}>
Generate new key
</.button>
</.p>
<.p class="my-4">
Key will expire in 180 days. Notification will be sent to <%= @current_user.email %>.
</.p>
<.link patch={@index_path}>Back</.link>
</div>
"""
end
def handle_event("get_api_key", _params, socket) do
{
:noreply,
socket |> assign(api_key: MyApp.Member.User.get_api_key(socket.assigns.current_user))
}
end
endLiveUI.Formatters.copy/1 is built-in component that shows an icon next to the value to copy it to the clipboard.
p and h3 components are from LiveUI.Components.Core which delegates the calls to PetalComponents.
It is also possible to add extra custom values to the socket assigns of the parent view by overriding mount callback,
current_user will be automatically available if set in Phoenix.LiveView.Router.live_session/3.
By default :allowed field is set to true; it also could point to a function that controls who has the access to the component.
Adds custom batch action to Index view that points to user defined Phoenix.LiveComponent module.
Custom component will be accessible with an action button and its content will be rendered inside the modal.
The action's URL is generated as /<namespace>/<resources>/<action-key> and is set automatically by LiveUI.Router.live_ui/3.
The component has access to @selected assign that holds ids of selected records.
Add custom :deactivate batch action:
defimpl LiveUI, for: MyApp.Admin.User do
def index_view(user) do
super(user)
|> add_batch_action(:deactivate, "Deactivate", MyAppWeb.Admin.UserLive.Deactivate)
end
endCustom component that will deactivate users:
defmodule LiveUIWeb.Admin.UserLive.Deactivate do
@moduledoc false
use Phoenix.LiveComponent
alias Phoenix.LiveView.JS
import LiveUI.Components.Core
import Ecto.Query
def update(assigns, socket) do
users =
from(users in LiveUI.Admin.User, where: users.id in ^assigns.selected)
|> LiveUI.Config.repo().all
{:ok, socket |> assign(assigns) |> assign(:users, users)}
end
def render(assigns) do
~H"""
<div>
<.p>Deactivating multiple users</.p>
<.h3>
Are you sure you want to deactivate <%= length(@selected) %> user(s)?
</.h3>
<.ul class="my-6">
<li :for={user <- @users}>
<%= user.name %> (<%= user.email %>)
</li>
</.ul>
<.button link_type="live_patch" to={@index_path} variant="outline">
Back
</.button>
<.link
class="px-2 text-red-600"
tabindex="-1"
phx-target={@myself}
phx-click={JS.push("deactivate", value: %{selected: @selected})}
>
Deactivate
</.link>
</div>
"""
end
def handle_event("deactivate", %{"selected" => selected}, socket) do
from(users in LiveUI.Admin.User, where: users.id in ^selected)
|> LiveUI.Config.repo().update_all(set: [active: false])
{:noreply,
socket
|> put_flash(:info, "#{length(selected)} user(s) are deactivated successfully.")
|> push_navigate(to: socket.assigns.index_path)}
end
end
Format fields with CSS classes, functions or components.
Multiple formatters for a field are set as a list of formatters.
import LiveUI.Formatters
def index_view(user) do
super(user)
|> add_formatters(
email: {&mask/2, [left: 4]},
bio: &markdown/1,
website: {&link_/1, %{name: "Web"}},
role: &String.upcase/1
)
endNOTE: Formatting enum field will also format its values in dropdown input in related form. This could be improved in future versions with support for custom input components.
Formatting with CSS class
When formatter is a string it will wrap the value with a span element with CSS class.
def index_view(user) do
super(user)
|> add_formatters(email: "text-green-700")
endFormatting with a function
Field value is passed as an argument to a function with arity of 1.
def index_view(user) do
super(user)
|> add_formatters(role: &String.upcase/1)
endRecord and field values are passed to a function with arity of 2.
# show prices as $1,234.00
def index_view(product) do
super(product)
|> ignore_fields([:currency])
|> add_formatters(price: &MyAppWeb.Formatters.money/2)
end
def money(record, value) do
Money.new(Map.get(record, :currency), value)
endExtra options are set as keyword list in 2-tuple.
def index_view(user) do
super(user)
|> add_formatters(email: {&mask/2, [left: 4]})
endFormatting with a component
If the function has a map as an argument it is treated as a component.
Component assigns are passed as %{field: field, value: value, record: record}.
# built-in component
def index_view(user) do
super(user)
|> add_formatters(email: &LiveUI.Formatters.copy/1
end
# custom component
def index_view(user) do
super(user)
|> add_formatters(email: &MyAppWeb.Components.email/1
endExtra options are set as map in 2-tuple.
def index_view(user) do
super(user)
|> add_formatters(website: {&link/1, %{name: "Web"}})
end
Change field's default input type.
# change input from text to textarea
def index_view(company) do
super(company)
|> configure_inputs([:new], description: "textarea")
endNOTE: - textarea is currently used for map and array ecto types until we add configurable components for inputs.
Don't show fields in Index or Show view.
def index_view(user) do
super(user)
|> ignore_fields([:updated_at, :inserted_at])
end
Don't show fields in actions.
# remove :confirmed_at from create form
def index_view(user) do
super(user)
|> ignore_fields(:new, [:confirmed_at])
end
# remove :email and :company_id from edit form
def show_view(user) do
super(user)
|> ignore_fields([:edit], [:email, :company_id])
endNOTE: fields can be disabled globally in config.exs:
config :live_ui,
ignored_fields: [:token, :hashed_password, :first_version_id, :current_version_id]
All fields are required in Ecto.Changeset unless they are marked as optional.
def index_view(user) do
super(user)
|> set_optional_fields(:new, [:age])
end