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

stjefim
Hello! Suppose you are building workflow (order / task / payment) processing system with the following requirements: Each workflow con...
New
itzmidinesh
Hi all, I’m working on a Phoenix project where I need to make external HTTP requests and I’m trying to choose between Req and HTTPoison....
New
pragdave
How do I tell Hologram not to escape HTML when substituting into templates? {Textile.render(@product.long_description)} escapes the HTM...
New
rlipscombe
I’d like to create a file with 600 permissions (rw-------), but there’s a race condition if I create it and then change the permissions. ...
New
leoschweizer
Hi, we run different LLM models across a cluster, with each node loading a configurable subset. Model-specific queues let nodes consume ...
New
rlipscombe
When I use Mix.install with a git dependency, using git@github.com:... how do I change the SSH identity used? I’ve got two Github accoun...
New
adr
Hello all. I have a question as to best practices for code organization when implementing a liveview with a lot of responsibilities. Sup...
New

Other Trending Topics Top

dominicletz
Hey there, I haven’t seen a list of useful Elixir specific skills for the popular AI tools yet so I’m starting one here with my first. ...
New
raa-ziq
Hi everyone :waving_hand: I wanted to share something I’ve been building — NowBlind, a random chat app designed to help people discover ...
New
mudasobwa
I fully migrated to my own harness from Anthropic/Gemini and I think it’s time to share it. Welcome DSH, the DeepSeek Harness, fully writ...
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
OndrejValenta
(I just needed to vent somewhere and LinkedIn is full of hope, or hype, I’m not sure which exactly) AI dream has many faces, but general...
New
kip
Localize is the next generation localisation library for Elixir. Think of it as ex_cldr version 3.0. The first version will be released ...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews