marschro

marschro

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 under Custom.

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

Showing Posts 1 to 2

marschro

marschro OP

So how I understand it, the problem with this concept is,

  • that the theme modules which host all the function components (UI) are hard-coded in PortalWeb.
  • If you change the theme files (for example just changing a style class) then this needs to recompile PortalWeb which then forces to recompile 62 other files (in my situation) - because PortalWeb is mighty and the whole Web part of the app basically depends on it…

So I am searching for a solution which gives that functionality to compile the app with different UI components but not blocking this central part.

So I somehow have to find a different approach, that does not do the decision making which theme to use in the central PortalWeb module.

marschro

marschro OP

Okay, I found an approach that

  • works
  • is not over engineered
  • I can use custom components in combination with the core_components
  • it important keeps every mechanism out of PortalWeb
  • I reduced compile-time cycles to ZERO and
  • have instant recompilation back for custom components

… so I mark is as solved, but do not hesitate to leave any comments if this is good practice or hint me to some points I haven’t considered..

And special thanks for supporting in resolving compile-cycles goes to @LostKobrakai !!!

Approach

  1. I removed this unquote resolve_theme logic completely from PortalWeb
  2. Introduced a “Custom” Theme-Wrapper that looks like this:
defmodule PortalWeb.Applications.Wcm.Sites.Frontend.Theme.Custom do
  @theme Application.compile_env(:portal, :theme)

  alias PortalWeb.Applications.Wcm.Sites.Frontend.Theme.Default
  alias PortalWeb.Applications.Wcm.Sites.Frontend.Theme.Devpunx

  case @theme do
    :default ->
      defdelegate brand_logo(assigns), to: Default
      defdelegate button(assigns), to: Default
      defdelegate content_block(assigns), to: Default
      #... and all other custom component functions

    :devpunx ->
      defdelegate brand_logo(assigns), to: Devpunx
      defdelegate button(assigns), to: Devpunx
      defdelegate content_block(assigns), to: Devpunx
      #... and all other custom component functions
  end

  def theme, do: @theme
end

Usage

  • In the live_components, views and layouts I want to use the currently configured custom theme, I just do:
...
alias PortalWeb.Applications.Wcm.Sites.Frontend.Theme.Custom
...

def render(assigns) do
    ~H"""
    <div>
      <Custom.modal :if={@update_needed} show={true} id={"update-needed-for-asset-#{@id}"}>
        <:header>

— All posts loaded —

Where Next? Top

Trending in Questions Top

katta
I having some trouble figuring out if I have set myself too strict of standards for my production server. Currently I can handle 75% of r...
New
achenet
Hello, I’m trying to build a basic Phoenix web-app, and I’d like to use Tailwind. However, when I launch mix phx.server, I get an error...
New
Cxx-mlr
I’m working on a small exercise involving update_in/3, and I came up with this solution: data = %{ name: "Periodic Table", category:...
New
ChrisAmelia
I’ve got trouble wrapping my head around the order in which functions are called in this snippet (from Phoenix’s authentication): toke...
New
dillonoconnor
Is there any way to avoid the Hologram compiler running when using iex? It seems like the front-end code could potentially be disregarded...
New
thiagogsr
** (ArgumentError) expected :max_attempts to be a positive integer, got: {:@, [line: 10, column: 19], [{:max_attempts, [line: 10, column:...
New
unaware8150
Hello folks! So at work, we are seeing some situations where we have to define some “fixed” strings that are used across the codebase in...
New

Other Trending Topics Top

GenericJam
Edit: 2026 May 15 - This post is archived. Mob is alive!! Main docs: mob v0.7.11 — Documentation A bit of explanation for the slightly c...
New
mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
budgie
A little off-topic, but I feel like people here have a good head on their shoulders. I used to be quite good at making software. Was luc...
New
KristerV
Hey. Is there anyone here who creates agents in their apps? Not talking about using agents, but creating them. I’m finding it pretty diff...
New
mcass19
ExRatatui lets you cook up rich terminal UIs in Elixir, powered by Rust’s ratatui via Rustler NIFs. Build interactive terminal applicatio...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews