marschro

marschro

Application with Multiple Configurable UI Themes

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

First Post!

marschro

marschro

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.

Last Post!

marschro

marschro

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>

Where Next?

Popular in Questions Top

Qqwy
Original source of discussion: This topic on the Pragmatic Programmers’ Functional Web Development with Elixir, OTP, and Phoenix forum. ...
New
openscript
Hello! Sorry for this astonishing simple question, but I’m really stuck. I try to set up the intellij-elixir plugin, but I don’t know ho...
New
dokuzbir
I want to highlight html closing tags when i click a html tag. That works in .html files but doesnt work for html.eex templates. How can...
New
sergio_101
I am VERY much an elixir newbie. I have taken one elixir course and one phoenix course on Udemy. During that course, I saw the instructor...
New
shijith.k
I am trying to start a new phoenix project with elixir 1.9, but mix phx.new does not work. It says that ** (Mix) The task "phx.new" could...
New
pmjoe
I have a relationship of love and hate with Elixir. Lots of things are just absolutely right, but there are some things that are kind of ...
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New

Other popular topics Top

vertexbuffer
Hello, can anybody help here..? I have a list of players and I what to delete an element, but every for loop the list is reverting to ori...
New
Darmani72
If I have a post route which an argument: post /my_post_route/:my_param1, MyController.my_post_handler How would get the post params ...
New
gausby
I asked this very same question on twitter and got some interesting feedback, but I thought it would be a good question to ask here as we...
1207 40165 209
New
shijith.k
I am trying to start a new phoenix project with elixir 1.9, but mix phx.new does not work. It says that ** (Mix) The task "phx.new" could...
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
AstonJ
Posting this to see if we can make things easier for people to get into Neovim. If you use Neovim and have a favourite distro please let ...
New

We're in Beta

About us Mission Statement