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
Blokh
Hey guys, I’ve got a huge CSV ( around 10 GB ) that needs to be processed hourly Do you guys have any suggestions what is the best prac...
New
kszambelanczyk
Hello! Could someone please give me a help/sample code, how to delete a file from s3 using waffle/waffle_ecto from Phoenix app. I creat...
New
Onor.io
I have what I’ve heard referred to as a “lookup table” in my database. This is a way of assigning codes to common values. One common lo...
New
jaybe78
Hello, I’m developing a online persistent chat system (what’s app) like using elixir/dynamodb/aws for a mobile app(flutter). The diffic...
New
Trolleger
What approach to take when sending live updates to “random” users Hi! I have a question, I have a little chat app, and when I create a DM...
New
widianto
I think I’ve found a small improvement I could contribute to &lt;%= web_namespace %&gt;.CoreComponents (installer/templates/phx_web/compo...
New

Other Trending Topics Top

garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
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
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge &amp; Solve. They are GUI (Emerge) and State management (S...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New
wintermeyer
There are three potential reasons for members of this forum to have a look at https://vutuv.de You are tired or annoyed of LinkedIn. Yo...
New
webofbits
Aludel - LLM Evaluation Workbench Aludel is an embeddable Phoenix LiveView dashboard for evaluating and comparing LLM prompts across mult...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews