Change how the admin looks
v0.0.1
Gervaz draws with a UI adapter, a module of 69 component functions such as button/1, text_input/1 and badge/1. The default adapter draws plain Tailwind CSS. This guide changes how a table and a form look, from a single class in the DSL up to an adapter of your own. The examples come from the layouts table, and from the Review and Admin adapters of Mishka CMS. The Dynamic adapter example comes from the Gervaz module documentation.
Before you begin
- Know the split between the two layers. A template decides where things go. An adapter decides how each piece looks. Swapping one never forces the other. See How Gervaz works.
- Use the lowest layer that solves the problem. A class in the DSL costs the least, and an adapter costs more to maintain.
Set a class where the element is declared
Most entities have a ui block with class options. A column has class for its cells and header_class for its header, and also width. A form group has class. A row action has class. A form field also has class and wrapper_class, but the Standard form template does not read them.
A row gets a class per record with row class. List every class that apply can return in possible. Tailwind scans source files, not run-time values, so listing the literals keeps them in the build. The layouts table tints the rows of global layouts:
row do
selectable true
class do
possible ["bg-[#f6f4fd]"]
apply fn record -> if is_nil(record.site_id), do: "bg-[#f6f4fd]" end
end
endThe presentation section has a theme block. The plain table template reads two of its keys, header_class and row_class. The built-in form template does not read the form theme keys, so use the ui blocks of the fields, groups and buttons for a form. Domain-level theme blocks set the same table keys for every table.
Choose the adapter
Set the adapter once on the domain, and every resource inherits it. A resource names its own in the presentation section when it differs:
presentation do
template MishkaBlogWeb.Templates.TagCard
ui_adapter MishkaCmsCoreWeb.UIAdapters.Admin
endOne adapter serves both the table and the form. The form-side presentation section takes the same ui_adapter option. Name the adapter in any resource that writes a presentation block, so the choice does not depend on inheritance.
Write an adapter that changes some components
use MishkaGervaz.Behaviours.UIAdapter writes a defdelegate to a fallback module for each of the 69 functions and marks them overridable. An adapter that defines nothing behaves like its fallback, and an adapter that defines button/1 changes buttons and nothing else. The fallback defaults to MishkaGervaz.UIAdapters.Tailwind, and it can be another adapter.
The Review adapter of Mishka CMS restyles five components for a 360px sidebar, badge/1, textarea/1, select/1, button/1 and empty_state/1, and leaves the rest to Tailwind. This is its button/1, shortened:
defmodule MishkaCmsCoreWeb.UIAdapters.Review do
use MishkaGervaz.Behaviours.UIAdapter,
fallback: MishkaGervaz.UIAdapters.Tailwind
attr :label, :string, default: nil
attr :class, :string, default: nil
attr :type, :string, default: "button"
attr :icon, :string, default: nil
attr :variant, :atom, default: :default
attr :rest, :global,
include:
~w(phx-click phx-target phx-value-id phx-value-event phx-value-values data-confirm disabled)
def button(assigns) do
~H"""
<button type={@type} class={@class} title={@label} {@rest}>
<span :if={@label not in [nil, ""]} class="lbl">{@label}</span>
</button>
"""
end
endThree habits keep an adapter from fighting the DSL:
- Honor a
classthat the caller passes. The DSL'sui do class "..." endmust win, or the adapter becomes a wall the resource cannot get past. - Always put the label in the DOM, even when the design hides it. A template or a class such as
[&_.lbl]:hiddenmay decide to hide it. - Declare
attr :rest, :globalwith thephx-*anddata-confirmattributes you need, so events reach the element.
The full list of functions is in Behaviours. MishkaGervaz.Behaviours.UIAdapter.component_functions/0 returns it at run time.
Read a field's extra options in an adapter
The form template passes the extra map of the field's ui block to the adapter component as an assign. The Admin adapter of Mishka CMS draws a rich-text editor for a textarea whose extra says so, and a code editor for one with a language:
field :content, :textarea do
ui do
label fn -> dgettext("mishka_gervaz", "Content") end
rows 8
extra %{content_editor: true}
end
enddef textarea(%{extra: %{code: language}} = assigns) when is_binary(language) do
...
end
def textarea(%{extra: %{content_editor: true}} = assigns) do
...
end
def textarea(assigns), do: super(assigns)Defining textarea/1 takes the whole function over from the fallback. The last clause calls super(assigns) so that every other textarea reaches the fallback. Without it, a plain textarea on the same form raises FunctionClauseError. Gervaz does not check the keys of extra, so read the module that consumes them before you fill one.
Build an adapter from your own components
If your application already has a components module with matching names, wire it with components:. Gervaz connects each function only when the module is loaded and exports the matching one-argument function.
defmodule MyAppWeb.UIAdapter do
use MishkaGervaz.Behaviours.UIAdapter,
components: MyAppWeb.Components
endnested_components: true looks for MyAppWeb.Components.Button.button/1. With nested_components: true, module_prefix: "Mishka" looks for Components.MishkaButton.button/1. component_prefix: "mc_" looks for mc_button/1. In a table's presentation, ui_adapter_opts component_module: MyAppWeb.Components makes Gervaz generate an adapter named after the resource, <Resource>.GervazUIAdapter, from the same options.
Resolve components at run time
MishkaGervaz.UIAdapters.Dynamic renders components looked up at run time and falls back per component when one is unavailable. Give it a renderer and a resolver:
presentation do
ui_adapter MishkaGervaz.UIAdapters.Dynamic
ui_adapter_opts site: "Global",
component_renderer: &MyApp.LiveViewHelpers.component/1,
module_resolver: &MyApp.Compilers.Helpers.module_name/3,
fallback: MishkaGervaz.UIAdapters.Tailwind
endNext steps
- Write a table or form template: change where things go.
- Behaviours: the 69 component functions.
- Table layout and presentation DSL: the options used above.