Hi all,
I build an application that can be configured during build time to use different sets of Phoenix UI Components.
The whole setup looks like this (app name is :portal - how creative…):
Configuration in config/dev.exs
config :portal, theme: :default
- this sets the theme named “default” during compile time
Configuration in config/prod.exs
# --------------------------
# THEME CONFIG
# --------------------------
#
# can be one of: [:default, :devpunx]
app_theme =
case System.get_env("APP_THEME") do
"" -> :default
nil -> :default
val -> String.to_atom(val)
end
## config :portal, theme: app_theme
- When compiling a release for production, env variable APP_THEME can be used to build the app with different themes
How does it come into place?
For example live_compoentns do use PortalWeb, :live_component
…
def live_component do
quote do
use Phoenix.LiveComponent
unquote(html_helpers())
unquote(utils())
end
end
… which includes html_helpers like this:
defp html_helpers do
quote do
use Gettext, backend: PortalWeb.Gettext
import Phoenix.HTML
import PortalWeb.CoreComponents
alias unquote(resolve_theme(@theme)), as: Custom
alias Phoenix.LiveView.JS
unquote(verified_routes())
end
end
… which resolves the theme, where theme is a compile time module attribute
@theme Application.compile_env!(:portal, :theme)
… and is resolved like that (currently just two themes)
defp resolve_theme(theme) do
case theme do
:devpunx -> PortalWeb.Applications.Wcm.Sites.Frontend.Ui.Devpunx
:default -> PortalWeb.Applications.Wcm.Sites.Frontend.Ui.Default
_ -> PortalWeb.Applications.Wcm.Sites.Frontend.Ui.Default
end
end
The libraries
… are then just regular component libraries as we know from core_components
Like so:
defmodule PortalWeb.Applications.Wcm.Sites.Frontend.Ui.Default do
use PortalWeb, :components_foundation
use PortalWeb, :verified_routes
alias PortalWeb.CoreComponents, as: Core
alias PortalWeb.Applications.Wcm.Sites.Frontend.Commons.Utils, as: FrontendUtils
alias Portal.Applications.Wcm.Blocks
alias Portal.Applications.Wcm.Pages
@wcm_config Application.compile_env(:portal, [:apps, :wcm])
def theme, do: "#{__MODULE__}" |> String.split(".") |> List.last()
slot(:inner_block, required: true)
def page_body(assigns) do
~H"""
<body class="antialiased mx-auto bg-slate-900">
{render_slot(@inner_block)}
</body>
"""
end
... # and so on till the end
How to use?
And then I can simply use these themes in the common live_components for example like this:
- I can use them now with the prefix
Custom
as I aliased them during compile time…
def render(assigns) do
~H"""
<div data-sortable-block-id={@id} class="mx-auto w-full p-4 md:p-6 md:max-w-6xl">
<Custom.sortable_item id={"content-block-#{@id}"} data-id={@id} edit={@block.edit}>
<Custom.content_block_meta block={@block} target={@myself} active_tab={@active_tab} />
<Custom.content_block
id={"block-#{@block.id}"}
block={@block}
type={@block.type}
edit_mode={true}
/>
</Custom.sortable_item>
<.modal id={"confirm-deleting-block#{@id}"}>
<:header>
Do you realy want to delete this block?
</:header>
<:body>
This is ths last used instance of this block. Its not referenced on any other page.
The content block will be deleted.
</:body>
<:footer>
<.button phx-click={hide_modal("confirm-deleting-block#{@id}")}>
Cancel
</.button>
<.button
kind={:caution}
phx-disable-with="Deleting..."
phx-click={hide_modal("confirm-deleting-block#{@id}") |> JS.push("delete_block")}
phx-target={@myself}
>
Delete
</.button>
</:footer>
</.modal>
The good!
- this actually works.
Of course the themes have to have always the same function components. But I can use these custom components besides my core library.
And I only have to add a new theme-library without changing any live_view as the currently build theme is always available underCustom
.
The bad!
- This somehow feels not right and maybe someone has a way nicer concept
- This introduces Compile time cycles that I can not get rid of… I even haven’t understood where this happens… its a bit spaghetti…
Any experience, hint, comment on this is appreciated.
Maybe I should use using macro to do that and not alias it … but then I have conflicts with component names.
One convenience is, that I can Adress <.Custom.button>
from the theme and still use the <.button>
from the core_components library.
Hope my issue is a bit understandable. I need an approach to do that kind of theming in a nice elegant ways that does not introduce compile time cycles. Also since I introduced that compile time dribbled or so…
kind regards!
Martin