WIP (pre-alpha) Ash UI Extension & Component Lib
frankdugan3:
Hey all. π
Iβm happy to
tease
the near public release of
Plegethon
(provisional title), a (very experimental) Ash UI extension and
LiveView
component library!
Itβs inspired by
Petal
,
AshAdmin
, and
AshAuthenticationPhoenix
. Iβm trying very hard to design in the ability to configure/extend nearly every aspect of the components, and to have an Ash-core level of escape hatches/overrides in the extension config. The idea is that you can declare UI config in your Ash resource, then use that in βsmart componentsβ that know how to introspect the Ash resource to build themselves. Kind of like
AshPhoenix.Form
w/
auto: true
, but for the UI (examples in images).
The override system for components is very similar to
AshAuthenticationPhoenix
, if youβve used that. It adds some other goodies like taking in a
color_theme
map, and using that to generate a
json
colors file for Tailwind/Tails and even custom themed (light & dark)
Makeup
css. π
The components go from whole-page level down to smaller things like forms, filter forms for queries, and datatables. Forms support groups & nesting, and Iβm currently working on a wizard syntax. Still working on finishing up form relationships & autocomplete components, that should be ready soon. I have an awesome datatable component in another project, I just need to port it over to this lib.
Iβve been working for quite some time on this, and Iβm coming close to releasing this publicly under MIT. Iβm interested in opening limited access to some brave souls that want to dig in and help get this to the beta phase. Particularly those are somewhat familiar with writing Ash extensions and/or
.heex
components and willing to submit PRs. This really isnβt ready to be used unless you want to get your hands dirty β lots of stuff is missing!
If youβre interested in jumping in, let me know via a DM and Iβll consider giving you GitHub access within the next few days. π



frankdugan3:
^ One thing to note in the default overrides image is that the class overrides are functions that are passed all the componentβs assigns, so you can get very specific! There is a
merge_classes
helper that also runs them through
Tails.classes
, passing in the components class props as well. This means your custom overrides can still be reliably overridden by props without weird Tailwind conflicts. π
zachdaniel:
Very very excited about this!!! Will absolutely test this out with AshHq and could maybe even see rewriting ash admin with these standardized components!
frankdugan3:
Did a little cleanup on the override system, and I think Iβm pretty happy with it! As an example:
defmodule Phlegethon.Components.Form.Label do
use Phlegethon.Components.Base,
overrides: [
class: "the class of the label"
]
@doc """
Renders a label.
"""
attr :class, :any, default: nil
attr :for, :string, default: nil
slot :inner_block, required: true
def label(assigns) do
~H"""
<label for={@for} class={class_for(:class, assigns)}>
<%= render_slot(@inner_block) %>
</label>
"""
end
end
override Label do
set :class, "block text-sm font-black text-root-fg dark:text-root-fg-dark"
end
The
class_for
macro looks up the override for the module, and figures out how to apply the override (whether itβs a function or simple binary/list). So you can also do e.g.:
set :class, &__MODULE__.label_class/1
# ...
def label_class(assigns) do
#...
It then assumes that it should append the attribute with the same key as the override to the end of the class list for runtime overrides, but it also accepts a different key (or list of keys) to get a different override value from the assigns. For example:
# smart_form.ex
defp render_field(%{field: %Phlegethon.Resource.Form.FieldGroup{}} = assigns) do
~H"""
<section class={class_for(:field_group_class, assigns, [:field, :class])}>
This really cleans up the code a lot, ensures classes always run through
Tails.classes
and makes it easier to grep whatβs going on!
As an added bonus, it makes it trivial to make your own components that leverage all that tooling. Just add
use Phlegethon.Components.Base
and set your overrides. π
frankdugan3:
And one more thing: Many of the basic components have the same API as the Phoenix
1.7
core_components
, so you can
probably
swap out
core_components
for
Phlegethon.Components
, and have it work out of the box w/ Phoenix generated LiveViews.
zachdaniel:
That sounds awesome!
.terris:
Wow. This is next level. I will use it. A component library that combines resources and tails is the ash way.. but might need something else like Flowbite or headlessUI
dblack1:
Love the look of this, awesome work! I had to google Phlegethon (turns out Iβm not up on Greek mythology). βRiver of fireβ, long may Ash inspire all the names
frankdugan3:
Yeah, every once in a while I have to do something to justify spending 5 semesters learning Greek. π
frankdugan3:
I have been hard at work grinding out more of the essential stuff. Did a lot of refinement on the override system.
For one thing, the overrides are set at compile time via application config, so given
config.exs
:
config :phlegethon, :overrides, [MyApp.CustomOverrides, Phlegethon.Overrides.Default]
The component is able to access them at compile time, allowing validation in attributes:
attr :color, :string,
default: @override_for[:default_color],
values: @override_for[:colors],
doc: "the color of the progress bar"
And a public function is also provided, allowing runtime access as well:
<%= for color <- Progress.override_for(:colors) do %>
This took quite a bit of refactoring, so Iβm glad I havenβt opened it just yet as that would have been painful for everyone else to endure. π
frankdugan3:
Another thing Iβm quite proud of is I think I came up w/ some great improvements to the Phoenix Flash message component. I made this component a little smarter in that it can accept JSON encoded strings, and it extracts some info from them, such as
ttl
(time-to-live for auto-closing flashes) and also a title. This allows for some really nice flash messages. This is all done w/ a hook so itβs snappy on the client-side, and it also intelligently resets the
ttl
if the content of the flash has changed, so you donβt get a quick-close if you overwrite the same flash type. All of this is configurable, of course.
To use the JSON flash, thereβs a simple encoder function:
socket
|> put_flash(
:success,
encode_flash(~s|User "#{user.name}" successfully created!|, title: "User Generated")
)


frankdugan3:
Putting a lot of effort into generating good docs. π

frankdugan3:
Well, after a long day/night and a lot of caffeine, I added an
override
prop to
Phlegethon.Component
(by extending/rewriting half of
Phoenix.Component.Declarative
). π₯
defmodule Phlegethon.Components.Alert do
use Phlegethon.Component
@doc """
A generic alert component.
"""
@doc type: :component
overridable :color, :string,
required: true,
values: :colors
doc: "the color of the alert"
overridable :class, :class
attr :rest, :global
slot :inner_block, required: true, doc: "the content of the alert"
def alert(assigns) do
~H"""
<div class={@class} {@rest}>
<%= render_slot(@inner_block) %>
</div>
"""
end
end
override Alert, :alert do
set :class, &__MODULE__.alert_class/1
set :color, "info"
set :colors, ~w[info success warning danger]
end
So a few notes about the behavior/options:
- All overrides get merged in as the default value
-
required: true
option will raise an error if no overrides provide a value β at compile time! π -
values
option can be either an atom, which will pull in overrides for it, or a list, which cannot be overridden - All types can be either the type itself, or an arrity 1 function that accepts assigns and returns the type itself
-
Adds the
:class
type, which additionally handles passing in assigns and running if itβs a function, and runs it throughTails.classes
Edit: hit enter too early, lol
zachdaniel:
π₯ π₯ π₯
zachdaniel:
That looks really awesome
zachdaniel:
gimme gimme gimme π
frankdugan3:
HalfLife 3 soon. π
frankdugan3:
I uhh⦠learned a lot about macros doing this, lol
frankdugan3:
A demo:
override Core, :alert do
set :class, &__MODULE__.alert_class/1
# set :color, "info"
set :colors, ~w[info success warning danger]
end
== Compilation error in file lib/phlegethon/components/core.ex ==
** (CompileError) lib/phlegethon/components/core.ex:15: cannot find an override setting for :color, please ensure you define one in a configured override module
And when corrected, notice the
default
and
values
extraction:
%{
doc: "the color of the alert",
line: 15,
name: :color,
opts: [default: "info", values: ["info", "success", "warning", "danger"]],
required: true,
type: :string
}
zachdaniel:
Question: can child components be overriden with this system? Or non-simple values like functions? Wondering if we could use this to get some of the flexibility we want in ash_authentication_phoenix
zachdaniel:
Since you essentially modeled it after what <@346791515923939328> did there
zachdaniel:
(but it seems have spent much more time on that part of the pattern)
zachdaniel:
Just wondering if there is some synergy that can be achieved with this.
frankdugan3:
-
Basically if you define an override prop, the logic for generic types is this:
prop || override || nil
. -
For CSS types, it ends up like this:
Tails.classes(override || nil, prop)
. -
If you mark an override prop as
required
, it will throw an error if you donβt have an override defined in some included override file -
ALL override types can be functions, in which case they will be passed the assigns:
The assigns get merged in this order: normal defaults -> prop assigns -> override functions -> class type override functions.def merge_function_overrides(assigns, []), do: assigns def merge_function_overrides(assigns, [{name, override} | overrides]) do if Map.has_key?(assigns, name) do assigns else Map.put( assigns, name, case override do function when is_function(function, 1) -> apply(function, [assigns]) other -> other end ) end |> merge_function_overrides(overrides) end def merge_class_overrides(assigns, []), do: assigns def merge_class_overrides(assigns, [{name, override} | overrides]) do assigns |> Map.put( name, Tails.classes([ case override do function when is_function(function, 1) -> apply(function, [assigns]) other -> other end, assigns[name] || nil ]) ) |> merge_class_overrides(overrides) end
frankdugan3:
%{unquote_splicing(defaults)}
|> Map.merge(assigns)
|> merge_function_overrides(unquote(function_overrides))
|> merge_class_overrides(unquote(class_overrides))
|> Map.put(:__given__, assigns)
frankdugan3:
One big difference from the AshAuthenticationPhoenix override system is that it does not support passing in runtime overrides, just overriding via props.
frankdugan3:
In practice, Iβm not sure if that matters or not.
frankdugan3:
But I needed compile-time override definitions to make components behave the way I wanted.
zachdaniel:
oh, interesting.
frankdugan3:
Also, at this point, I donβt think there is any shared implementation code. Itβs more βinspired byβ at this point, lol
zachdaniel:
Makes sense. I think there would likely still be some potential synergy there
frankdugan3:
Iβm a bit biased, but I think this could make quite a splash. In Phoenix-land, even outside Ash. π
zachdaniel:
I think you are very much correct π May also be a big draw for people to use Ash if its got stuff to integrate in fancy ways
zachdaniel:
Is the stuff like smart form in its own separate dependency? Or is it all one package?
frankdugan3:
ATM, Ash is a hard dependency, but thatβs just because I havenβt looked up how to do optional dependencies/optional compilation. Should be no reason why it would need Ash, and I have the smart components in separate modules that could optionally compile.
zachdaniel:
Honestly, its pretty much just what it sounds like
frankdugan3:
I plan to put it all in the same package, but to have optional deps.
zachdaniel:
you mark the dep as optional, and then wrap those modules in
if Code.ensure_loaded?(Ash) do
frankdugan3:
Cool. π
frankdugan3:
How about testing?
zachdaniel:
Well, in your test/dev environments the dep is always present
frankdugan3:
Marking it as optional only count for
:prod
env?
zachdaniel:
I donβt know the best way to test without it though
zachdaniel:
yeah, exactly
frankdugan3:
Awesome.
zachdaniel:
Youβre using spark for the DSLs right?
zachdaniel:
Ah, I guess you arenβt
zachdaniel:
because youβve got top level things
zachdaniel:
π’
frankdugan3:
For the Ash extension? Yeah. Not for the overrides, though.
frankdugan3:
Would it make sense to?
zachdaniel:
If we supported top level builders, yeah
zachdaniel:
but alas, we do not
frankdugan3:
Ahh, right.
zachdaniel:
Iβd add it though, just for you
frankdugan3:
Well, itβs pretty simple anyway. Would be no reason not to swap later, Iβve already done all the work for it at this point, lol
frankdugan3:
You can take a look soon, open to any feedback on it,.
frankdugan3:
Once I finish this refactor of the core components w/ my new API, Iβll give you access even though itβs still a bit of a mess in a lot of places. π
jharton:
Iβd add it though, just for you He really wants to add this
jharton:
he keeps asking me if I want it
zachdaniel:
lol
zachdaniel:
I just donβt want the lack of it to stop people from using it
jharton:
nah.
frankdugan3:
β¦Will you add it and refactor to use it? π
jharton:
Iβm going to wind up with a DSL like:
component do
prop :foo
slot :bar
event :wat
end
jharton:
itβs fine
zachdaniel:
lol, probably not
zachdaniel:
I could see Ash using it for the basic resource things like:
use Ash.Resource
description "..."
...
frankdugan3:
Thinking it over, perhaps using Spark would be great even if the only benefit I got from it was not having to fight w/ the formatter on re-adding parenthesis every time I have a compile error. lol
jharton:
Now youβre getting it.
frankdugan3:
For those interested, the repo is now public! https://github.com/frankdugan3/phlegethon
There is a lot to work on and lots of stuff half-finished, but I feel like the component API and docs are pretty stable. Here is a list of the top priorities I will be working on this week:
-
Finish up the
Core
components (replacement ofcore_components.ex
- <.modal> needs polish
- <.table> needs polish
-
Flesh out the
<.smart_form>
component -
Port my
<.smart_data_table>
component -
Trim out all the deprecated cruft that doesnβt need to be in
info.ex
(ignore that file for now) and finish re-working the tests to work in the lib (they were from a different app originally) -
Add in the ability to do multi-step forms in the extension and
<.smart_form>
component (aka wizard) - Add a nice autocomplete/multiselect component, and make it ash-smart for use in forms w/ relationship management
frankdugan3:
To clarify the current state, itβs not
quite
usable as a component lib just yet, so this is more of a thing for those interested in development and checking out the
overridable
component API, and the
:class
type. I think it makes for a very composable and clean experience. If you follow the simple dev instructions in the readme, you can check out the component previewer and documentation for yourself. <:ashley:1017613763945443398>
frankdugan3:
I did try and put some effort into decent docs out of the gate, in particular the override modules and components are self-documenting (it extends Phoenix.Component), and any functions used in overridables will link to the appropriate override module that defines them. It should be very easy to click right through any part of a component or override module and find the source right away.





frankdugan3:
And FWIW, the project is already almost 8K LoC! No wonder itβs been taking me so long to get it out the door. π
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Language Files Lines Blanks Comments Code Complexity
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Elixir 63 7198 999 370 5829 396
Markdown 6 331 83 0 248 0
JavaScript 4 234 19 39 176 18
JSON 2 82 0 0 82 0
Shell 2 24 6 2 16 3
CSS 1 5 1 0 4 0
License 1 21 4 0 17 0
gitignore 1 33 10 10 13 0
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Total 80 7928 1122 421 6385 417
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Estimated Cost to Develop (organic) $189,240
Estimated Schedule Effort (organic) 7.31 months
Estimated People Required (organic) 2.30
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Processed 236899 bytes, 0.237 megabytes (SI)
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
frankdugan3:
Been hard at work over the past couple weeks!
The core components is just about βreadyβ to be used after a significant refactor. I no longer hack Phoenix.Component.Declarative (too much maintenance), and instead use a macro to add the overrides at runtime. There are still quite a few compile time checks and optimizations to ensure things work without much head-scratching.
One example is fuzzy-matching on attribute names when it canβt match up attribute with override.
Going for Elm-level errors. π
attr :classy, :any
def flash(assigns) do
assigns =
assigns
|> assign_overridable(:class, class?: true, required?: true)
** (CompileError) lib/phlegethon/components/core.ex:196: Phlegethon.Component - Invalid Overridable Option
* Component: Phlegethon.Components.Core.flash/1
* Prop: attr :class
* Overridable: assign_overridable(:class, ...)
* Reason: unable to find prop :class on this component
* Similar existing prop names: :classy
Currently only "attr" props are supported.
jharton:
Nicely done
frankdugan3:
Found a nice way to cleanly extend
Phoenix.Component.attr/3
, so this is the final(?) component override API:
defmodule MyApp.Components.ExternalLink do
@moduledoc """
An external link component.
"""
use Phlegethon.Component
attr :overrides, :list, default: nil, doc: @overrides_attr_doc
attr :class, :tails_classes, overridable: true, required: true
attr :href, :string, required: true
attr :rest, :global, include: ~w[download hreflang referrerpolicy rel target type]
slot :inner_block, required: true
def external_link(assigns) do
assigns = assign_overridables(assigns)
~H"""
<a class={@class} href={@href}} {@rest}>
<%= render_slot(@inner_block) %>
</a>
"""
end
end
Has compile-time checks to catch any missing calls/incompatible options. The overridable attrs are accumulated and assigned (in order of definition) by
assign_overridables/1
.
All of the components and documentation have been updated to the new new API. Been dog-fooding it and itβs starting to feel pretty good, though I may be biased. π




frankdugan3:
Oh, and the default style is a dark/light variant on the one Phoenix gives you out of the box.


frankdugan3:
Just pushed up the basic version of an autocomplete component, along with support for simple relationships in
SmartForm
.
For simple use:
<.simple_form for={@form} phx-change="validate" phx-submit="save">
<.live_component
module={Phlegethon.Components.Autocomplete}
id="fiend_id_autocomplete"
field={@form[:friend_id]}
label="Friend"
search_fn={search_friends/1}
lookup_fn={lookup_friend/1} />
<:actions>
<.button>Save</.button>
</:actions>
</.simple_form>
There are lots of props/overrides to configure lots of behavior, including a slot for custom templates for the options.
To use an autocomplete for simple relationships in Ash w/ a
SmartForm
:
field :best_friend_id do
label "Best Friend"
type :autocomplete
prompt "Search friends for your bestie"
autocomplete_option_label_key :name_email
end
argument :best_friend_id, :uuid
change manage_relationship(:best_friend_id, :best_friend, type: :append_and_remove)
belongs_to :best_friend, __MODULE__, api: ComponentPreviewer.Ash.Api
It makes use of hooks and
Phoenix.JS
so that all the things like moving selection, intercepting key events etc. are all done client-slide for a snappy feel.
It should also be relatively accessible, but Iβm no expert on that. π€
Thereβs still a lot more to add, such as loading indicators, supporting multiple entries, supporting more complex relationship types, and allowing templates for adding new entries on the fly. And of course all the bugs that will naturally pop up. Will be adding to it bit-by-bit!
.terris:
Would it be possible to create a channel for phlegethon or do you want questions/discussion in github issues?
zachdaniel:
Where do you stand on this <@433654314175692800> ? Should I make a phlegethon channel or a maple ui channel π
frankdugan3:
Hmmβ¦ I think stick w/ phlegethon channel for now. I donβt want to change it twice, and I havenβt made up my mind on maple ui as a name yet. π
zachdaniel:
<#1094268769641185370>
frankdugan3:
I donβt mind questions/discussion on either, but if there is an existing issue on the topic, would be better to continue it on the issue to keep it all in one place.
.terris:
Maple is easier than phegle.. <checks notes>
.terris:
I posted my findings in <#1094268769641185370> which Iβm quickly learning how to spell.